Merge branch 'stable-3.12' into stable-3.13

* stable-3.12:
  Bump JGit submodule to the tip of stable-7.5
  Bazel: Move Gerrit off JGit servlet-4 via EE8 bridge
  Update git submodules

Release-Notes: skip
Change-Id: If2d26b219aefef34e4879f88643a70915c104e20
diff --git a/.bazelproject b/.bazelproject
index c3d0591..86e568d 100644
--- a/.bazelproject
+++ b/.bazelproject
@@ -23,10 +23,6 @@
 build_flags:
   --javacopt=-g
 
-additional_languages:
-  javascript
-  typescript
-
 ts_config_rules:
   //tools/node_tools/node_modules_licenses:tsconfig_editor
   //tools/node_tools/polygerrit_app_preprocessor:preprocessor_tsconfig.json
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 90a5c9b..c8f9926 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1555,7 +1555,9 @@
 
 Allow users to impersonate any other user with the `X-Gerrit-RunAs`
 HTTP header on REST API calls, or the link:cmd-suexec.html[suexec]
-SSH command.
+SSH command. When having Run As capability the user is considered to implicitly
+have other more narrow impersonation capabilities as well, such as
+Submitting/Voting on behalf of another user.
 
 When impersonating an administrator the Administrate Server capability
 is not honored.  This security feature tries to prevent a role with
@@ -1641,7 +1643,7 @@
 Allows viewing secondary emails of other accounts.
 
 Users with the link:#capability_modifyAccount[Modify Account] capability have
-this capbility implicitly.
+this capability implicitly.
 
 
 [[reference]]
diff --git a/Documentation/backend_licenses.txt b/Documentation/backend_licenses.txt
index ceb7d7c..feaa137 100755
--- a/Documentation/backend_licenses.txt
+++ b/Documentation/backend_licenses.txt
@@ -32,7 +32,7 @@
 Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
 uploads of changes directly from `git push` command line clients.
 
-Gerrit includes an SSH client (JSch), to support authenticated
+Gerrit includes an SSH client (Apache SSHD), to support authenticated
 replication of changes to remote systems, such as for automatic
 updates of mirror servers, or realtime backups.
 
@@ -42,16 +42,17 @@
 [[Apache2_0]]
 Apache2.0
 
+* auto:auto-factory
 * auto:auto-value
 * auto:auto-value-annotations
 * auto:auto-value-gson
 * commons:codec
 * commons:compress
 * commons:dbcp
-* commons:lang
 * commons:lang3
 * commons:net
 * commons:pool
+* commons:text
 * commons:validator
 * dropwizard:dropwizard-core
 * errorprone:annotations
@@ -60,6 +61,7 @@
 * guice:guice-assistedinject
 * guice:guice-library
 * guice:guice-servlet
+* guice:jakarta-inject
 * guice:javax_inject
 * httpcomponents:httpclient
 * httpcomponents:httpcore
@@ -73,7 +75,8 @@
 * jetty:util-ajax
 * log:log4j
 * lucene:lucene-analyzers-common
-* lucene:lucene-core-and-backward-codecs-merged
+* lucene:lucene-backward-codecs
+* lucene:lucene-core
 * lucene:lucene-misc
 * lucene:lucene-queryparser
 * mime4j:core
@@ -96,6 +99,7 @@
 * javaewah
 * jsr305
 * mime-util
+* roaringbitmap
 * servlet-api
 * servlet-api-without-neverlink
 * soy
@@ -308,620 +312,6 @@
 ----
 
 
-[[CC0-1_0]]
-CC0-1.0
-
-* mina:eddsa
-
-[[CC0-1_0_license]]
-----
-Creative Commons Legal Code
-
-CC0 1.0 Universal
-
-    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
-    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
-    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
-    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
-    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
-    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
-    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
-    HEREUNDER.
-
-Statement of Purpose
-
-The laws of most jurisdictions throughout the world automatically confer
-exclusive Copyright and Related Rights (defined below) upon the creator
-and subsequent owner(s) (each and all, an "owner") of an original work of
-authorship and/or a database (each, a "Work").
-
-Certain owners wish to permanently relinquish those rights to a Work for
-the purpose of contributing to a commons of creative, cultural and
-scientific works ("Commons") that the public can reliably and without fear
-of later claims of infringement build upon, modify, incorporate in other
-works, reuse and redistribute as freely as possible in any form whatsoever
-and for any purposes, including without limitation commercial purposes.
-These owners may contribute to the Commons to promote the ideal of a free
-culture and the further production of creative, cultural and scientific
-works, or to gain reputation or greater distribution for their Work in
-part through the use and efforts of others.
-
-For these and/or other purposes and motivations, and without any
-expectation of additional consideration or compensation, the person
-associating CC0 with a Work (the "Affirmer"), to the extent that he or she
-is an owner of Copyright and Related Rights in the Work, voluntarily
-elects to apply CC0 to the Work and publicly distribute the Work under its
-terms, with knowledge of his or her Copyright and Related Rights in the
-Work and the meaning and intended legal effect of CC0 on those rights.
-
-1. Copyright and Related Rights. A Work made available under CC0 may be
-protected by copyright and related or neighboring rights ("Copyright and
-Related Rights"). Copyright and Related Rights include, but are not
-limited to, the following:
-
-  i. the right to reproduce, adapt, distribute, perform, display,
-     communicate, and translate a Work;
- ii. moral rights retained by the original author(s) and/or performer(s);
-iii. publicity and privacy rights pertaining to a person's image or
-     likeness depicted in a Work;
- iv. rights protecting against unfair competition in regards to a Work,
-     subject to the limitations in paragraph 4(a), below;
-  v. rights protecting the extraction, dissemination, use and reuse of data
-     in a Work;
- vi. database rights (such as those arising under Directive 96/9/EC of the
-     European Parliament and of the Council of 11 March 1996 on the legal
-     protection of databases, and under any national implementation
-     thereof, including any amended or successor version of such
-     directive); and
-vii. other similar, equivalent or corresponding rights throughout the
-     world based on applicable law or treaty, and any national
-     implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention
-of, applicable law, Affirmer hereby overtly, fully, permanently,
-irrevocably and unconditionally waives, abandons, and surrenders all of
-Affirmer's Copyright and Related Rights and associated claims and causes
-of action, whether now known or unknown (including existing as well as
-future claims and causes of action), in the Work (i) in all territories
-worldwide, (ii) for the maximum duration provided by applicable law or
-treaty (including future time extensions), (iii) in any current or future
-medium and for any number of copies, and (iv) for any purpose whatsoever,
-including without limitation commercial, advertising or promotional
-purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
-member of the public at large and to the detriment of Affirmer's heirs and
-successors, fully intending that such Waiver shall not be subject to
-revocation, rescission, cancellation, termination, or any other legal or
-equitable action to disrupt the quiet enjoyment of the Work by the public
-as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason
-be judged legally invalid or ineffective under applicable law, then the
-Waiver shall be preserved to the maximum extent permitted taking into
-account Affirmer's express Statement of Purpose. In addition, to the
-extent the Waiver is so judged Affirmer hereby grants to each affected
-person a royalty-free, non transferable, non sublicensable, non exclusive,
-irrevocable and unconditional license to exercise Affirmer's Copyright and
-Related Rights in the Work (i) in all territories worldwide, (ii) for the
-maximum duration provided by applicable law or treaty (including future
-time extensions), (iii) in any current or future medium and for any number
-of copies, and (iv) for any purpose whatsoever, including without
-limitation commercial, advertising or promotional purposes (the
-"License"). The License shall be deemed effective as of the date CC0 was
-applied by Affirmer to the Work. Should any part of the License for any
-reason be judged legally invalid or ineffective under applicable law, such
-partial invalidity or ineffectiveness shall not invalidate the remainder
-of the License, and in such case Affirmer hereby affirms that he or she
-will not (i) exercise any of his or her remaining Copyright and Related
-Rights in the Work or (ii) assert any associated claims and causes of
-action with respect to the Work, in either case contrary to Affirmer's
-express Statement of Purpose.
-
-4. Limitations and Disclaimers.
-
- a. No trademark or patent rights held by Affirmer are waived, abandoned,
-    surrendered, licensed or otherwise affected by this document.
- b. Affirmer offers the Work as-is and makes no representations or
-    warranties of any kind concerning the Work, express, implied,
-    statutory or otherwise, including without limitation warranties of
-    title, merchantability, fitness for a particular purpose, non
-    infringement, or the absence of latent or other defects, accuracy, or
-    the present or absence of errors, whether or not discoverable, all to
-    the greatest extent permissible under applicable law.
- c. Affirmer disclaims responsibility for clearing rights of other persons
-    that may apply to the Work or any use thereof, including without
-    limitation any person's Copyright and Related Rights in the Work.
-    Further, Affirmer disclaims responsibility for obtaining any necessary
-    consents, permissions or other rights required for any use of the
-    Work.
- d. Affirmer understands and acknowledges that Creative Commons is not a
-    party to this document and has no duty or obligation with respect to
-    this CC0 or use of the Work.
-
-For more information, please see https://creativecommons.org/publicdomain/zero/1.0/
-
-----
-
-
-[[MPL1_1]]
-MPL1.1
-
-* juniversalchardet
-
-[[MPL1_1_license]]
-----
-                          MOZILLA PUBLIC LICENSE
-                                Version 1.1
-
-                              ---------------
-
-1. Definitions.
-
-     1.0.1. "Commercial Use" means distribution or otherwise making the
-     Covered Code available to a third party.
-
-     1.1. "Contributor" means each entity that creates or contributes to
-     the creation of Modifications.
-
-     1.2. "Contributor Version" means the combination of the Original
-     Code, prior Modifications used by a Contributor, and the Modifications
-     made by that particular Contributor.
-
-     1.3. "Covered Code" means the Original Code or Modifications or the
-     combination of the Original Code and Modifications, in each case
-     including portions thereof.
-
-     1.4. "Electronic Distribution Mechanism" means a mechanism generally
-     accepted in the software development community for the electronic
-     transfer of data.
-
-     1.5. "Executable" means Covered Code in any form other than Source
-     Code.
-
-     1.6. "Initial Developer" means the individual or entity identified
-     as the Initial Developer in the Source Code notice required by Exhibit
-     A.
-
-     1.7. "Larger Work" means a work which combines Covered Code or
-     portions thereof with code not governed by the terms of this License.
-
-     1.8. "License" means this document.
-
-     1.8.1. "Licensable" means having the right to grant, to the maximum
-     extent possible, whether at the time of the initial grant or
-     subsequently acquired, any and all of the rights conveyed herein.
-
-     1.9. "Modifications" means any addition to or deletion from the
-     substance or structure of either the Original Code or any previous
-     Modifications. When Covered Code is released as a series of files, a
-     Modification is:
-          A. Any addition to or deletion from the contents of a file
-          containing Original Code or previous Modifications.
-
-          B. Any new file that contains any part of the Original Code or
-          previous Modifications.
-
-     1.10. "Original Code" means Source Code of computer software code
-     which is described in the Source Code notice required by Exhibit A as
-     Original Code, and which, at the time of its release under this
-     License is not already Covered Code governed by this License.
-
-     1.10.1. "Patent Claims" means any patent claim(s), now owned or
-     hereafter acquired, including without limitation,  method, process,
-     and apparatus claims, in any patent Licensable by grantor.
-
-     1.11. "Source Code" means the preferred form of the Covered Code for
-     making modifications to it, including all modules it contains, plus
-     any associated interface definition files, scripts used to control
-     compilation and installation of an Executable, or source code
-     differential comparisons against either the Original Code or another
-     well known, available Covered Code of the Contributor's choice. The
-     Source Code can be in a compressed or archival form, provided the
-     appropriate decompression or de-archiving software is widely available
-     for no charge.
-
-     1.12. "You" (or "Your")  means an individual or a legal entity
-     exercising rights under, and complying with all of the terms of, this
-     License or a future version of this License issued under Section 6.1.
-     For legal entities, "You" includes any entity which controls, is
-     controlled by, or is under common control with You. For purposes of
-     this definition, "control" means (a) the power, direct or indirect,
-     to cause the direction or management of such entity, whether by
-     contract or otherwise, or (b) ownership of more than fifty percent
-     (50%) of the outstanding shares or beneficial ownership of such
-     entity.
-
-2. Source Code License.
-
-     2.1. The Initial Developer Grant.
-     The Initial Developer hereby grants You a world-wide, royalty-free,
-     non-exclusive license, subject to third party intellectual property
-     claims:
-          (a)  under intellectual property rights (other than patent or
-          trademark) Licensable by Initial Developer to use, reproduce,
-          modify, display, perform, sublicense and distribute the Original
-          Code (or portions thereof) with or without Modifications, and/or
-          as part of a Larger Work; and
-
-          (b) under Patents Claims infringed by the making, using or
-          selling of Original Code, to make, have made, use, practice,
-          sell, and offer for sale, and/or otherwise dispose of the
-          Original Code (or portions thereof).
-
-          (c) the licenses granted in this Section 2.1(a) and (b) are
-          effective on the date Initial Developer first distributes
-          Original Code under the terms of this License.
-
-          (d) Notwithstanding Section 2.1(b) above, no patent license is
-          granted: 1) for code that You delete from the Original Code; 2)
-          separate from the Original Code;  or 3) for infringements caused
-          by: i) the modification of the Original Code or ii) the
-          combination of the Original Code with other software or devices.
-
-     2.2. Contributor Grant.
-     Subject to third party intellectual property claims, each Contributor
-     hereby grants You a world-wide, royalty-free, non-exclusive license
-
-          (a)  under intellectual property rights (other than patent or
-          trademark) Licensable by Contributor, to use, reproduce, modify,
-          display, perform, sublicense and distribute the Modifications
-          created by such Contributor (or portions thereof) either on an
-          unmodified basis, with other Modifications, as Covered Code
-          and/or as part of a Larger Work; and
-
-          (b) under Patent Claims infringed by the making, using, or
-          selling of  Modifications made by that Contributor either alone
-          and/or in combination with its Contributor Version (or portions
-          of such combination), to make, use, sell, offer for sale, have
-          made, and/or otherwise dispose of: 1) Modifications made by that
-          Contributor (or portions thereof); and 2) the combination of
-          Modifications made by that Contributor with its Contributor
-          Version (or portions of such combination).
-
-          (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
-          effective on the date Contributor first makes Commercial Use of
-          the Covered Code.
-
-          (d)    Notwithstanding Section 2.2(b) above, no patent license is
-          granted: 1) for any code that Contributor has deleted from the
-          Contributor Version; 2)  separate from the Contributor Version;
-          3)  for infringements caused by: i) third party modifications of
-          Contributor Version or ii)  the combination of Modifications made
-          by that Contributor with other software  (except as part of the
-          Contributor Version) or other devices; or 4) under Patent Claims
-          infringed by Covered Code in the absence of Modifications made by
-          that Contributor.
-
-3. Distribution Obligations.
-
-     3.1. Application of License.
-     The Modifications which You create or to which You contribute are
-     governed by the terms of this License, including without limitation
-     Section 2.2. The Source Code version of Covered Code may be
-     distributed only under the terms of this License or a future version
-     of this License released under Section 6.1, and You must include a
-     copy of this License with every copy of the Source Code You
-     distribute. You may not offer or impose any terms on any Source Code
-     version that alters or restricts the applicable version of this
-     License or the recipients' rights hereunder. However, You may include
-     an additional document offering the additional rights described in
-     Section 3.5.
-
-     3.2. Availability of Source Code.
-     Any Modification which You create or to which You contribute must be
-     made available in Source Code form under the terms of this License
-     either on the same media as an Executable version or via an accepted
-     Electronic Distribution Mechanism to anyone to whom you made an
-     Executable version available; and if made available via Electronic
-     Distribution Mechanism, must remain available for at least twelve (12)
-     months after the date it initially became available, or at least six
-     (6) months after a subsequent version of that particular Modification
-     has been made available to such recipients. You are responsible for
-     ensuring that the Source Code version remains available even if the
-     Electronic Distribution Mechanism is maintained by a third party.
-
-     3.3. Description of Modifications.
-     You must cause all Covered Code to which You contribute to contain a
-     file documenting the changes You made to create that Covered Code and
-     the date of any change. You must include a prominent statement that
-     the Modification is derived, directly or indirectly, from Original
-     Code provided by the Initial Developer and including the name of the
-     Initial Developer in (a) the Source Code, and (b) in any notice in an
-     Executable version or related documentation in which You describe the
-     origin or ownership of the Covered Code.
-
-     3.4. Intellectual Property Matters
-          (a) Third Party Claims.
-          If Contributor has knowledge that a license under a third party's
-          intellectual property rights is required to exercise the rights
-          granted by such Contributor under Sections 2.1 or 2.2,
-          Contributor must include a text file with the Source Code
-          distribution titled "LEGAL" which describes the claim and the
-          party making the claim in sufficient detail that a recipient will
-          know whom to contact. If Contributor obtains such knowledge after
-          the Modification is made available as described in Section 3.2,
-          Contributor shall promptly modify the LEGAL file in all copies
-          Contributor makes available thereafter and shall take other steps
-          (such as notifying appropriate mailing lists or newsgroups)
-          reasonably calculated to inform those who received the Covered
-          Code that new knowledge has been obtained.
-
-          (b) Contributor APIs.
-          If Contributor's Modifications include an application programming
-          interface and Contributor has knowledge of patent licenses which
-          are reasonably necessary to implement that API, Contributor must
-          also include this information in the LEGAL file.
-
-               (c)    Representations.
-          Contributor represents that, except as disclosed pursuant to
-          Section 3.4(a) above, Contributor believes that Contributor's
-          Modifications are Contributor's original creation(s) and/or
-          Contributor has sufficient rights to grant the rights conveyed by
-          this License.
-
-     3.5. Required Notices.
-     You must duplicate the notice in Exhibit A in each file of the Source
-     Code.  If it is not possible to put such notice in a particular Source
-     Code file due to its structure, then You must include such notice in a
-     location (such as a relevant directory) where a user would be likely
-     to look for such a notice.  If You created one or more Modification(s)
-     You may add your name as a Contributor to the notice described in
-     Exhibit A.  You must also duplicate this License in any documentation
-     for the Source Code where You describe recipients' rights or ownership
-     rights relating to Covered Code.  You may choose to offer, and to
-     charge a fee for, warranty, support, indemnity or liability
-     obligations to one or more recipients of Covered Code. However, You
-     may do so only on Your own behalf, and not on behalf of the Initial
-     Developer or any Contributor. You must make it absolutely clear than
-     any such warranty, support, indemnity or liability obligation is
-     offered by You alone, and You hereby agree to indemnify the Initial
-     Developer and every Contributor for any liability incurred by the
-     Initial Developer or such Contributor as a result of warranty,
-     support, indemnity or liability terms You offer.
-
-     3.6. Distribution of Executable Versions.
-     You may distribute Covered Code in Executable form only if the
-     requirements of Section 3.1-3.5 have been met for that Covered Code,
-     and if You include a notice stating that the Source Code version of
-     the Covered Code is available under the terms of this License,
-     including a description of how and where You have fulfilled the
-     obligations of Section 3.2. The notice must be conspicuously included
-     in any notice in an Executable version, related documentation or
-     collateral in which You describe recipients' rights relating to the
-     Covered Code. You may distribute the Executable version of Covered
-     Code or ownership rights under a license of Your choice, which may
-     contain terms different from this License, provided that You are in
-     compliance with the terms of this License and that the license for the
-     Executable version does not attempt to limit or alter the recipient's
-     rights in the Source Code version from the rights set forth in this
-     License. If You distribute the Executable version under a different
-     license You must make it absolutely clear that any terms which differ
-     from this License are offered by You alone, not by the Initial
-     Developer or any Contributor. You hereby agree to indemnify the
-     Initial Developer and every Contributor for any liability incurred by
-     the Initial Developer or such Contributor as a result of any such
-     terms You offer.
-
-     3.7. Larger Works.
-     You may create a Larger Work by combining Covered Code with other code
-     not governed by the terms of this License and distribute the Larger
-     Work as a single product. In such a case, You must make sure the
-     requirements of this License are fulfilled for the Covered Code.
-
-4. Inability to Comply Due to Statute or Regulation.
-
-     If it is impossible for You to comply with any of the terms of this
-     License with respect to some or all of the Covered Code due to
-     statute, judicial order, or regulation then You must: (a) comply with
-     the terms of this License to the maximum extent possible; and (b)
-     describe the limitations and the code they affect. Such description
-     must be included in the LEGAL file described in Section 3.4 and must
-     be included with all distributions of the Source Code. Except to the
-     extent prohibited by statute or regulation, such description must be
-     sufficiently detailed for a recipient of ordinary skill to be able to
-     understand it.
-
-5. Application of this License.
-
-     This License applies to code to which the Initial Developer has
-     attached the notice in Exhibit A and to related Covered Code.
-
-6. Versions of the License.
-
-     6.1. New Versions.
-     Netscape Communications Corporation ("Netscape") may publish revised
-     and/or new versions of the License from time to time. Each version
-     will be given a distinguishing version number.
-
-     6.2. Effect of New Versions.
-     Once Covered Code has been published under a particular version of the
-     License, You may always continue to use it under the terms of that
-     version. You may also choose to use such Covered Code under the terms
-     of any subsequent version of the License published by Netscape. No one
-     other than Netscape has the right to modify the terms applicable to
-     Covered Code created under this License.
-
-     6.3. Derivative Works.
-     If You create or use a modified version of this License (which you may
-     only do in order to apply it to code which is not already Covered Code
-     governed by this License), You must (a) rename Your license so that
-     the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
-     "MPL", "NPL" or any confusingly similar phrase do not appear in your
-     license (except to note that your license differs from this License)
-     and (b) otherwise make it clear that Your version of the license
-     contains terms which differ from the Mozilla Public License and
-     Netscape Public License. (Filling in the name of the Initial
-     Developer, Original Code or Contributor in the notice described in
-     Exhibit A shall not of themselves be deemed to be modifications of
-     this License.)
-
-7. DISCLAIMER OF WARRANTY.
-
-     COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
-     WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
-     WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
-     DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
-     THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
-     IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
-     YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
-     COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
-     OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
-     ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
-
-8. TERMINATION.
-
-     8.1.  This License and the rights granted hereunder will terminate
-     automatically if You fail to comply with terms herein and fail to cure
-     such breach within 30 days of becoming aware of the breach. All
-     sublicenses to the Covered Code which are properly granted shall
-     survive any termination of this License. Provisions which, by their
-     nature, must remain in effect beyond the termination of this License
-     shall survive.
-
-     8.2.  If You initiate litigation by asserting a patent infringement
-     claim (excluding declatory judgment actions) against Initial Developer
-     or a Contributor (the Initial Developer or Contributor against whom
-     You file such action is referred to as "Participant")  alleging that:
-
-     (a)  such Participant's Contributor Version directly or indirectly
-     infringes any patent, then any and all rights granted by such
-     Participant to You under Sections 2.1 and/or 2.2 of this License
-     shall, upon 60 days notice from Participant terminate prospectively,
-     unless if within 60 days after receipt of notice You either: (i)
-     agree in writing to pay Participant a mutually agreeable reasonable
-     royalty for Your past and future use of Modifications made by such
-     Participant, or (ii) withdraw Your litigation claim with respect to
-     the Contributor Version against such Participant.  If within 60 days
-     of notice, a reasonable royalty and payment arrangement are not
-     mutually agreed upon in writing by the parties or the litigation claim
-     is not withdrawn, the rights granted by Participant to You under
-     Sections 2.1 and/or 2.2 automatically terminate at the expiration of
-     the 60 day notice period specified above.
-
-     (b)  any software, hardware, or device, other than such Participant's
-     Contributor Version, directly or indirectly infringes any patent, then
-     any rights granted to You by such Participant under Sections 2.1(b)
-     and 2.2(b) are revoked effective as of the date You first made, used,
-     sold, distributed, or had made, Modifications made by that
-     Participant.
-
-     8.3.  If You assert a patent infringement claim against Participant
-     alleging that such Participant's Contributor Version directly or
-     indirectly infringes any patent where such claim is resolved (such as
-     by license or settlement) prior to the initiation of patent
-     infringement litigation, then the reasonable value of the licenses
-     granted by such Participant under Sections 2.1 or 2.2 shall be taken
-     into account in determining the amount or value of any payment or
-     license.
-
-     8.4.  In the event of termination under Sections 8.1 or 8.2 above,
-     all end user license agreements (excluding distributors and resellers)
-     which have been validly granted by You or any distributor hereunder
-     prior to termination shall survive termination.
-
-9. LIMITATION OF LIABILITY.
-
-     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
-     (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
-     DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
-     OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
-     ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
-     CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
-     WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
-     COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
-     INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
-     LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
-     RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
-     PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
-     EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
-     THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
-
-10. U.S. GOVERNMENT END USERS.
-
-     The Covered Code is a "commercial item," as that term is defined in
-     48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
-     software" and "commercial computer software documentation," as such
-     terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
-     C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
-     all U.S. Government End Users acquire Covered Code with only those
-     rights set forth herein.
-
-11. MISCELLANEOUS.
-
-     This License represents the complete agreement concerning subject
-     matter hereof. If any provision of this License is held to be
-     unenforceable, such provision shall be reformed only to the extent
-     necessary to make it enforceable. This License shall be governed by
-     California law provisions (except to the extent applicable law, if
-     any, provides otherwise), excluding its conflict-of-law provisions.
-     With respect to disputes in which at least one party is a citizen of,
-     or an entity chartered or registered to do business in the United
-     States of America, any litigation relating to this License shall be
-     subject to the jurisdiction of the Federal Courts of the Northern
-     District of California, with venue lying in Santa Clara County,
-     California, with the losing party responsible for costs, including
-     without limitation, court costs and reasonable attorneys' fees and
-     expenses. The application of the United Nations Convention on
-     Contracts for the International Sale of Goods is expressly excluded.
-     Any law or regulation which provides that the language of a contract
-     shall be construed against the drafter shall not apply to this
-     License.
-
-12. RESPONSIBILITY FOR CLAIMS.
-
-     As between Initial Developer and the Contributors, each party is
-     responsible for claims and damages arising, directly or indirectly,
-     out of its utilization of rights under this License and You agree to
-     work with Initial Developer and Contributors to distribute such
-     responsibility on an equitable basis. Nothing herein is intended or
-     shall be deemed to constitute any admission of liability.
-
-13. MULTIPLE-LICENSED CODE.
-
-     Initial Developer may designate portions of the Covered Code as
-     "Multiple-Licensed".  "Multiple-Licensed" means that the Initial
-     Developer permits you to utilize portions of the Covered Code under
-     Your choice of the NPL or the alternative licenses, if any, specified
-     by the Initial Developer in the file described in Exhibit A.
-
-EXHIBIT A -Mozilla Public License.
-
-     ``The contents of this file are subject to the Mozilla Public License
-     Version 1.1 (the "License"); you may not use this file except in
-     compliance with the License. You may obtain a copy of the License at
-     http://www.mozilla.org/MPL/
-
-     Software distributed under the License is distributed on an "AS IS"
-     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
-     License for the specific language governing rights and limitations
-     under the License.
-
-     The Original Code is ______________________________________.
-
-     The Initial Developer of the Original Code is ________________________.
-     Portions created by ______________________ are Copyright (C) ______
-     _______________________. All Rights Reserved.
-
-     Contributor(s): ______________________________________.
-
-     Alternatively, the contents of this file may be used under the terms
-     of the _____ license (the  "[___] License"), in which case the
-     provisions of [______] License are applicable instead of those
-     above.  If you wish to allow use of your version of this file only
-     under the terms of the [____] License and not to allow others to use
-     your version of this file under the MPL, indicate your decision by
-     deleting  the provisions above and replace  them with the notice and
-     other provisions required by the [___] License.  If you do not delete
-     the provisions above, a recipient may use your version of this file
-     under either the MPL or the [___] License."
-
-     [NOTE: The text of this Exhibit A may differ slightly from the text of
-     the notices in the Source Code files of the Original Code. You should
-     use the text of this Exhibit A rather than the text found in the
-     Original Code Source Code for Your Modifications.]
-
-----
-
-
 [[PublicDomain]]
 PublicDomain
 
@@ -1079,6 +469,7 @@
 [[bouncycastle]]
 bouncycastle
 
+* bouncycastle:bcpg
 * bouncycastle:bcpg-neverlink
 * bouncycastle:bcpkix-neverlink
 * bouncycastle:bcprov-neverlink
@@ -1720,6 +1111,8 @@
 
 ----
 
+----
+
 
 [[icu4j]]
 icu4j
@@ -2168,43 +1561,6 @@
 ----
 
 
-[[jsch]]
-jsch
-
-* jsch
-
-[[jsch_license]]
-----
-Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-  1. Redistributions of source code must retain the above copyright notice,
-     this list of conditions and the following disclaimer.
-
-  2. Redistributions in binary form must reproduce the above copyright
-     notice, this list of conditions and the following disclaimer in
-     the documentation and/or other materials provided with the distribution.
-
-  3. The names of the authors may not be used to endorse or promote products
-     derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
-INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
 [[jsoup]]
 jsoup
 
diff --git a/Documentation/check_licenses_test.sh b/Documentation/check_licenses_test.sh
index 52e27f2..adfc2da 100755
--- a/Documentation/check_licenses_test.sh
+++ b/Documentation/check_licenses_test.sh
@@ -2,7 +2,7 @@
 
 hook=$(pwd)/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
 
-for f in  js_licenses licenses ; do
+for f in backend_licenses js_licenses licenses ; do
   if ! diff -u Documentation/${f}.txt Documentation/${f}.gen.txt  ; then
      echo ""
      echo "FAIL: ${f}.txt out of date"
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 7999b8f..9eba0bc 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -12,6 +12,7 @@
   [--email <EMAIL>]
   [--ssh-key - | <KEY>]
   [--http-password <PASSWORD>]
+  [--token <TOKEN>]
   <USERNAME>
 --
 
@@ -62,9 +63,17 @@
 	Preferred email address for the user account.
 
 --http-password::
-    HTTP password for the user account.
+    HTTP password for the user account. (deprecated)
     **NOTE** The password is limited to 71 characters.
 
+--token::
+    Authentication token for the user account.
+    **NOTE** The token is limited to 71 characters.
+
+--token-id::
+    ID used for the provided token. If not provided, the token id will
+    be generated based on the timestamp.
+
 == EXAMPLES
 Create a new batch/role access user account called `watcher` in
 the 'Service Users' group.
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index 71de014..def35ca 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -15,6 +15,9 @@
   [--generate-http-password]
   [--http-password <PASSWORD>]
   [--clear-http-password]
+  [--token <TOKEN>]
+  [--generate-token <TOKEN-ID>]
+  [--delete-token <TOKEN-ID>]
   [--delete-external-id <EXTERNALID>] <USER>
 --
 
@@ -108,6 +111,18 @@
 --clear-http-password::
     Clear the HTTP password for the user account.
 
+--token::
+    Authorization token for the user account. Multiple --token
+    options may be specified to add multiple tokens to an account.
+    Requires administrator privileges.
+
+--generate-token::
+    Generate a new random token for the user account. The token
+    will be output to the user on success.
+
+--delete-token::
+    Delete a token from a user's account.
+
 --delete-external-id::
     Delete an external ID from a user's account if it exists.
     If the external ID provided is 'ALL', all associated
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index 005ffe0..305dc39 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -44,6 +44,10 @@
 -q::
 	Group tasks by queue and print queue info.
 
+--queue::
+-Q::
+        Show only the tasks from the specified queue.
+
 == DISPLAY
 
 Task::
diff --git a/Documentation/cmd-suexec.txt b/Documentation/cmd-suexec.txt
index 9808edc..7c1a104 100644
--- a/Documentation/cmd-suexec.txt
+++ b/Documentation/cmd-suexec.txt
@@ -21,7 +21,7 @@
 registered user account.
 
 suexec can only be invoked by the magic user `Gerrit Code Review`,
-or any user granted granted the link:access-control.html#capability_runAs[Run As]
+or any user granted the link:access-control.html#capability_runAs[Run As]
 capability. The run as capability is permitted to be used only if
 link:config-gerrit.html[auth.enableRunAs] is true.
 
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index f46c821..7f74348 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -141,6 +141,11 @@
 +
 Stores the link:#project-watches[project watches] of the account.
 
+
+* `tokens.config`:
++
+Stores the link:#authentication-tokens[authentication tokens] of the account.
+
 In addition it contains an
 link:https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys[
 authorized_keys,role=external,window=_blank] file with the link:#ssh-keys[SSH keys] of the account.
@@ -251,6 +256,39 @@
 available from the account cache and Gerrit can check if any watch
 matches the change and the event.
 
+[[authentication-tokens]]
+=== Authentication Tokens
+
+When `auth.gitBasicAuthPolicy` is set to `HTTP` or `HTTP_LDAP`, Gerrit requires
+users to authenticate with an authentication token generated in Gerrit when
+using the REST API or Git to communicate with the Gerrit server.
+
+These authentication tokens are stored in the user branch in the `tokens.config`
+file. The file uses the Git config format:
+
+----
+[token "some-token-id"]
+  hash = bcrypt0:4:....
+  expiration = 2025-07-30T07:14:10.745274Z
+----
+
+Each subsection represents a token. The subsection name represents an ID that
+is unique for the user.
+
+The token is never stored in plain text. Only the serialized hash of the token
+will be stored which can be used to verify the token during authentication.
+
+A token may have an expiration timestamp. This timestamp is always in UTC and
+uses the DateTimeFormatter#ISO_INSTANT format. A token is considered valid if
+the expiration timestamp is in the future. If no expiration timestamp is
+provided the token is considered to be valid forever or until it is deleted.
+
+When a token is deleted, the respective subsection is removed from the file.
+
+During authentication Gerrit will verify the provided token against all valid
+tokens present in the account. If any token matches the provided token,
+authentication will succeed.
+
 [[ssh-keys]]
 === SSH Keys
 
@@ -286,7 +324,7 @@
 == External IDs
 
 External IDs are used to link identities, such as the username and email
-addresses, and external identies such as an LDAP account or an OAUTH
+addresses, and external identities such as an LDAP account or an OAUTH
 identity, to an account in Gerrit.
 
 External IDs are stored as Git Notes in the `All-Users` repository. The
@@ -347,7 +385,9 @@
 subsection name.
 
 The `accountId` field is mandatory. The `email` and `password` fields
-are optional.
+are optional. The `password` field is deprecated, because passwords
+are being replaced by tokens, which are stored in the account's user
+reference.
 
 Note that git will automatically nest these notes at varying levels. If
 refs/meta/external-ids:7c/2a55657d911109dbc930836e7a770fb946e8ef is not
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 911eb56..528469c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -115,7 +115,7 @@
 [[accounts.caseInsensitiveLocalPart]]accounts.caseInsensitiveLocalPart::
 +
 When querying by email, the case sensitivity of the email local part depends on the domains
-specified in the this list.
+specified in this list.
 +
 Users can register emails with mixed case, and if the email’s domain matches one in the configured
 list, the local part is treated as case-insensitive.
@@ -278,12 +278,15 @@
 <<ldap.authentication,ldap.authentication>> is set to `GSSAPI`.
 +
 If link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
-the randomly generated HTTP password is used for authentication. On the other hand,
-if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
-the password in the request is first checked against the HTTP password and, if
-it does not match, it is then validated against the LDAP password.
+the randomly generated authentication tokens are used for authentication. On the
+other hand, if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set
+to `HTTP_LDAP`, the password in the request is first checked against the token
+and, if it does not match, it is then validated against the LDAP password.
 Service users that are link:cmd-create-account.html[internal-only] are
-authenticated by their HTTP passwords.
+authenticated by their tokens.
++
+Note, that in an interim period, if the user does not have an authentication
+token, Gerrit will fall back to a potentially still existing HTTP password.
 
 * `LDAP_BIND`
 +
@@ -569,13 +572,6 @@
 +
 If not set, HTTP request's domain is used.
 
-[[auth.cookieSecure]]auth.cookieSecure::
-+
-Sets "secure" flag of the authentication cookie.  If `true`, cookies
-will be transmitted only over HTTPS protocol.
-+
-By default, `false`.
-
 [[auth.cookieHttpOnly]]auth.cookieHttpOnly::
 +
 Sets "httpOnly" flag of the authentication cookie. If `true`, cookie
@@ -595,7 +591,7 @@
 +
 + Setting this to `0` disables memoization.
 +
-**Note** during the memoization time exterenal-ids modification are not visible by Gerrit.
+**Note** during the memoization time external-ids modification are not visible by Gerrit.
 +
 By default `0`.
 
@@ -648,13 +644,14 @@
 [[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
 +
 When `auth.type` is `LDAP`, `LDAP_BIND` or `OAUTH`, it allows using either the generated
-HTTP password, the LDAP or OAUTH password, or a combination of HTTP and LDAP
-authentication, to authenticate Git over HTTP and REST API requests.
+tokens, the LDAP or OAUTH password, or a combination of HTTP and LDAP authentication,
+to authenticate Git over HTTP and REST API requests.
 The supported values are:
 +
 *`HTTP`
 +
-Only the HTTP password is accepted when doing Git over HTTP and REST API requests.
+Only the authentication tokens, with a fallback to HTTP passwords, are accepted
+when doing Git over HTTP and REST API requests.
 +
 *`LDAP`
 +
@@ -668,7 +665,7 @@
 +
 *`HTTP_LDAP`
 +
-The password in the request is first checked against the HTTP password and, if
+The password in the request is first checked against the authentication tokens and, if
 it does not match, it is then validated against the `LDAP` password.
 +
 By default this is set to `LDAP` when link:#auth.type[`auth.type`] is `LDAP`
@@ -677,11 +674,48 @@
 +
 When gitBasicAuthPolicy is set to `LDAP` or `HTTP_LDAP` and the user
 is authenticating with the LDAP username/password, the Git client config
-needs to have `http.cookieFile` set to a local file, otherwise every
-single call would trigger a full LDAP authentication and groups resolution
-which could introduce a noticeable latency on the overall execution
+needs to have `http.cookieFile` set to the local file and `http.saveCookies` set
+to `true`, so that HTTP session cookies can be read and saved. Otherwise
+every single call would trigger a full LDAP authentication and groups
+resolution which could introduce a noticeable latency on the overall execution
 and produce unwanted load to the LDAP server.
 
+[[auth.maxAuthTokenLifetime]]auth.maxAuthTokenLifetime::
++
+The maximum lifetime allowed for an authentication token. The minimum time is
+1 minute.
+Values should use common unit suffixes to express their setting:
++
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
++
+If not set or if a value <= 0 is used, tokens can have an unlimited lifetime.
++
+Existing tokens will not be affected by changing this value.
+
+[[auth.httpPasswordFallbackEnabled]]auth.httpPasswordFallbackEnabled::
++
+To provide backward compatibility Gerrit will automatically fall back to
+using the existing HTTP password, if no auth token has been created yet
+for the user account. This does not provide any benefit if all HTTP
+passwords were migrated to authentication tokens or if the Gerrit site
+is new. Thus, this option can be used to disable the fallback, which should
+slightly improve performance during authentication and managing tokens.
++
+By default, `true`, i.e. the fallback is enabled.
+
+[[auth.maxAuthTokensPerAccount]]auth.maxAuthTokensPerAccount::
++
+The maximum number of authentication tokens a user is allowed to have in their
+account. If that number is reached, creating a new token will fail until an
+existing token is being deleted. Expired tokens count to the limit.
++
+Defaults to 10.
+
 [[auth.gitOAuthProvider]]auth.gitOAuthProvider::
 +
 Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
@@ -1007,6 +1041,7 @@
 Default is 128 MiB per cache, except:
 +
 * `"change_notes"`: disk storage is disabled by default
+* `"changes_by_project"`: default is `1g` (1 GiB of disk space)
 * `"diff_summary"`: default is `1g` (1 GiB of disk space)
 * `"external_ids_map"`: disk storage is disabled by default
 * `"persisted_projects"`: default is `1g` (1 GiB of disk space)
@@ -1100,10 +1135,11 @@
 
 cache `"changes_by_project"`::
 +
-Ideally, the memorylimit of this cache is large enough to cover all projects.
-This should significantly speed up change ref advertisements and git pushes,
-especially for projects with lots of changes, and particularly on replicas
-where there is no index.
+For the cache to work as expected, if the `memoryLimit` is not large enough to
+cover all projects, then the `diskLimit` should be enabled and set large enough
+for all projects. This should significantly speed up change ref advertisements
+and git pushes, especially for projects with lots of changes, and particularly
+on replicas where there is no index.
 
 cache `"git_modified_files"`::
 +
@@ -1338,6 +1374,11 @@
 NOTE: This cache should be disabled or set with a low refreshAfterWrite
 in a cluster setup using multiple primary or multiple replica nodes.
 
+cache `"tokens"`::
++
+Caches HTTP authentication tokens. This cache is only enabled, if the
+option `auth.gitBasicAuthPolicy` is either `HTTP` or `HTTP_LDAP`.
+
 cache `"web_sessions"`::
 +
 Tracks the live user sessions coming in over HTTP.  Flushing this
@@ -1554,7 +1595,7 @@
 
 [[change.commentSizeLimit]]change.commentSizeLimit::
 +
-Maximum allowed size in characters of a regular (non-robot) comment. Comments
+Maximum allowed size in characters of a comment. Comments
 which exceed this size will be rejected. Size computation is approximate and may
 be off by roughly 1%. Common unit suffixes of 'k', 'm', or 'g' are supported.
 The value must be positive.
@@ -1570,7 +1611,7 @@
 
 [[change.cumulativeCommentSizeLimit]]change.cumulativeCommentSizeLimit::
 +
-Maximum allowed size in characters of all comments (including robot comments)
+Maximum allowed size in characters of all comments
 and change messages. Size computation is approximate and may be off by roughly
 1%. Common unit suffixes of 'k', 'm', or 'g' are supported.
 +
@@ -1584,7 +1625,7 @@
 
 [[change.maxComments]]change.maxComments::
 +
-Maximum number of comments (regular plus robot) allowed per change. Additional
+Maximum number of comments allowed per change. Additional
 comments are rejected.
 +
 By default 5,000.
@@ -1617,7 +1658,7 @@
 [[change.maxUpdates]]change.maxUpdates::
 +
 Maximum number of updates to a change. Counts only updates to the main NoteDb
-meta ref; draft comments, robot comments, stars, etc. do not count towards the
+meta ref; draft comments, stars, etc. do not count towards the
 total.
 +
 Many NoteDb operations require walking the entire change meta ref and loading
@@ -1701,10 +1742,22 @@
 +
 By default `true`.
 
-[[change.enableRobotComments]]change.enableRobotComments::
+[[change.allowMarkdownBase64ImagesInComments]]change.allowMarkdownBase64ImagesInComments::
 +
-Are robot comments enabled in the Gerrit UI? This setting allows phasing out
-robot comments. Soon robot comments will be entirely removed.
+Allows Base64 encoded images, embedded via Markdown, to be rendered in Gerrit comments.
++
+This feature addresses the need for teams to share images directly within Gerrit comments.
+Instead of relying on external image hosting, images are encoded into Base64 strings
+and included in the comment text using Markdown image syntax.
++
+When enabled, the Gerrit UI will detect and render these inline images.
++
+[NOTE]
+====
+The size of Base64 encoded images is not counted against the
+link:#change.commentSizeLimit[comment size limit] and
+link:#change.cumulativeCommentSizeLimit[cumulative comment size limit].
+====
 +
 By default `false`.
 
@@ -1737,15 +1790,6 @@
 than handling parsing errors gracefully, which can make Gerrit for impacted
 users unusable.
 
-[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
-+
-Maximum allowed size in characters of a robot comment. Robot comments which
-exceed this size will be rejected on addition. Size computation is approximate
-and may be off by roughly 1%. Common unit suffixes of 'k', 'm', or 'g' are
-supported. Zero or negative values allow robot comments of unlimited size.
-+
-The default limit is 1MiB.
-
 [[change.sendNewPatchsetEmails]]change.sendNewPatchsetEmails::
 +
 When `false`, emails will not be sent to owners, reviewers, and cc for
@@ -2431,6 +2475,15 @@
 The list of submit requirement names that should be displayed as separate
 columns in the dashboard.
 
+[[dashboard.showAllLabels]]dashboard.showAllLabels::
++
+Display all labels in the dashboard, even if they are not submit
+requirements.
++
+This has no effect if submitRequirementColumns is configured.
++
+Default is `false`.
+
 [[download]]
 === Section download
 
@@ -2572,6 +2625,16 @@
 For this reason `zip` format is always excluded from formats offered
 through the `Download` drop down or accessible in the REST API.
 
+[[flows]]
+=== Section flows
+
+[[flows.maxPerChange]]flows.maxPerChange::
++
+Controls how many flows can be created per change at most. 0 or negative values
+mean "unlimited".
++
+By default, `20`.
+
 [[gc]]
 === Section gc
 
@@ -2861,7 +2924,7 @@
 If link:#gerrit.canLoadInIFrame is set to `false` this option is ignored and the
 `X-Frame-Options` header is always set to `DENY`.
 Setting this option to `ALLOW` will cause the `X-Frame-Options` header to be omitted
-the the page can be displayed in a frame.
+the page can be displayed in a frame.
 +
 By default SAMEORIGIN.
 
@@ -2914,7 +2977,7 @@
 2. Set the first master unhealthy
 3. Shutdown the first master and [upgrade](install.html#init) to the next version
 4. Startup the first master, wait for the online reindex to complete (where applicable)
-5. Verify the the first master upgrade is successful and online reindex is complete
+5. Verify the first master upgrade is successful and online reindex is complete
 6. Set the first master healthy
 7. Repeat steps 2. to 6. for all the other Gerrit nodes
 +
@@ -3057,7 +3120,7 @@
 [[gitweb.filehistory]]gitweb.filehistory::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
-at the history of a file in a specific branch when when `gitweb.type`
+at the history of a file in a specific branch when `gitweb.type`
 is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit,
@@ -3339,7 +3402,7 @@
 +
 [NOTE]
 --
-For secruity reasons, make sure to only allow connections from a
+For security reasons, make sure to only allow connections from a
 trusted reverse proxy in your network, as clients could otherwise
 easily spoof these headers and thus spoof their originating IP
 address effectively. If the reverse proxy is running on the same
@@ -4087,7 +4150,7 @@
 the in-memory buffer fills, but only committed and guaranteed to be synced
 to disk when the process finishes.
 +
-Defaults to 300000 ms (5 minutes).
+Defaults to 5 minutes.
 
 
 [[index.name.maxMergeCount]]index.name.maxMergeCount::
@@ -4193,7 +4256,7 @@
 +
 Add patch set level comment as event comment. Without this option, patch set
 level comment will not be included in the event comment attribute. Given that
-currently patch set level, file and robot comments are not exposed in the
+currently patch set level and file comments are not exposed in the
 `comment-added` event type, those comments will be lost. One particular use
 case is to re-trigger CI build from the change screen by adding a comment with
 specific content, e.g.: `recheck`. Jenkins Gerrit Trigger plugin and Zuul CI
@@ -4264,6 +4327,22 @@
   disabled = ExperimentKey
 ----
 
+[[experiments.ui_feature_get_ai_prompt]]
+==== UiFeature__get_ai_prompt
+
+Controls the visibility of the AI Review Prompt action in the change screen UI.
+
+When enabled, the "Create AI Review Prompt" button and dialog are shown in the UI.
+
+This experiment is enabled by default. It can be disabled explicitly:
+
+----
+[experiments]
+  disabled = UiFeature__get_ai_prompt
+----
+
+Disabling this experiment hides the AI Review Prompt action from the UI.
+
 [[ldap]]
 === Section ldap
 
@@ -4386,8 +4465,9 @@
 A timeout can be used to avoid blocking all of the SSH command start
 threads in case the LDAP server becomes slow.
 +
-By default there is no timeout and Gerrit will wait for the LDAP
-server to respond until the TCP connection times out.
+By default 1 minute.  If set to 0 there is no timeout and Gerrit
+will wait for the LDAP server to respond until the TCP connection
+times out.
 
 [[ldap.accountBase]]ldap.accountBase::
 +
@@ -4664,7 +4744,8 @@
 The value is in the usual time-unit format like "1 s", "100 ms",
 etc...
 +
-By default there is no timeout and Gerrit will wait indefinitely.
+By default 10 seconds. If set to 0 there is no timeout and Gerrit will
+wait indefinitely.
 
 [[ldap-connection-pooling]]
 ==== LDAP Connection Pooling
@@ -4938,7 +5019,7 @@
 result in a smaller overall transfer for the client, but requires
 more server memory and CPU time.
 +
-False (off) by default, matching Gerrit Code Review 2.1.4.
+False (off) by default.
 
 [[pack.threads]]pack.threads::
 +
@@ -5227,9 +5308,9 @@
 cancellation signal that allows the task to finish gracefully.
 link:#receive.cancellationTimeout[receive.cancellationTimeout]
 defines how much time the task has to react to the cancellation signal
-before it is focefully cancelled.
+before it is forcefully cancelled.
 +
-The receive timeout cannot be overriden by setting a higher
+The receive timeout cannot be overridden by setting a higher
 link:user-upload.html#deadline[deadline] on the git push request.
 +
 Default is 4 minutes. If no unit is specified, milliseconds
@@ -5337,6 +5418,42 @@
 +
 Path must be absolute.
 
+[[repository.name.defaultConfig]]repository.<name>.defaultConfig::
++
+Sets the default value for boolean project configs:
++
+--
+* link:config-project-config.html#change.privateByDefault[change.privateByDefault]
+* link:config-project-config.html#change.workInProgressByDefault[change.workInProgressByDefault]
+* link:config-project-config.html#receive.createNewChangeForAllNotInTarget[receive.createNewChangeForAllNotInTarget]
+* link:config-project-config.html#receive.enableSignedPush[receive.enableSignedPush]
+* link:config-project-config.html#receive.requireChangeId[receive.requireChangeId]
+* link:config-project-config.html#receive.requireContributorAgreement[receive.requireContributorAgreement]
+* link:config-project-config.html#receive.rejectImplicitMerges[receive.rejectImplicitMerges]
+* link:config-project-config.html#receive.requireSignedOffBy[receive.requireSignedOffBy]
+* link:config-project-config.html#receive.requireSignedPush[receive.requireSignedPush]
+* link:config-project-config.html#reviewer.enableByEmail[reviewer.enableByEmail]
+* link:config-project-config.html#reviewer.skipAddingAuthorAndCommitterAsReviewers[reviewer.skipAddingAuthorAndCommitterAsReviewers]
+* link:config-project-config.html#submit.matchAuthorToCommitterDate[submit.matchAuthorToCommitterDate]
+* link:config-project-config.html#submit.mergeContent[submit.mergeContent]
+* link:config-project-config.html#submit.rejectEmptyCommit[submit.rejectEmptyCommit]
+--
++
+May be specified multiple times.
++
+The values must have the format `<key>=<defaultValue>`, e.g.
+`receive.rejectImplicitMerges=true`.
++
+Allowed default values are:
++
+* `true`: The default value is `true`. Projects may override the default in
+  their project configuration.
+* `forced`: The value is `true` for all projects. Projects cannot override this
+  default in their project configuration (existing configurations for this
+  parameter on project level are ignored).
+* `false`: The default value is `false`. Projects may override the default in
+  their project configuration.
+
 [[repository.name.defaultSubmitType]]repository.<name>.defaultSubmitType::
 +
 The default submit type for newly created projects. Supported values
@@ -5388,7 +5505,8 @@
 +
 Total timeout for retrying operations of type `<operationType>` when one
 attempt fails. `<operationType>` can be `ACCOUNT_UPDATE`, `CHANGE_UPDATE`,
-`GROUP_UPDATE` and `INDEX_QUERY`.
+`GROUP_UPDATE`, `INDEX_QUERY`, `PLUGIN_UPDATE`, `REPO_CREATION`,
+`REST_READ_REQUEST`, `REST_WRITE_REQUEST` and `SEND_EMAIL`.
 +
 Defaults to link:#retry.timeout[`retry.timeout`]; unit suffixes are supported,
 and assumes milliseconds if not specified.
@@ -5419,8 +5537,8 @@
 +
 Modification and deletion of existing rules are still allowed.
 +
-Default is `true`, to allow creation of prolog rules in projects
-not having any already.
+Default is `false`, disallowing creation of prolog rules in projects
+not having any already since prolog rules are now deprecated.
 
 [[rules.reductionLimit]]rules.reductionLimit::
 +
@@ -5482,16 +5600,39 @@
 
 [[execution.fanOutThreadPoolSize]]execution.fanOutThreadPoolSize::
 +
-Maximum size of thread pool to on which a serving thread can fan-out
-work to parallelize it.
+Maximum size of thread pool a serving thread can fan-out work on to
+parallelize it.
 +
-When set to 0, a direct executor will be used.
+When set to 0 the work happens directly in the caller thread.
 +
-By default, 25 which means that formatting happens in the caller thread.
+By default 25.
 
 [[performance]]
 === Section performance
 
+[[performance.maxOperationsToLog]]performance.maxOperationsToLog::
++
+If a request is traced a performance summary with the most expensive
+operations is logged at the end of a request. This setting controls
+the maximum number of operations for which the latency should be
+included into the logged performance summary.
++
+Note the performance summary is only logged if the request is traced
+and performance logging is enabled (see
+link:#tracing.performanceLogging[tracing.performanceLogging] setting).
++
+By default 25.
+
+[[performance.slowRequestThreshold]]performance.slowRequestThreshold::
++
+Threshold from which on a request is considered as "slow" if exceeded.
++
+For slow requests a performance summary is logged on warning level (only if
+performance logging is enabled (see
+link:#tracing.performanceLogging[tracing.performanceLogging] setting).
++
+By default 30s.
+
 [[performance.metric]]
 ==== Subsection performance.metric
 
@@ -5619,7 +5760,7 @@
 ('ms', 'sec', 'min', etc.).
 If no unit is specified, milliseconds is assumed.
 +
-Default is 0. A timeout of zero is interpreted as an infinite
+Default is 10 seconds. A timeout of zero is interpreted as an infinite
 timeout. The connection will then block until established or
 an error occurs.
 
@@ -5632,7 +5773,7 @@
 ('ms', 'sec', 'min', etc.).
 If no unit is specified, milliseconds is assumed.
 +
-Default is 0. A timeout of zero is interpreted as an infinite
+Default is 10 seconds. A timeout of zero is interpreted as an infinite
 timeout. With this config value set to a non-zero timeout,
 a read() call on the InputStream associated with this Socket
 will block for only this amount of time. Hence administrators
@@ -5759,7 +5900,7 @@
 +
 If `true`, new change emails and merged change emails from Gerrit
 will include the complete unified diff of the change.
-Variable maxmimumDiffSize places an upper limit on how large the
+Variable maximumDiffSize places an upper limit on how large the
 email can get when this option is enabled.
 +
 By default, `false`.
@@ -6256,9 +6397,9 @@
 +
 When suggesting reviewers, we go over recent changes of the user, and
 give priority to users that are present as reviewers in any of those
-changes. The number of changes we go over is `sugggest.relevantChanges`.
+changes. The number of changes we go over is `suggest.relevantChanges`.
 +
-This nubmer is a tradeoff between speed and accuracy.
+This number is a tradeoff between speed and accuracy.
 A high number would be accurate but slow, and a low number would be
 fast but inaccurate.
 +
@@ -6547,7 +6688,8 @@
 especially over a WAN link, while 10-30 seconds is a much more
 reasonable timeout value.
 +
-Defaults to 0 seconds, wait indefinitely.
+Defaults to 1 minute. If set to 0 there is no timeout and Gerrit
+will wait indefinitely.
 
 
 [[upload]]
@@ -6664,7 +6806,7 @@
 
 Schedule configurations are used for running periodic background jobs.
 
-A schedule configuration consists of two parameters:
+A schedule configuration consists of the following parameters:
 
 [[schedule-configuration-interval]]
 * `interval`:
@@ -6704,9 +6846,17 @@
 The time zone cannot be specified but is always the system default
 time zone. Hours must be zero-padded, i.e. `06:00` rather than `6:00`.
 
-The section (and optionally the subsection) in which the `interval` and
-`startTime` keys must be set depends on the background job for which a
-schedule should be configured. E.g. for the change cleanup job the keys
+[[schedule-configuration-jitter]]
+* `jitter`:
+A maximum random delay that will be added to the job's scheduled start time. The same suffixes as
+link:#schedule-configuration-interval[interval] are supported for defining the
+time unit.
++
+By default, no jitter is applied.
+
+The section (and optionally the subsection) in which the `interval`,
+`startTime` and `jitter` keys must be set depends on the background job for
+which a schedule should be configured. E.g. for the change cleanup job the keys
 must be set in the link:#changeCleanup[changeCleanup] section:
 
 ----
@@ -6723,14 +6873,18 @@
 ----
   startTime = Fri 10:30
   interval  = 2 days
+  jitter    = 30 min
 ----
 +
 Assuming that the server is started on `Mon 07:00` then
 `startTime - now` is `4 days 3:30 hours`. This is larger than the
 interval hence the start time is preponed by the maximum integral
 multiple of the interval so that start time is still in the future,
-i.e. preponed by 4 days. This yields a start time of `Mon 10:30`, next
-executions are `Wed 10:30`, `Fri 10:30`. etc.
+i.e. preponed by 4 days. This yields a start time of `Mon 10:30`.
+After this adjustment, a random delay between 0 and 30 minutes is added.
+For example, if the delay is 7 minutes, the final start time becomes
+`Mon 10:37`. Subsequent executions will occur at fixed intervals from
+this updated start time, such as `Wed 10:37`, `Fri 10:37`, and so on.
 
 * Example 2:
 +
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 0d987a3..879251a 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -266,6 +266,10 @@
 `AnyWithBlock`} will be rejected. Existing label function definitions can only
 be updated to {`NoBlock`, `NoOp`, `PatchSetLock`}.
 
+[IMPORTANT]
+New label definitions should explicitly set the `function` attribute to a
+non-blocking value since the default is `MaxWithBlock`.
+
 If your project has a
 blocking label function, we highly encourage you to change it to `NoBlock` and
 add a submit-requirement for the same label. See the
@@ -281,13 +285,13 @@
 Valid values are:
 
 [[NoBlock]]
-* `NoBlock`/`NoOp` (default)
+* `NoBlock`/`NoOp`
 +
 The label is purely informational and values are not considered when
 determining whether a change is submittable.
 
 [[MaxWithBlock]]
-* `MaxWithBlock`
+* `MaxWithBlock` (default)
 +
 The lowest possible negative value, if present, blocks a submit, while
 the highest possible positive value is required to enable submit. There
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 1ec5208..49ec3f4 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -95,6 +95,11 @@
 The Footer templates will determine the contents of the footer text appended to
 the end of all outgoing emails after the ChangeFooter and CommentFooter.
 
+=== AuthTokenUpdate.soy and AuthTokenUpdateHtml.soy
+
+AuthTokenUpdate templates will determine the contents of the email related to adding,
+changing or deleting an authentication token on a user account.
+
 === HttpPasswordUpdate.soy and HttpPasswordUpdateHtml.soy
 
 HttpPasswordUpdate templates will determine the contents of the email related to adding,
diff --git a/Documentation/config-robot-comments.txt b/Documentation/config-robot-comments.txt
deleted file mode 100644
index ef99d80..0000000
--- a/Documentation/config-robot-comments.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-= Gerrit Code Review - Robot Comments
-
-[NOTE]
-Robot Comments are deprecated in favour of link:pg-plugin-checks-api.html[Checks API] and human
-comments.
-
-Gerrit has special support for inline comments that are generated by
-automated third-party systems, so called "robot comments". For example
-robot comments can be used to represent the results of code analyzers.
-
-In contrast to regular inline comments which are free-text comments,
-robot comments are more structured and can contain additional data,
-such as a robot ID, a robot run ID and a URL, see
-link:rest-api-changes.html#robot-comment-info[RobotCommentInfo] for
-details.
-
-It is planned to visualize robot comments differently in the web UI so
-that they can be easily distinguished from human comments. Users should
-also be able to use filtering on robot comments, so that only part of
-the robot comments or no robot comments are shown. In addition robot
-comments can contain fixes, that users can apply by a single click.
-
-== REST endpoints
-
-* Posting robot comments is done by the
-  link:rest-api-changes.html[Set Review] REST endpoint. The
-  link:rest-api-changes.html#review-input[input] for this REST endpoint
-  can contain robot comments in its `robot_comments` field.
-* link:rest-api-changes.html#list-robot-comments[List Robot Comments]
-* link:rest-api-changes.html#get-robot-comment[Get Robot Comment]
-
-== Storage
-
-Robot comments are stored per change in a
-`refs/changes/XX/YYYY/robot-comments` ref, where `XX/YYYY` is the
-sharded change ID.
-
-Robot comments can be dropped by deleting this ref.
-
-== Limitations
-
-* There is no support for draft robot comments, but robot comments are
-  always published and visible to everyone who can see the change.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 071bded..bb4b020 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -501,7 +501,7 @@
 'user', 'group').
 +
 'label:Code-Review=MAX,users=human_reviewers' can be used to
-implement "Want-Code-Review-From-All" functionaly, see
+implement "Want-Code-Review-From-All" functionality, see
 link#require-code-review-approvals-from-all-human-reviewers-example[examples
 below].
 
@@ -570,10 +570,24 @@
 the link:user-search.html#labels[label] query operator.
 +
 If used in submit requirement expressions, this operator supports an additional
-`user=non_contributor` argument. This argument works similar to the
-link:user-search.html#non_uploader["user=non_uploader"] argument and returns
-true if the change has a matching label vote that is applied by a user that's
-not the uploader, author or committer of the latest patchset.
+arguments that work similar to the
+link:user-search.html#non_uploader["user=non_uploader"] argument:
+
+* `user=non_author` returns true if the change has a matching
+  label vote that is applied by a user that's not the author of the
+  latest patchset (note that the author can be forged with the
+  link:access-control.html#category_forge_author[Forge Author]
+  permission, if this permission is assigned requiring a non-author
+  approval is not sufficient to prevent self-approvals)
+* `user=non_committer` returns true if the change has a matching
+  label vote that is applied by a user that's not the committer of the
+  latest patchset (note that the committer can be forged with the
+  link:access-control.html#category_forge_committer[Forge Committer]
+  permission, if this permission is assigned requiring a non-committer
+  approval is not sufficient to prevent self-approvals)
+* `user=non_contributor` returns true if the change has a matching
+  label vote that is applied by a user that's not the uploader, author
+  or committer of the latest patchset
 
 [[unsupported_operators]]
 === Unsupported Operators
@@ -646,6 +660,8 @@
   submittableIf = hasfooter:\"Bug\"
 ----
 
+Note, the matching doesn't work for the footer keys that contain underscores.
+
 [[require-code-review-approvals-from-all-human-reviewers-example]]
 === Require Code-Review approvals from all human reviewers
 
diff --git a/Documentation/config-themes.txt b/Documentation/config-themes.txt
index 7cf61e4..4336d3c 100644
--- a/Documentation/config-themes.txt
+++ b/Documentation/config-themes.txt
@@ -62,20 +62,9 @@
 which is always sent with a no-cache header.  Clients will see any
 changes immediately after they are made.
 
-Assets under `'$site_path'/static` whose file name matches one of the
-following patterns are served with a 1 year expiration, permitting
-very aggressive caching by clients and edge-proxies:
-
- * `*.cache.html`
- * `*.cache.gif`
- * `*.cache.png`
- * `*.cache.css`
- * `*.cache.jar`
- * `*.cache.swf`
-
-All other assets under `'$site_path'/static` are served with a 5
-minute expire, permitting some (limited) caching.  It may take up
-to 5 minutes after making a change, before clients see the changes.
+Assets under `'$site_path'/static` are served with a 15 minutes
+expiration, permitting some (limited) caching.  It may take up
+to 15 minutes after making a change, before clients see the changes.
 
 It is recommended that static images used in the site header
 or footer be named with a unique caching file name, for example
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index b0149fe..8b2c9d4 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -6,7 +6,6 @@
 [[new-commit-validation]]
 == New commit validation
 
-
 Plugins implementing the `CommitValidationListener` interface can
 perform additional validation checks against new commits.
 
@@ -21,6 +20,13 @@
 Out of the box, Gerrit includes a plugin that checks the length of the
 subject and body lines of commit messages on uploaded commits.
 
+[[push-options-validation]]
+== Push options validation
+
+Plugins implementing the `PushOptionsValidator` interface can validate push
+options. For example, they can reject options (or a combination of options) or
+emit a warning when a deprecated option is being used.
+
 [plugin-push-options]]
 === Plugin push options
 
diff --git a/Documentation/cross-repository-changes.txt b/Documentation/cross-repository-changes.txt
index 16067d5..1a87fb0 100644
--- a/Documentation/cross-repository-changes.txt
+++ b/Documentation/cross-repository-changes.txt
@@ -81,7 +81,7 @@
     used in Gerrit's search bar to find all the submitted changes for the
     submission. This ID is relevant when <<reverting,reverting a submission>>.
 
-To better underestand this behavior, consider this following example.
+To better understand this behavior, consider this following example.
 
 [[example_submission]]
 === Example Submission
@@ -104,7 +104,7 @@
 |===
 | Important point: B1 can unexpectedly block the submission of A2!
 This kind of situation is hard to immediately grok: B1 isn't in the topic you're
-trying to submit, and it isn't a depnedent change of A2. If your topic isn’t
+trying to submit, and it isn't a dependent change of A2. If your topic isn’t
 submittable and you can’t figure out why, this might be a reason.
 |===
 
@@ -245,7 +245,7 @@
 
 In the Gerrit search bar, you can search for changes attached to a specific
 topic using the `topic` operator e.g. `topic:MY_TOPIC_NAME`. The `intopic`
-operator works similary but supports free-text and regular expression search.
+operator works similarly but supports free-text and regular expression search.
 
 You can also search for a submission using the `submissionid` operator. Topic
 submission IDs are "<id>-<topic>" where id is the change number of the change
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 49f918d..246aafc 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -267,6 +267,26 @@
  `bazel-bin/Documentation/`
 ----
 
+[[license]]
+== License
+
+To check that licenses are up to date, run:
+
+----
+  bazelisk test Documentation:check_licenses
+----
+
+You can then copy by doing:
+
+----
+  cp bazel-bin/Documentation/licenses.gen.txt Documentation/licenses.txt
+  cp bazel-bin/Documentation/backend_licenses.gen.txt Documentation/backend_licenses.txt
+  cp bazel-bin/Documentation/js_licenses.gen.txt Documentation/js_licenses.txt
+----
+
+Note: Follow link:#npm-binary[NPM Binaries] for adding npm package dependencies licenses.
+      This is for js_licenses.txt.
+
 [[tests]]
 == Running Unit Tests
 
diff --git a/Documentation/dev-plugins-lifecycle.txt b/Documentation/dev-plugins-lifecycle.txt
index 2f5ffb7..7933743 100644
--- a/Documentation/dev-plugins-lifecycle.txt
+++ b/Documentation/dev-plugins-lifecycle.txt
@@ -245,7 +245,7 @@
 The plugin's maintainer puts a deprecation notice in the README.md of the
 plugin and pushes it for review. If nobody is willing to bring the code
 forward, the change gets merged, and the master branch is removed from the list
-of branches to be built on the GerritFoge CI.
+of branches to be built on the GerritForge CI.
 
 GERRIT
 ------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 0d0ccc2..aadfe1c 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1361,10 +1361,6 @@
 Runtime exceptions generated by the implementors of ChangePluginDefinedInfoFactory
 are encapsulated in PluginDefinedInfo objects which are part of SSH/REST query output.
 
-Implementors of the `ChangePluginDefinedInfoFactory` interface should check whether
-they need to contribute to the link:#change-etag-computation[change ETag computation]
-to prevent callers using ETags from potentially seeing outdated plugin attributes.
-
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
 
@@ -1876,43 +1872,6 @@
 }
 ----
 
-Sometimes plugins may want to be able to change the state of a patch set or
-change in the `UiAction.apply()` method and reflect these changes on the core
-UI. For example a buildbot plugin which exposes a 'Schedule' button on the
-patch set panel may want to disable that button after the build was scheduled
-and update the tooltip of that button. But because of Gerrit's caching
-strategy the following must be taken into consideration.
-
-The browser is allowed to cache the `UiAction` information until something on
-the change is modified. More accurately the change row needs to be modified in
-the database to have a more recent `lastUpdatedOn` or a new `rowVersion`, or
-the +refs/meta/config+ of the project or any parents needs to change to a new
-SHA-1. The ETag SHA-1 computation code can be found in the
-`ChangeResource.getETag()` method.
-
-The easiest way to accomplish this is to update `lastUpdatedOn` of the change:
-
-[source,java]
-----
-@Override
-public Object apply(RevisionResource rcrs, Input in) {
-  // schedule a build
-  [...]
-  // update change
-  try (BatchUpdate bu = batchUpdateFactory.create(
-      project.getNameKey(), user, TimeUtil.nowTs())) {
-    bu.addOp(change.getId(), new BatchUpdate.Op() {
-      @Override
-      public boolean updateChange(ChangeContext ctx) {
-        return true;
-      }
-    });
-    bu.execute();
-  }
-  [...]
-}
-----
-
 `UiAction` must be bound in a plugin module:
 
 [source,java]
@@ -3160,23 +3119,19 @@
 ----
 
 Plugin authors should also consider binding their SubmitRule using a `Gerrit-BatchModule`.
-See link:dev-plugins.html[Batch runtime] for more informations.
+See link:dev-plugins.html[Batch runtime] for more information.
 
 
 The SubmitRule extension point allows you to write complex rules, but writing
 small self-contained rules should be preferred: doing so allows end users to
 compose several rules to form more complex submit checks.
 
+[[submit-requirement]]
 The `SubmitRequirement` class allows rules to communicate what the user needs
 to change in order to be compliant. These requirements should be kept once they
 are met, but marked as `OK`. If the requirements were not displayed, reviewers
 would need to use their precious time to manually check that they were met.
 
-Implementors of the `SubmitRule` interface should check whether they need to
-contribute to the link:#change-etag-computation[change ETag computation] to
-prevent callers using ETags from potentially seeing outdated submittability
-information.
-
 `SubmitRule` interface will soon deprecated. Instead, a global `SubmitRequirement`
 can be bound by plugin.
 
@@ -3194,53 +3149,35 @@
 }
 ----
 
-[[change-etag-computation]]
-== Change ETag Computation
+[[label-type]]
+== Label Type
 
-By implementing the `com.google.gerrit.server.change.ChangeETagComputation`
-interface plugins can contribute a value to the change ETag computation.
-
-Plugins can affect the result of the get change / get change details REST
-endpoints by:
-
-* providing link:#query_attributes[plugin defined attributes] in
-  link:rest-api-changes.html#change-info[ChangeInfo]
-* implementing a link:#pre-submit-evaluator[pre-submit evaluator] which affects
-  the computation of `submittable` field in
-  link:rest-api-changes.html#change-info[ChangeInfo]
-
-If the plugin defined part of link:rest-api-changes.html#change-info[
-ChangeInfo] depends on plugin specific data, callers that use change ETags to
-avoid unneeded recomputations of ChangeInfos may see outdated plugin attributes
-and/or outdated submittable information, because a ChangeInfo is only reloaded
-if the change ETag changes.
-
-By implementating the `com.google.gerrit.server.change.ChangeETagComputation`
-interface plugins can contribute to the ETag computation and thus ensure that
-the change ETag changes when the plugin data was changed. This way it can be
-ensured that callers do not see outdated ChangeInfos.
-
-IMPORTANT: Change ETags are computed very frequently and the computation must
-be cheap. Take good care to not perform any expensive computations when
-implementing this.
+The `com.google.gerrit.entities.LabelType` extension point allows to define a
+global label programmatically that applies to all projects, unless overriding
+the label is allowed and projects override the label by defining a label with
+the same name.
 
 [source, java]
 ----
-import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
+import com.google.inject.AbstractModule;
 
-import com.google.common.hash.Hasher;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.change.ChangeETagComputation;
-
-public class MyPluginChangeETagComputation implements ChangeETagComputation {
-  public String getETag(Project.NameKey projectName, Change.Id changeId) {
-    Hasher hasher = Hashing.murmur3_128().newHasher();
-
-    // Add hashes for all plugin-specific data that affects change infos.
-    hasher.putString(sha1OfPluginSpecificChangeRef, UTF_8);
-
-    return hasher.hash().toString();
+public class MyPluginModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(LabelType.class).toInstance(
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .setCanOverride(false)
+            .build());
   }
 }
 ----
@@ -3426,6 +3363,17 @@
 Implementors can write logic to validate the diff before it's returned on the
 API.
 
+[[flow-service]]
+== Flow Service
+
+`com.google.gerrit.server.flow.FlowService` is an extension point to plug in a
+service to manage flows.
+
+A flow is an automation rule on a change that triggers actions on the change
+when the flow conditions become satisfied. For example, a flow can be an
+automation rule that adds a reviewer to the change when the change has been
+verified by the CI.
+
 == SEE ALSO
 
 * link:pg-plugin-dev.html[JavaScript Plugin API]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 70f41af..a22caec 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -148,8 +148,8 @@
 git clone http://username@localhost:8080/projectname
 ----
 
-The default password for user `admin` is `secret`. You can regenerate a
-password in the UI under User Settings -- HTTP credentials. The password can be
+The default token for user `admin` is `secret`. You can generate tokens
+in the UI under User Settings -- HTTP credentials. The tokens can be
 stored locally to avoid retyping it:
 
 ----
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
index a28e230..0b21b7f 100644
--- a/Documentation/dev-rest-api.txt
+++ b/Documentation/dev-rest-api.txt
@@ -73,7 +73,7 @@
  curl -n http://localhost:8080/a/path/to/api/
 ----
 
-In both cases, the password should be the user's link:user-upload.html#http[HTTP password].
+In both cases, the password should be one of the user's link:user-upload.html#http[authentication tokens].
 
 === Verifying Header Content
 
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 5d4a29b..cc9ac6c 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -63,7 +63,6 @@
 . link:config-hooks.html[Hooks]
 . link:config-mail.html[Mail Templates]
 . link:config-cla.html[Contributor Agreements]
-. link:config-robot-comments.html[Robot Comments]
 
 == Server Administration
 . link:install.html[Installation Guide]
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index f4c5504..eed18fb 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -601,7 +601,7 @@
 - REST API:
 +
 Gerrit provides a rich link:rest-api.html[REST API] that other tools
-can use to query information from Gerrit and and to trigger actions in
+can use to query information from Gerrit and to trigger actions in
 Gerrit.
 
 - Gerrit Plugins:
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 5b333f9..60970b0 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -475,7 +475,7 @@
 [[cherrypickof]]
 == Cherry-Pick changes of a Change
 
-When a change is created/updated using the 'cherry-pick' functionalty,
+When a change is created/updated using the 'cherry-pick' functionality,
 the original change and patchset details are recorded in the Change's
 cherrypick field. This field cannot be set or updated by the user in
 any way. It is set automatically after the cherry-pick operation completes
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 9253609..baf8e6a 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -461,6 +461,219 @@
 ----
 
 
+[[@material/web]]
+@material/web
+
+* @material/web
+
+[[@material/web_license]]
+----
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2018-2023 Google, Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+----
+
+
 [[DefinitelyTyped]]
 DefinitelyTyped
 
@@ -600,42 +813,16 @@
 ** package.json
 ** roboto.js
 ** update-fonts.sh
-* @polymer/iron-a11y-announcer
 * @polymer/iron-a11y-keys-behavior
-* @polymer/iron-autogrow-textarea
 * @polymer/iron-behaviors
 * @polymer/iron-checked-element-behavior
-* @polymer/iron-dropdown
-* @polymer/iron-fit-behavior
 * @polymer/iron-flex-layout
 * @polymer/iron-form-element-behavior
-* @polymer/iron-icon
-* @polymer/iron-iconset-svg
-* @polymer/iron-input
-* @polymer/iron-menu-behavior
 * @polymer/iron-meta
-* @polymer/iron-overlay-behavior
-* @polymer/iron-resizable-behavior
-* @polymer/iron-selector
 * @polymer/iron-validatable-behavior
-* @polymer/marked-element
-* @polymer/neon-animation
 * @polymer/paper-behaviors
 * @polymer/paper-button
-* @polymer/paper-card
-* @polymer/paper-dialog
-* @polymer/paper-dialog-behavior
-* @polymer/paper-dialog-scrollable
-* @polymer/paper-dropdown-menu
-* @polymer/paper-fab
-* @polymer/paper-icon-button
-* @polymer/paper-input
 * @polymer/paper-item
-* @polymer/paper-listbox
-* @polymer/paper-menu-button
-* @polymer/paper-tabs
-* @polymer/paper-toggle-button
-* @polymer/paper-tooltip
 
 [[Polymer-2015_license]]
 ----
@@ -677,52 +864,6 @@
 ----
 
 
-[[Polymer-2016]]
-Polymer-2016
-
-* @polymer/iron-image
-* @polymer/paper-checkbox
-
-[[Polymer-2016_license]]
-----
-Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
-
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
 [[Polymer-2017]]
 Polymer-2017
 
@@ -1396,34 +1537,34 @@
 
 [[highlightjs-structured-text_license]]
 ----
-BSD 3-Clause License

-

-Copyright (c) 2006, Ivan Sagalaev

-All rights reserved.

-

-Redistribution and use in source and binary forms, with or without

-modification, are permitted provided that the following conditions are met:

-

-1. Redistributions of source code must retain the above copyright notice, this

-   list of conditions and the following disclaimer.

-

-2. Redistributions in binary form must reproduce the above copyright notice,

-   this list of conditions and the following disclaimer in the documentation

-   and/or other materials provided with the distribution.

-

-3. Neither the name of the copyright holder nor the names of its

-   contributors may be used to endorse or promote products derived from

-   this software without specific prior written permission.

-

-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"

-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE

-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL

-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR

-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE

+BSD 3-Clause License
+
+Copyright (c) 2006, Ivan Sagalaev
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 ----
@@ -1595,23 +1736,23 @@
 
 [[resemblejs_license]]
 ----
-The MIT License (MIT) Copyright © 2013 Huddle

-

-Permission is hereby granted, free of charge, to any person obtaining a copy of

-this software and associated documentation files (the “Software”), to deal in

-the Software without restriction, including without limitation the rights to

-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of

-the Software, and to permit persons to whom the Software is furnished to do so,

-subject to the following conditions:

-

-The above copyright notice and this permission notice shall be included in all

-copies or substantial portions of the Software.

-

-THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS

-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR

-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER

-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN

+The MIT License (MIT) Copyright © 2013 Huddle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the “Software”), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 ----
@@ -2050,17 +2191,17 @@
 
 [[tslib_license]]
 ----
-Copyright (c) Microsoft Corporation.

-

-Permission to use, copy, modify, and/or distribute this software for any

-purpose with or without fee is hereby granted.

-

-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH

-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY

-AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,

-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM

-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR

-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR

+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 PERFORMANCE OF THIS SOFTWARE.
 
 ----
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 6783a15..d907106 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -2586,6 +2586,219 @@
 ----
 
 
+[[@material/web]]
+@material/web
+
+* @material/web
+
+[[@material/web_license]]
+----
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2018-2023 Google, Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+----
+
+
 [[DefinitelyTyped]]
 DefinitelyTyped
 
@@ -2725,42 +2938,16 @@
 ** package.json
 ** roboto.js
 ** update-fonts.sh
-* @polymer/iron-a11y-announcer
 * @polymer/iron-a11y-keys-behavior
-* @polymer/iron-autogrow-textarea
 * @polymer/iron-behaviors
 * @polymer/iron-checked-element-behavior
-* @polymer/iron-dropdown
-* @polymer/iron-fit-behavior
 * @polymer/iron-flex-layout
 * @polymer/iron-form-element-behavior
-* @polymer/iron-icon
-* @polymer/iron-iconset-svg
-* @polymer/iron-input
-* @polymer/iron-menu-behavior
 * @polymer/iron-meta
-* @polymer/iron-overlay-behavior
-* @polymer/iron-resizable-behavior
-* @polymer/iron-selector
 * @polymer/iron-validatable-behavior
-* @polymer/marked-element
-* @polymer/neon-animation
 * @polymer/paper-behaviors
 * @polymer/paper-button
-* @polymer/paper-card
-* @polymer/paper-dialog
-* @polymer/paper-dialog-behavior
-* @polymer/paper-dialog-scrollable
-* @polymer/paper-dropdown-menu
-* @polymer/paper-fab
-* @polymer/paper-icon-button
-* @polymer/paper-input
 * @polymer/paper-item
-* @polymer/paper-listbox
-* @polymer/paper-menu-button
-* @polymer/paper-tabs
-* @polymer/paper-toggle-button
-* @polymer/paper-tooltip
 
 [[Polymer-2015_license]]
 ----
@@ -2802,52 +2989,6 @@
 ----
 
 
-[[Polymer-2016]]
-Polymer-2016
-
-* @polymer/iron-image
-* @polymer/paper-checkbox
-
-[[Polymer-2016_license]]
-----
-Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
-
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
 [[Polymer-2017]]
 Polymer-2017
 
@@ -3521,34 +3662,34 @@
 
 [[highlightjs-structured-text_license]]
 ----
-BSD 3-Clause License

-

-Copyright (c) 2006, Ivan Sagalaev

-All rights reserved.

-

-Redistribution and use in source and binary forms, with or without

-modification, are permitted provided that the following conditions are met:

-

-1. Redistributions of source code must retain the above copyright notice, this

-   list of conditions and the following disclaimer.

-

-2. Redistributions in binary form must reproduce the above copyright notice,

-   this list of conditions and the following disclaimer in the documentation

-   and/or other materials provided with the distribution.

-

-3. Neither the name of the copyright holder nor the names of its

-   contributors may be used to endorse or promote products derived from

-   this software without specific prior written permission.

-

-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"

-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE

-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL

-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR

-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE

+BSD 3-Clause License
+
+Copyright (c) 2006, Ivan Sagalaev
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 ----
@@ -3720,23 +3861,23 @@
 
 [[resemblejs_license]]
 ----
-The MIT License (MIT) Copyright © 2013 Huddle

-

-Permission is hereby granted, free of charge, to any person obtaining a copy of

-this software and associated documentation files (the “Software”), to deal in

-the Software without restriction, including without limitation the rights to

-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of

-the Software, and to permit persons to whom the Software is furnished to do so,

-subject to the following conditions:

-

-The above copyright notice and this permission notice shall be included in all

-copies or substantial portions of the Software.

-

-THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS

-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR

-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER

-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN

+The MIT License (MIT) Copyright © 2013 Huddle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the “Software”), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 ----
@@ -4175,17 +4316,17 @@
 
 [[tslib_license]]
 ----
-Copyright (c) Microsoft Corporation.

-

-Permission to use, copy, modify, and/or distribute this software for any

-purpose with or without fee is hereby granted.

-

-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH

-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY

-AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,

-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM

-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR

-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR

+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 PERFORMANCE OF THIS SOFTWARE.
 
 ----
diff --git a/Documentation/logs.txt b/Documentation/logs.txt
index c7adcdf..2beddcc 100644
--- a/Documentation/logs.txt
+++ b/Documentation/logs.txt
@@ -154,7 +154,7 @@
 * `level`: log level (ERROR, WARN, INFO, DEBUG).
 * `logger`: name of the logger.
 * `message`: log message.
-* `stacktrace`: Java stacktrace when an execption was caught, usually spans multiple lines.
+* `stacktrace`: Java stacktrace when an exception was caught, usually spans multiple lines.
 
 === GC Log
 
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index 29c794c..5e883fc 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -91,12 +91,6 @@
     },
 ----
 
-Automated systems may post "robot comments" instead of normal
-comments, which are an extension of the previous comment, defined in
-the
-link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/entities/RobotComment.java[RobotComment]
-class.
-
 [[migration]]
 == Migration
 
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 08be0bb..8e6873c 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -124,18 +124,6 @@
 The end point contains the `<gr-formatted-text>` element holding the
 `CheckResult.message` (if any was set).
 
-=== robot-comment-controls
-The `robot-comment-controls` extension point is located inside each comment
-rendered on the diff page, and is only visible when the comment is a robot
-comment, specifically if the comment has a `robot_id` property.
-
-In addition to default parameters, the following are available:
-
-* `comment`
-+
-current comment displayed, an instance of
-link:rest-api-changes.html#comment-info[CommentInfo]
-
 === repo-command
 This endpoint is situated among the repository commands.
 
@@ -213,7 +201,7 @@
 
 * `change`
 +
-The change beinng potentially submitted, an instance of
+The change being potentially submitted, an instance of
 link:rest-api-changes.html#change-info[ChangeInfo]
 
 * `action`
diff --git a/Documentation/pgm-MigratePasswordsToTokens.txt b/Documentation/pgm-MigratePasswordsToTokens.txt
new file mode 100644
index 0000000..1a7b7d5
--- /dev/null
+++ b/Documentation/pgm-MigratePasswordsToTokens.txt
@@ -0,0 +1,49 @@
+= MigratePasswordsToTokens
+
+== NAME
+MigratePasswordsToTokens - Convert HTTP passwords of all users
+to authentication tokens.
+
+== SYNOPSIS
+[verse]
+--
+_java_ -jar gerrit.war _MigratePasswordsToTokens_
+  -d <SITE_PATH>
+  --lifetime <LIFETIME>
+--
+
+== DESCRIPTION
+The HTTP passwords have been deprecated in favor of authentication tokens.
+This command migrates all users' HTTP passwords to authentication tokens.
+It will delete the password from the `username` ExternalID and create a new
+token in the user-ref of the account. The new token will have the ID
+`legacy` and have the same value as the password.
+
+== OPTIONS
+
+-d::
+--site-path::
+	Path of the Gerrit site
+
+--lifetime::
+	Default lifetime of the tokens
+
+== CONTEXT
+This command can only be run offline with direct access to the server's
+site.
+
+== EXAMPLES
+To convert the HTTP passwords to tokens:
+
+----
+    $ java -jar gerrit.war MigratePasswordsToTokens -d site_path
+----
+
+== SEE ALSO
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/pgm-ReduceMaxTokenLifetime.txt b/Documentation/pgm-ReduceMaxTokenLifetime.txt
new file mode 100644
index 0000000..80adafb6
--- /dev/null
+++ b/Documentation/pgm-ReduceMaxTokenLifetime.txt
@@ -0,0 +1,47 @@
+= ReduceMaxTokenLifetime
+
+== NAME
+ReduceMaxTokenLifetime - Adapt lifetime of existing auth tokens to lower
+maximum lifetime.
+
+== SYNOPSIS
+[verse]
+--
+_java_ -jar gerrit.war _ReduceMaxTokenLifetime_
+  -d <SITE_PATH>
+  --lifetime <LIFETIME>
+--
+
+== DESCRIPTION
+If the maximum lifetime of auth tokens is being reduced, existing tokens might
+still have a longer lifetime. If the lifetime of these tokens should be reduced
+to match the new maximum lifetime, this can be done with this tool.
+
+== OPTIONS
+
+-d::
+--site-path::
+	Path of the Gerrit site
+
+--lifetime::
+	New maximum lifetime
+
+== CONTEXT
+This command can only be run offline with direct access to the server's
+site.
+
+== EXAMPLES
+To convert the HTTP passwords to tokens:
+
+----
+    $ java -jar gerrit.war ReduceMaxTokenLifetime -d site_path --lifetime "2d"
+----
+
+== SEE ALSO
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
index 2946d44a..01dc9cd 100644
--- a/Documentation/pgm-reindex.txt
+++ b/Documentation/pgm-reindex.txt
@@ -13,6 +13,7 @@
   [--verbose]
   [--list]
   [--index]
+  [--read-only-disk-caches]
 --
 
 == DESCRIPTION
@@ -55,6 +56,11 @@
 	Without this option a restart recreates the complete index
 	from scratch without reusing existing index documents.
 
+--read-only-disk-caches|--ro-disk-caches::
+    Use read-only disk caches for the reindexing process. This option
+    can be used to avoid writing old data into caches, which might
+    cause performance issues and a lot of disk space being used.
+
 == CONTEXT
 The secondary index must be enabled. See
 link:config-gerrit.html#index.type[index.type].
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index af944cf..2733e7c 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -326,7 +326,7 @@
 the change that is being evaluated. The easiest way to load this state is to
 test your `submit_rule` against a real change on a running gerrit instance. The
 command link:cmd-test-submit-rule.html[test-submit rule] loads a specific change
-and executes the `submit_rule`. It optionally reads the rule from from `stdin`
+and executes the `submit_rule`. It optionally reads the rule from `stdin`
 to facilitate easy testing.
 
 ----
diff --git a/Documentation/quota.txt b/Documentation/quota.txt
index 475ae72..3f2f37d 100644
--- a/Documentation/quota.txt
+++ b/Documentation/quota.txt
@@ -25,7 +25,7 @@
 If the quota enforcer wants to throttle HTTP requests, they should use
 link:quota.html#http-requests[HTTP Requests] instead.
 
-The quota groups used for checking follow the exact definition of the endoint in the REST
+The quota groups used for checking follow the exact definition of the endpoint in the REST
 API, but remove all IDs. The schema is:
 
 /restapi/<ENDPOINT>:<HTTP-METHOD>
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 45eca7c..c2ad7a0 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -65,14 +65,13 @@
 [[details]]
 --
 * `DETAILS`: Includes full name, preferred email, username, display
-name, avatars, status, state and tags for each account.
+name, avatars, status, state, tags and a deleted flag for each account.
 --
 
 [[all-emails]]
 --
 * `ALL_EMAILS`: Includes all registered emails. Requires the caller
-to have the link:access-control.html#capability_modifyAccount[Modify
-Account] global capability.
+to have the link:access-control.html#capability_viewSecondaryEmails[View Secondary Emails] global capability.
 --
 
 [[suggest-account]]
@@ -89,7 +88,7 @@
 ----
 
 Secondary emails are only included if the calling user has the
-link:access-control.html#capability_modifyAccount[Modify Account]
+link:access-control.html#capability_viewSecondaryEmails[View Secondary Emails]
 capability.
 
 .Response
@@ -167,7 +166,10 @@
     "display_name": "Super John",
     "email": "john.doe@example.com",
     "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==",
-    "http_password": "19D9aIn7zePb",
+    "tokens": [{
+        "id": "token1",
+        "token": "19D9aIn7zePb"
+    }],
     "groups": [
       "MyProject-Owners"
     ]
@@ -200,7 +202,7 @@
 --
 
 Deletes the given account if config `enableDelete` under
-link:#config-gerrit.html#accounts[accounts] section is enbaled.
+link:#config-gerrit.html#accounts[accounts] section is enabled.
 
 Currently only supporting self deletion (regardless of the way
 link:#account-id[\{account-id\}] is provided).
@@ -529,11 +531,17 @@
 
 [[set-http-password]]
 === Set/Generate HTTP Password
+
+*Note*: The use of HTTP passwords is deprecated. It is recommended to use
+authentication tokens instead. See link:#create-token[Create Authentication
+token]. This endpoint serves as an alias for link:#create-token[Create
+Authentication token] to provide backwards compatibility.
+
 --
 'PUT /accounts/link:#account-id[\{account-id\}]/password.http'
 --
 
-Sets/Generates the HTTP password of an account.
+Sets/Generates an authentication token with id `legacy` for an account.
 
 The options for setting/generating the HTTP password must be provided
 in the request body inside a link:#http-password-input[
@@ -551,7 +559,7 @@
   }
 ----
 
-As response the new HTTP password is returned.
+As response the new token is returned.
 
 .Response
 ----
@@ -563,15 +571,21 @@
   "ETxgpih8xrNs"
 ----
 
-If the HTTP password was deleted the response is "`204 No Content`".
+If the token was deleted the response is "`204 No Content`".
 
 [[delete-http-password]]
 === Delete HTTP Password
+
+*Note*: The use of HTTP passwords is deprecated. It is recommended to use
+authentication tokens instead. See link:#delete-token[Delete Authentication
+token]. This endpoint serves as an alias for link:#delete-token[Delete
+Authentication token] to provide backwards compatibility.
+
 --
 'DELETE /accounts/link:#account-id[\{account-id\}]/password.http'
 --
 
-Deletes the HTTP password of an account.
+Deletes the token with id `legacy` of an account.
 
 .Request
 ----
@@ -583,6 +597,116 @@
   HTTP/1.1 204 No Content
 ----
 
+[[create-token]]
+=== Create Authentication token
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/tokens/\{token-id\}'
+--
+
+Creates a new token for an account. The token is usually generated.
+Administrators can also set a specific token for an account.
+
+The options for setting the token must be provided in the
+request body inside a link:#auth-token-input[AuthTokenInput] entity.
+
+The account must have a username.
+The token-id must be unique among other token-ids used by the account. It has to
+start with a letter and must only contain upper- or lowercase letters, digits,
+`-` or `_`.
+
+Optionally, a lifetime for the token can be configured. The minimum lifetime is
+1 minute. The lifetime has to be provided using the following unit format:
+
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
+
+If no lifetime is provided, the token's lifetime will be unlimited.
+
+.Request
+----
+  PUT /accounts/self/tokens/example HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "id": "example",
+    "token": "secret_token_123",
+    "lifetime": "30d"
+  }
+----
+
+As response a link:#auth-token-info[AuthTokenInfo] containing the new token
+is returned. Note, that this is the only time the plain text token
+will be returned by Gerrit.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "example",
+    "token": "someRandomToken",
+    "expiration": "2025-05-02 08:54:35.198000000"
+  }
+----
+
+[[get-tokens]]
+=== List Authentication Tokens
+--
+'GET /accounts/link:#account-id[\{account-id\}]/tokens'
+--
+
+Lists the token ids of an account as a list of link:#auth-token-info[AuthTokenInfos].
+The plain text token will never be returned.
+
+.Request
+----
+  GET /accounts/self/tokens HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+        "id": "example",
+        "expiration": "2025-05-02 08:54:35.198000000"
+    },
+    {
+        "id": "another",
+        "expiration": "2025-05-04 08:54:35.198000000"
+    }
+  ]
+----
+
+[[delete-token]]
+=== Delete Authentication Token
+--
+'DELETE /accounts/link:#account-id[\{account-id\}]/tokens/\{token-id\}'
+--
+
+Deletes the token with the given `token-id` of an account.
+
+.Request
+----
+  DELETE /accounts/self/tokens/example HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[get-oauth-token]]
 === Get OAuth Access Token
 --
@@ -2311,6 +2435,11 @@
 |`status`          |optional|Status message of the account.
 |`inactive`        |not set if `false`|
 Whether the account is inactive.
+|`deleted`        |not set if `false`|
+Whether the account is deleted. +
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS]
 |`tags`      |optional, not set if empty|
 List of additional tags that this account has. The only +
 current tag an account can have is `SERVICE_USER`. +
@@ -2333,7 +2462,9 @@
 |`display_name` |optional|The display name of the user.
 |`email`        |optional|The email address of the user.
 |`ssh_key`      |optional|The public SSH key of the user.
-|`http_password`|optional|The HTTP password of the user.
+|`http_password`|optional|The HTTP password of the user. (deprecated)
+|`tokens`       |optional|
+A list of tokens in the form of link:#auth-token-input[AuthTokenInputs] to assign to the user.
 |`groups`       |optional|
 A list of link:rest-api-groups.html#group-id[group IDs] that identify
 the groups to which the user should be added.
@@ -2391,7 +2522,7 @@
 
 [[avatar-info]]
 === AvatarInfo
-The `AccountInfo` entity contains information about an avatar image of
+The `AvatarInfo` entity contains information about an avatar image of
 an account.
 
 [options="header",cols="1,6"]
@@ -2399,7 +2530,6 @@
 |Field Name|Description
 |`url`     |The URL to the avatar image.
 |`height`  |The height of the avatar image in pixels.
-|`width`   |The width of the avatar image in pixels.
 |======================
 
 [[capability-info]]
@@ -2780,6 +2910,37 @@
 password is deleted.
 |============================
 
+[[auth-token-input]]
+=== AuthTokenInput
+The `AuthTokenInput` entity contains information for setting/generating
+an authentication token.
+
+[options="header",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`id`           |If not set, the id in the URL will be used.|
+Must be the same as the id used in the URL.
+|`token`        |optional|
+The new token. Only Gerrit administrators may set the token directly.
+|`lifetime`     |optional|
+Lifetime of the token. After the given duration the token will be invalid.
+|============================
+
+[[auth-token-info]]
+=== AuthTokenInfo
+The `AuthTokenInfo` entity contains information about an authentication token.
+
+[options="header",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`id`           ||The id of the token.
+|`token`        |optional|
+The token in plain text. Will only be returned once when creating the token.
+|`expiration`   |optional|
+The timestamp at which the token will expire or has been expired. If `null`, token
+lifetime is unlimited.
+|============================
+
 [[oauth-token-info]]
 === OAuthTokenInfo
 The `OAuthTokenInfo` entity contains information about an OAuth access token.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4f1833c..b8b011d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2529,62 +2529,6 @@
   }
 ----
 
-[[list-change-robot-comments]]
-=== List Change Robot Comments (deprecated)
---
-'GET /changes/link:#change-id[\{change-id\}]/robotcomments'
---
-
-Lists the robot comments of all revisions of the change.
-
-Return a map that maps the file path to a list of
-link:#robot-comment-info[RobotCommentInfo] entries. The entries in the
-map are sorted by file path.
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/robotcomments/ HTTP/1.0
-----
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
-      {
-        "id": "TvcXrmjM",
-        "line": 23,
-        "message": "unused import",
-        "updated": "2016-02-26 15:40:43.986000000",
-        "author": {
-          "_account_id": 1000110,
-          "name": "Code Analyzer",
-          "email": "code.analyzer@example.com"
-        },
-        "robot_id": "importChecker",
-        "robot_run_id": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
-      },
-      {
-        "id": "TveXwFiA",
-        "line": 49,
-        "message": "wrong indention",
-        "updated": "2016-02-26 15:40:45.328000000",
-        "author": {
-          "_account_id": 1000110,
-          "name": "Code Analyzer",
-          "email": "code.analyzer@example.com"
-        },
-        "robot_id": "styleChecker",
-        "robot_run_id": "5c606c425dd45184484f9d0a2ffd725a7607839b"
-      }
-    ]
-  }
-----
-
 [[list-change-drafts]]
 === List Change Drafts
 --
@@ -3249,6 +3193,56 @@
   }
 ----
 
+[[evaluate-change-query-expression]]
+=== Evaluate Change Query Expression
+--
+'GET /changes/link:#change-id[\{change-id\}]/query?expression=\{expression\}'
+--
+
+Evaluates whether the given change query expression matches the change.
+
+The following URL parameters are supported:
+
+[[change-query-expression]]
+--
+* `expression`: link:user-search.html[Change query expression] for which it should be checked if
+  the change matches.
+--
+
+[[change-query-use-index]]
+--
+* `use-index`: Whether the change query expression should be evaluated against the change state in
+  the index. This is useful to avoid executing submit requirements when the query expression
+  contains operators that require checking if the change is submittable (e.g. "is:submittable").
+--
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/query?expression=is:submittable label:Auto-Submit+1 HTTP/1.0
+----
+
+As response a link:#evaluate-change-query-expression-result-info[
+EvaluateChangeQueryExpressionResultInfo] entity is returned that describes whether the change
+matches the expression.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "status": false
+    "passing_atoms": [
+      "label:Auto-Submit+1"
+    ],
+    "failing_atoms": [
+      "is:submittable"
+    ]
+  }
+----
+
 [[edit-endpoints]]
 == Change Edit Endpoints
 
@@ -3326,7 +3320,8 @@
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
 ----
 
-To upload a file as binary data in the request body:
+To upload a file as binary data a link:#file-content-input[FileContentInput]
+entity can be specified in the request body:
 
 .Request
 ----
@@ -3339,17 +3334,10 @@
   }
 ----
 
-Note that it must be base-64 encoded data uri.
+When a change edit doesn't exist for this change yet it is created
+automatically.
 
-The "file_mode" field is optional, and if provided must be in octal format. The field
-indicates whether the file is executable or not and has a value of either 100755
-(executable) or 100644 (not executable). If it's unset, this indicates no change
-has been made. New files default to not being executable if this parameter is not
-provided
-
-When change edit doesn't exist for this change yet it is created. When file
-content isn't provided, it is wiped out for that file. As response
-"`204 No Content`" is returned.
+As response "`204 No Content`" is returned:
 
 .Response
 ----
@@ -3734,6 +3722,278 @@
   HTTP/1.1 204 No Content
 ----
 
+[[flow-endpoints]]
+== Flow Endpoints
+
+
+[[is-flows-enabled]]
+=== Is Flows Enabled
+--
+'GET /changes/link:#change-id[\{change-id\}]/is-flows-enabled'
+--
+
+Returns whether flows are enabled for this change.
+
+As result an link:#enabled-info[IsFlowsEnabledInfo] entity is returned.
+
+.Request
+----
+  GET /changes/myProject~65178/is-flows-enabled HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "enabled": true
+  }
+----
+
+[[list-flows]]
+=== List Flows
+--
+'GET /changes/link:#change-id[\{change-id\}]/flows/'
+--
+
+Lists the flows of a change that are visible to the caller.
+
+As result a list of link:#flow-info[FlowInfo] entries is returned.
+
+The order of the returned flows is stable, but depends on the flow service
+implementation.
+
+.Request
+----
+  GET /changes/myProject~65178/flows/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "uuid": "zgcFrmAM",
+      "owner" {
+        "_account_id": 1000600
+      },
+      "created": "2025-06-03 09:59:32.126000000",
+      "stages": [
+        {
+          "expression": {
+            "condition": "com.google.gerrit[change:65779 is:merged]",
+            "action": {
+              "name": "MarkAsReady"
+            }
+          },
+          "state": "DONE"
+        }
+      ],
+      "last_evaluated": "2025-06-03 10:32:12.083000000",
+    },
+    {
+      "uuid": "XvcXrmjM",
+      "owner" {
+        "_account_id": 1000600
+      },
+      "created": "2025-06-03 10:55:36.133000000",
+      "stages": [
+        {
+          "expression": {
+            "condition": "com.google.gerrit[change:65178 label:Verified+1]",
+            "action": {
+              "name": "AddReviewer",
+              "parameters": {
+                "user": "foo@example.com"
+              }
+            }
+          },
+          "state": "FAILED",
+          "message": "user foo@example.com not found"
+        },
+        {
+          "expression": {
+            "condition": "com.google.gerrit[change:65178 label:Code-Review+2]",
+            "action": {
+              "name": "Submit",
+              "parameters": {
+                "change": "65178"
+              }
+            }
+          },
+          "state": "TERMINATED",
+          "message": "stage 1 has failed"
+        }
+      ],
+      "last_evaluated": "2025-06-03 12:12:10.084000000",
+    }
+  ]
+----
+
+Flows that are not visible to the caller are omitted from the result. Which
+permissions are required to see a flow depends on the flow service
+implementation.
+
+
+If no flow service is bound (i.e. if no plugin that provides a flow service is
+installed) `405 Method Not Allowed` is returned.
+
+[[create-flow]]
+=== Create Flow
+--
+'POST /changes/link:#change-id[\{change-id\}]/flows/'
+--
+
+Creates a flow on the change.
+
+The flow input must be provided as a link:#flow-input[FlowInput] entity in the
+request body.
+
+.Request
+----
+  POST /changes/ HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "stage_expressions": [
+      {
+        "condition": "com.google.gerrit[change:65178 label:Verified+1]",
+        "action": {
+          "name": "AddReviewer",
+          "parameters": {
+            "user": "foo@example.com"
+          }
+        }
+      }
+    ]
+  }
+----
+
+The created flow is returned as a link:#flow-info[FlowInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "uuid": "XvcXrmjM",
+    "owner" {
+      "_account_id": 1000600
+    },
+    "created": "2025-06-03 09:59:32.126000000",
+    "stages": [
+      {
+        "expression": {
+          "condition": "com.google.gerrit[change:65178 label:Verified+1]",
+          "action": {
+            "name": "AddReviewer",
+            "parameters": {
+              "user": "foo@example.com"
+            }
+          }
+        }
+        "state": "PENDING"
+      }
+    ]
+  }
+----
+
+If the caller is not allowed to create the flow `403 Forbidden` is returned.
+Which permissions are required for creating a flow depends on the flow service
+implementation.
+
+If no flow service is bound (i.e. if no plugin that provides a flow service is
+installed) `405 Method Not Allowed` is returned.
+
+[[get-flow]]
+=== Get Flow
+--
+'GET /changes/link:#change-id[\{change-id\}]/flows/link:#flow-id[\{flow-id\}]'
+--
+
+Gets a flow on the change.
+
+.Request
+----
+  GET /changes/myProject~65178/flows/XvcXrmjM HTTP/1.0
+----
+
+The flow is returned as a link:#flow-info[FlowInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "uuid": "XvcXrmjM",
+    "owner" {
+      "_account_id": 1000600
+    },
+    "created": "2025-06-03 09:59:32.126000000",
+    "stages": [
+      {
+        "expression": {
+          "condition": "com.google.gerrit[change:65178 label:Verified+1]",
+          "action": {
+            "name": "AddReviewer",
+            "parameters": {
+              "user": "foo@example.com"
+            }
+          }
+        }
+        "state": "PENDING"
+      }
+    ]
+  }
+----
+
+If the flow exists, but is not visible to the caller `404 Not Found` is
+returned. Which permissions are required to see a flow depends on the flow
+service implementation.
+
+If no flow service is bound (i.e. if no plugin that provides a flow service is
+installed) `405 Method Not Allowed` is returned.
+
+[[delete-flow]]
+=== Delete Flow
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/flows/link:#flow-id[\{flow-id\}]'
+--
+
+Delete a flow on the change.
+
+.Request
+----
+  DELETE /changes/myProject~65178/flows/XvcXrmjM HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+If the flow exists, but is not visible to the caller `404 Not Found` is
+returned.
+
+If the caller can see the flow but is not allowed to delete it `403 Forbidden`
+is returned. Which permissions are required to delete a flow depends on the flow
+service implementation.
+
+If no flow service is bound (i.e. if no plugin that provides a flow service is
+installed) `405 Method Not Allowed` is returned.
 
 [[reviewer-endpoints]]
 == Reviewer Endpoints
@@ -5079,6 +5339,11 @@
 parent's position in the commit object and the returned content is a diff
 compared to that parent.
 
+Adding query parameter `context` (e.g. `/changes/.../patch?context=10`)
+sets the number of context lines that are displayed in the patch. If the
+context is not set, the default of 3 is used. It can also be abbreviated as
+`U`.
+
 [[get-mergeable]]
 === Get Mergeable
 --
@@ -5616,102 +5881,6 @@
   }
 ----
 
-[[list-robot-comments]]
-=== List Robot Comments (deprecated)
---
-'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/'
---
-
-Lists the link:config-robot-comments.html[robot comments] of a
-revision.
-
-As result a map is returned that maps the file path to a list of
-link:#robot-comment-info[RobotCommentInfo] entries. The entries in the
-map are sorted by file path.
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/robotcomments/ HTTP/1.0
-----
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
-      {
-        "id": "TvcXrmjM",
-        "line": 23,
-        "message": "unused import",
-        "updated": "2016-02-26 15:40:43.986000000",
-        "author": {
-          "_account_id": 1000110,
-          "name": "Code Analyzer",
-          "email": "code.analyzer@example.com"
-        },
-        "robot_id": "importChecker",
-        "robot_run_id": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
-      },
-      {
-        "id": "TveXwFiA",
-        "line": 49,
-        "message": "wrong indention",
-        "updated": "2016-02-26 15:40:45.328000000",
-        "author": {
-          "_account_id": 1000110,
-          "name": "Code Analyzer",
-          "email": "code.analyzer@example.com"
-        },
-        "robot_id": "styleChecker",
-        "robot_run_id": "5c606c425dd45184484f9d0a2ffd725a7607839b"
-      }
-    ]
-  }
-----
-
-[[get-robot-comment]]
-=== Get Robot Comment (deprecated)
---
-'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/link:#comment-id[\{comment-id\}]'
---
-
-Retrieves a link:config-robot-comments.html[robot comment] of a
-revision.
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/robotcomments/TvcXrmjM HTTP/1.0
-----
-
-As response a link:#robot-comment-info[RobotCommentInfo] entity is
-returned that describes the robot comment.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "id": "TvcXrmjM",
-    "line": 23,
-    "message": "unused import",
-    "updated": "2016-02-26 15:40:43.986000000",
-    "author": {
-      "_account_id": 1000110,
-      "name": "Code Analyzer",
-      "email": "code.analyzer@example.com"
-    },
-    "robot_id": "importChecker",
-    "robot_run_id": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
-  }
-----
-
 [[list-ported-comments]]
 === List Ported Comments
 --
@@ -5725,10 +5894,8 @@
 comments are ported as that logic might change in the future. Instead, callers must be able to
 handle any smaller/larger set of comments returned by this endpoint.
 
-Typically, a comment thread is returned fully or excluded fully. However, draft comments and
-robot comments are ignored and not returned via this endpoint. Hence, it's possible to get ported
-comments from this endpoint which are a reply to a non-ported robot comment. Callers must be
-able to deal with this situation.
+Typically, a comment thread is returned fully or excluded fully. However, draft comments
+are ignored and not returned via this endpoint.
 
 The returned comments are organized in a map of file path to link:#comment-info[CommentInfo] entries
 in the same fashion as for the link:#list-comments[List Revision Comments] endpoint.
@@ -5811,7 +5978,7 @@
 Depending on the filtering rules, it's possible that this endpoint returns a draft comment which is
 a reply to a comment thread which is not returned by the
 link:#list-ported-comments[List Ported Comments] endpoint. That's intended behavior. Callers must be
-able to handle this situation. The same holds for drafts which are a reply to a robot comment.
+able to handle this situation.
 
 Different than the link:#list-ported-comments[List Ported Comments] endpoint, the `author` of the
 returned comments is not filled for this endpoint as only comments of the calling user are returned.
@@ -6955,6 +7122,10 @@
 === \{draft-id\}
 UUID of a draft comment.
 
+[[flow-id]]
+=== \{flow-id\}
+UUID of a flow.
+
 [[label-id]]
 === \{label-id\}
 The name of the label.
@@ -7377,7 +7548,7 @@
 labels] are requested.
 |`permitted_labels`   |optional|
 A map of the permitted labels that maps a label name to the list of
-values that are allowed for that label. +
+values that the current user can vote on. +
 Only set if link:#detailed-labels[detailed labels] are requested.
 |`removable_labels`   |optional|
 A map of the removable labels that maps a label name to the map of
@@ -7884,6 +8055,20 @@
 [options="header",cols="1,^1,5"]
 |=============================
 |Field Name           ||Description
+|`base`               |optional|
+The SHA1 of the commit that was used as the base commit for the Git merge that created the
+revision. +
+A base is not set if: +
+- the merged commits do not have a common ancestor (in this case `no_base_reason` is
+`NO_COMMON_ANCESTOR`). +
+- the merged commits have multiple merge bases (happens for criss-cross-merges) and the base was
+computed (in this case `no_base_reason` is `COMPUTED_BASE`). +
+- a one sided merge strategy (e.g. `ours` or `theirs`) has been used and computing a base was not
+required for the merge (in this case `no_base_reason` is `ONE_SIDED_MERGE_STRATEGY`). +
+- the revision was not created by performing a Git merge operation (in this case `no_base_reason`
+is `NO_MERGE_PERFORMED`). +
+- the revision has been created before Gerrit started to store the base for conflicts (in this case
+`no_base_reason` is `HISTORIC_DATA_WITHOUT_BASE`).
 |`ours`               |optional|
 The SHA1 of the commit that was used as "ours" for the Git merge that created the revision. +
 - For merge commits that are created by the link:#create-change[Create Change] REST endpoint
@@ -7907,7 +8092,7 @@
 set if the revision was created by Gerrit as a result of performing a Git merge.
 |`theirs`             |optional|
 The SHA1 of the commit that was used as "theirs" for the Git merge that created the revision. +
-- For merge commits that are created by the the link:#create-change[Create Change] and the
+- For merge commits that are created by the link:#create-change[Create Change] and the
 link:#create-merge-patch-set-for-change[Create Merge Patch Set For Change] REST endpoints "theirs"
 is the SHA1 of the source branch (the branch that is specified as `source` in the
 link:#merge-input[MergeInput]). +
@@ -7919,6 +8104,21 @@
 patch set is being rebased. +
 Guaranteed to be set if `contains_conflicts` is `true`. If `contains_conflicts` is `false`, only
 set if the revision was created by Gerrit as a result of performing a Git merge.
+|`merge_strategy`     |optional|
+The merge strategy was used for the Git merge that created the revision. +
+Possible values: `resolve`, `recursive`, `simple-two-way-in-core`, `ours` and `theirs`.
+|`no_base_reason`     |optional|
+Reason why `base` is not set. +
+Only set if `base` is not set. +
+Possible values are: +
+- `NO_COMMON_ANCESTOR`: The merged commits do not have a common ancestor. +
+- `COMPUTED_BASE`: The merged commits have multiple merge bases (happens for criss-cross-merges)
+and the base was computed. +
+- `ONE_SIDED_MERGE_STRATEGY`: A one sided merge strategy (e.g. `ours` or `theirs`) has been used
+and computing a base was not required for the merge. +
+- `NO_MERGE_PERFORMED`: The revision was not created by performing a Git merge operation. +
+- `HISTORIC_DATA_WITHOUT_BASE`: The revision has been created before Gerrit started to store the
+base for conflicts.
 |`contains_conflicts` ||
 Whether any of the files in the revision has a conflict due to merging "ours" and "theirs". +
 If "true" at least one of the files in the revision has a conflict and contains Git conflict
@@ -8194,6 +8394,33 @@
 link:#rebase-edit[rebases the change edit] and conflicts are allowed.
 |===========================
 
+[[enabled-info]]
+=== IsFlowsEnabledInfo
+The `IsFlowsEnabledInfo` entity contains information about whether flows is enabled.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`enabled`     ||A boolean indicating whether the feature is enabled.
+|===========================
+
+[[evaluate-change-query-expression-result-info]]
+=== EvaluateChangeQueryExpressionResultInfo
+The `EvaluateChangeQueryExpressionResultInfo` entity contains the result of
+evaluating a change query expression.
+
+[options="header",cols="1,^1,5"]
+|===============================
+|Field Name        ||Description
+|`status`          ||Whether the change matches the change query expression.
+|`passing_atoms`   ||List of passing leaf atoms (atoms that match the change).
+|`failing_atoms`   ||List of failing leaf atoms (atoms that do not match the
+change).
+|`atom_explanation`|optional|Explanations for why atoms pass or fail.
+Explanations are only available for a few atoms, for most atoms no explanation
+is provided. Not set if none of the atoms has an explanation.
+|===============================
+
 [[fetch-info]]
 === FetchInfo
 The `FetchInfo` entity contains information about how to fetch a patch
@@ -8210,6 +8437,24 @@
 Only set if link:#download-commands[download commands] are requested.
 |==========================
 
+[[file-content-input]]
+=== FileContentInput
+The `FileContentInput` entity contains information for creating/updating a file
+in a change edit.
+
+[options="header",cols="1,^1,5"]
+|==============================
+|Field Name       ||Description
+|`binary_content` |optional|The file content as a base-64 encoded data URI. If
+no content is provided, an empty is created or if an existing file is updated
+the file content is removed so that the file becomes empty. The content must be
+a SHA1 if the file mode is `160000` (gitlink).
+|`file_mode`      |optional|The file mode in octal format. Supported values are
+`100644` (regular file), `100755` (executable file), `120000` (symlink) and
+`160000` (gitlink).  If unset, new files are created with file mode `100644`
+(regular file) and for existing files the existing file mode is kept.
+|==============================
+
 [[file-info]]
 === FileInfo
 The `FileInfo` entity contains information about a file in a patch set.
@@ -8244,6 +8489,10 @@
 |`new_mode`        |optional|File mode in octal (e.g. 100644) at the new commit.
 The first three digits indicate the file type and the last three digits contain
 the file permission bits. For deleted files, this field will not be present.
+|`old_sha`        |optional|SHA-1 of the file content at the old commit.
+For added files, this field will not be present.
+|`new_sha`        |optional|SHA-1 of the file content at the new commit.
+For deleted files, this field will not be present.
 |=============================
 
 [[fix-input]]
@@ -8296,6 +8545,91 @@
 |`replacement`   |The content which should be used instead of the current one.
 |==========================
 
+[[flow-info]]
+=== FlowInfo
+The `FlowInfo` entity contains information about a flow. A flow is an automation
+rule on a change that triggers actions on the change when the flow conditions
+become satisfied. For example, a flow can be an automation rule that adds a
+reviewer to the change when the change has been verified by the CI.
+
+[options="header",cols="1,^1,5"]
+|==============================
+|Field Name       ||Description
+|`uuid`           ||The universally unique identifier that identifies the flow.
+|`owner`          ||The owner of the flow as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`created`        ||The link:rest-api.html#timestamp[timestamp] of when the flow
+was created.
+|`stages`         ||The stages of this flow as a list of
+link:#flow-stage-info[FlowStageInfo] entities (sorted by execution order).
+|`last_evaluated` |optional|The link:rest-api.html#timestamp[timestamp] of when
+the flow was last evaluated. Not set if the flow has not been evaluated yet.
+|==============================
+
+[[flow-action-info]]
+=== FlowActionInfo
+The `FlowActionInfo` entity contains information about a flow action. A flow
+action is an action that should be triggered when the condition of a
+link:#flow-expression-info[FlowExpressionInfo] becomes satisfied.
+
+[options="header",cols="1,6"]
+|=========================
+|Field Name   |Description
+|`name`       |The name of the action. Which actions are supported depends on
+the flow service implementation.
+|`parameters` |Parameters for the action as list of strings. Which parameters
+are supported and their format depends on the flow service implementation.
+|=========================
+
+[[flow-expression-info]]
+=== FlowExpressionInfo
+The `FlowExpressionInfo` entity contains information about a flow expression. A
+flow expression defines an action that should be triggered when a condition
+becomes satisfied.
+
+[options="header",cols="1,6"]
+|========================
+|Field Name  ||Description
+|`condition` ||The condition which must be satisfied for the action to be
+triggered. Can contain multiple conditions separated by comma. The syntax of the
+condition depends on the flow service implementation.
+|`action`    |optional|The action that should be triggered when the condition is
+satisfied as a link:#flow-action-info[FlowActionInfo] entity.
+|========================
+
+[[flow-input]]
+=== FlowInput
+The `FlowInput` entity defines the properties for creating a new flow.
+
+[options="header",cols="1,6"]
+|================================
+|Field Name          |Description
+|`stage_expressions` |The expressions for the stages of the flow (sorted by
+execution order) as  a list of link:#flow-expression-info[FlowExpressionInfo]
+entities.
+|================================
+
+[[flow-stage-info]]
+=== FlowStageInfo
+The `FlowStageInfo` entity contains information about a stage in a flow. A flow
+stage consists out of a flow expression that defines an action that should be
+triggered when a condition becomes satisfied and a status.
+
+[options="header",cols="1,^1,5"]
+|==========================
+|Field Name   ||Description
+|`expression` ||The expression defining the condition and the action of
+this stage as a link:#flow-expression-info[FlowExpressionInfo] entity.
+|`state`      ||The state for this stage. Can be `PENDING` (the condition of
+the stage is not satisfied yet or the action has not been executed yet), `DONE`
+(the condition of the stage is satisfied and the action has been executed),
+`FAILED` (the stage has a non-recoverable error, e.g. performing the action has
+failed) or `TERMINATED` (the stage has been terminated without having been
+executed, e.g. because a previous stage failed or because it wasn't done within
+a timeout).
+|`message`    |optional|Optional message for the stage.
+|==========================
+
 [[git-person-info]]
 === GitPersonInfo
 The `GitPersonInfo` entity contains information about the
@@ -8749,6 +9083,8 @@
 |`submittable`             |optional|Boolean indicating whether the change is
 submittable. +
 Only populated if link:#get-related-changes-submittable[requested].
+|`work_in_progress`   |optional, not set if `false`|Boolean indicating whether the change is
+work in progress.
 |===========================
 
 [[related-changes-info]]
@@ -8802,8 +9138,7 @@
 |=================================
 |Field Name          ||Description
 |`message`           |optional|
-Message to be added as review comment to the change when reverting the
-change.
+Commit message of the revert commit. If not specified, a default commit message is set.
 |`notify`            |optional|
 Notify handling that defines to whom email notifications should be sent
 for reverting the change. +
@@ -8905,9 +9240,6 @@
 |`comments`                             |optional|
 The comments that should be added as a map that maps a file path to a
 list of link:#comment-input[CommentInput] entities.
-|`robot_comments`                       |optional, deprecated|
-The robot comments that should be added as a map that maps a file path
-to a list of link:#robot-comment-input[RobotCommentInput] entities.
 |`drafts`                               |optional|
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
@@ -9170,58 +9502,6 @@
 by Gerrit operation).
 |===========================
 
-[[robot-comment-info]]
-=== RobotCommentInfo (deprecated)
-The `RobotCommentInfo` entity contains information about a robot inline
-comment.
-
-`RobotCommentInfo` has the same fields as <<comment-info,CommentInfo>>
-except for the `unresolved` field which doesn't exist for robot comments.
-In addition `RobotCommentInfo` has the following fields:
-
-[options="header",cols="1,^1,5"]
-|===========================
-|Field Name       ||Description
-|`robot_id`       ||The ID of the robot that generated this comment.
-|`robot_run_id`   ||An ID of the run of the robot.
-|`url`            |optional|URL to more information.
-|`properties`     |optional|Robot specific properties as map that maps arbitrary
-keys to values.
-|===========================
-
-[[robot-comment-input]]
-=== RobotCommentInput (deprecated)
-The `RobotCommentInput` entity contains information for creating an inline
-robot comment.
-
-[options="header",cols="1,^1,5"]
-|===========================
-|Field Name    ||Description
-|`path`        ||
-link:#file-id[The file path] for which the inline comment should be added.
-|`side`        |optional|
-The side on which the comment should be added. +
-Allowed values are `REVISION` and `PARENT`. +
-If not set, the default is `REVISION`.
-|`line`        |optional|
-The number of the line for which the comment should be added. +
-`0` if it is a file comment. +
-If neither line nor range is set, a file comment is added. +
-If range is set, this value is ignored in favor of the `end_line` of the range.
-|`range`       |optional|
-The range of the comment as a link:#comment-range[CommentRange]
-entity.
-|`in_reply_to` |optional|
-The URL encoded UUID of the comment to which this comment is a reply.
-|`message`     |optional|
-The comment message.
-|`robot_id`       ||The ID of the robot that generated this comment.
-|`robot_run_id`   ||An ID of the run of the robot.
-|`url`            |optional|URL to more information.
-|`properties`     |optional|Robot specific properties as map that maps arbitrary
-keys to values.
-|===========================
-
 [[rule-input]]
 === RuleInput
 The `RuleInput` entity contains information to test a Prolog rule.
@@ -9256,7 +9536,7 @@
 Notify handling that defines to whom email notifications should be sent after
 the change is submitted. +
 Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
-If not set, the default is `ALL`.+
+If not set, the default is `ALL`. +
 Ignored if a post approval diff is present (i.e. if the change is submitted
 with copied approvals), because in this case everyone should be informed
 about the non-reviewed diff which has been applied after the change has been
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 76298d7..8c4a96e 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -940,6 +940,85 @@
 * `enabled-only`: If specified only enabled experiments are listed.
 --
 
+[[list-global-labels]]
+=== List Global Labels
+--
+'GET /config/server/labels'
+--
+
+Lists the globally defined labels (labels that are added programatically via the
+link:dev-plugins.html#label-type[LabelType] extension point).
+
+As result a list of labels as link:rest-api-projects.html#label-definition-info[
+LabelDefinitionInfo] entities is returned.
+
+The entries in the list are sorted by label name.
+
+.Request
+----
+  GET /config/server/labels/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "name": "Global-Label",
+      "function": "NoBlock",
+      "values": {
+        " 0": "No score",
+        "-1": "Rejected",
+        "+1": "Approved",
+      },
+      "default_value": 0,
+      "can_override": false,
+      "copy_condition": "changekind:NO_CHANGE OR changekind:TRIVIAL_REBASE or is:MIN",
+      "allow_post_submit": true
+    }
+  ]
+----
+
+[[list-global-submit-requirements]]
+=== List Global Submit Requirements
+--
+'GET /config/server/submit-requirements'
+--
+
+Lists the globally defined submit requirements (submit requirements that are
+added programatically via the
+link:dev-plugins.html#submit-requirement[SubmitRequirement] extension point).
+
+As result a list of submit-requirements as
+link:rest-api-projects.html#submit-requirement-info[ SubmitRequirementInfo]
+entities is returned.
+
+The entries in the list are sorted by submit requirement name.
+
+.Request
+----
+  GET /config/server/submit-requirements/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "name": "Global-Submit-Requirement",
+      "description": "A global submit requirement",
+      "submittability_expression": "label:Global-Label=+2",
+      "allow_override_in_child_projects": false
+    }
+  ]
+----
+
 [[list-tasks]]
 === List Tasks
 --
@@ -1828,6 +1907,60 @@
   Content-Disposition: attachment
 ----
 
+[[passwords.to.tokens]]
+=== Migration of HTTP passwords to authentication tokens
+
+This endpoint allows Gerrit administrators to migrate remaining HTTP passwords
+to authentication tokens. For the user the migration is invisible, i.e. the
+same credential is still valid.
+
+An optional lifetime for the credential can be set in the
+link:#migrate-passwords-to-tokens-input[MigratePasswordsToTokens.Input].
+
+This migration task will run asynchronously.
+
+.Request
+----
+  POST /config/server/passwords.to.tokens HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "lifetime": "3 months",
+  }
+----
+
+.Response
+----
+  HTTP/1.1 202 Accepted
+  Content-Disposition: attachment
+----
+
+[[reduce.token.lifetime]]
+=== Apply maximum lifetime to authentication tokens
+
+This endpoint allows Gerrit administrators to apply a shorter maximum lifetime
+to existing authentication tokens. This is required, when a shorter maximum
+lifetime is enforced. Changing the configuration will not apply that maximum
+to existing tokens. These have to be adapted using this REST API endpoint.
+
+This migration task will run asynchronously.
+
+.Request
+----
+  POST /config/server/reduce.token.lifetime HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "lifetime": "3 months",
+  }
+----
+
+.Response
+----
+  HTTP/1.1 202 Accepted
+  Content-Disposition: attachment
+----
+
 [[experiment-endpoints]]
 == Experiment Endpoints
 
@@ -1965,6 +2098,9 @@
 Git over HTTP and REST API requests when
 link:config-gerrit.html#auth.type[authentication type] is `LDAP`,
 `LDAP_BIND` or `OAUTH`. Can be `HTTP`, `LDAP`, `HTTP_LDAP` or `OAUTH`.
+|`max_token_lifetime`         |optional|
+The link:config-gerrit.html#auth.maxAuthTokenLifetime[maximum lifetime]
+of authentication tokens.
 |==========================================
 
 [[cache-info]]
@@ -2048,10 +2184,10 @@
 configuration parameter] that controls whether the mergeability bit in
 link:rest-api-changes.html#change-info[ChangeInfo] will never be set and if the
 bit is indexed.
-|`enable_robot_comments`|not set if `false`|
-link:config-gerrit.html#change.enableRobotComments[Are robot comments enabled?].
 |`conflicts_predicate_enabled`|not set if `false`|
 link:config-gerrit.html#change.conflictsPredicateEnabled[Are conflicts enabled?].
+|`allow_markdown_base64_images_in_comments`|not set if `false`|
+link:config-gerrit.html#change.allowMarkdownBase64ImagesInComments[Are markdown base64 images in comments allowed?].
 |=============================
 
 [[change-index-config-info]]
@@ -2337,6 +2473,19 @@
 link:config-gerrit.html#gerrit.defaultBranch[Name of the default branch to use on the project creation].
 |=================================
 
+[[groups-config-info]]
+=== GroupsConfigInfo
+The `GroupsConfigInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#groups[groups]
+section.
+
+[options="header",cols="1,6"]
+|=============================
+|Field Name            |Description
+|`enable_delete_group`   |
+link:config-gerrit.html#groups.enableDeleteGroup[gerrit.config].
+|=============================
+
 [[index-config-info]]
 === IndexConfigInfo
 The `IndexConfigInfo` entity contains information about Gerrit
@@ -2516,6 +2665,10 @@
 Information about the configuration from the
 link:config-gerrit.html#gerrit[gerrit] section as link:#gerrit-info[
 GerritInfo] entity.
+|`groups`                ||
+Information about the configuration from the
+link:config-gerrit.html#groups[groups] section as
+link:#groups-config-info[GroupsConfigInfo] entity.
 |`index`                  ||
 Information about the configuration from the
 link:config-gerrit.html#index[index] section as link:#index[
@@ -2547,6 +2700,9 @@
 The list of submit requirement names that should be displayed as separate
 columns in the dashboard. If empty, the default is to display all submit
 requirements that are applicable for changes appearing in the dashboard.
+|`dashboard_show_all_labels` |not set if `false`|
+Whether to show all labels in the dashboard, even if they are not submit
+requirements.
 |`metadata`                ||
 Optional server metadata as a list of link:#metadata-info[MetadataInfo]
 entities. If and which metadata is provided depends on the Gerrit setup.
@@ -2734,6 +2890,16 @@
 `age:4w limit:100 -project:some/repo -hashtag:keep-alive`. By default unset.
 |=============================
 
+[[migrate-passwords-to-tokens-input]]
+=== MigratePasswordsToTokens.Input
+The `MigratePasswordsToTokens.Input` entity is being used to configure the
+migration of HTTP passwords to auth tokens.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name||Description
+|`lifetime`||Lifetime of the migrated token.
+|=============================
 
 GERRIT
 ------
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index c43ebf2..5e9136b 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -507,6 +507,47 @@
 UUID was specified and the UUID is already in use, the response is
 "`409 Conflict`".
 
+[[delete-group]]
+=== Delete Group
+--
+'DELETE /groups/link:#group-id[\{group-id\}]'
+--
+
+Delete group.
+The group to delete must be internal group.
+
+This endpoint is only allowed for Gerrit's internal groups; attempting to call on a
+non-internal group will return 405 Method Not Allowed.
+
+.Request
+----
+  DELETE /groups/MyProject-Committers HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-internal-group]]
+=== Delete Internal Group
+--
+'POST /groups/link:#group-id[\{group-id\}].delete'
+--
+
+Delete group.
+The deleted group must be internal group.
+
+.Request
+----
+  POST /groups/MyProject-Committers.delete HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
 [[get-group-detail]]
 === Get Group Detail
 --
@@ -1479,47 +1520,6 @@
   HTTP/1.1 204 No Content
 ----
 
-[[delete-group]]
-=== Delete Group
---
-'DELETE /groups/link:#group-id[\{group-id\}]'
---
-
-Delete group.
-The group to delete must be internal group.
-
-This endpoint is only allowed for Gerrit's internal groups; attempting to call on a
-non-internal group will return 405 Method Not Allowed.
-
-.Request
-----
-  DELETE /groups/MyProject-Committers HTTP/1.0
-----
-
-.Response
-----
-  HTTP/1.1 204 No Content
-----
-
-[[delete-internal-group]]
-=== Delete Internal Group
---
-'POST /groups/link:#group-id[\{group-id\}].delete'
---
-
-Delete group.
-The deleted group must be internal group.
-
-.Request
-----
-  POST /groups/MyProject-Committers.delete HTTP/1.0
-----
-
-.Response
-----
-  HTTP/1.1 204 No Content
-----
-
 
 [[ids]]
 == IDs
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 632c3fa..4a0a602 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -550,6 +550,10 @@
   }
 ----
 
+The response code is `201 Created`, unless `init_only` is set in the request
+input and an existing project is updated. In this case the response code is `200
+OK`.
+
 [[get-project-description]]
 === Get Project Description
 --
@@ -984,7 +988,7 @@
 ----
 
 [[create-config-change]]
-=== Create Config Change for review.
+=== Create Config Change for review
 --
 'PUT /projects/link:#project-name[\{project-name\}]/config:review'
 --
@@ -1337,7 +1341,7 @@
 ----
 
 [[create-change]]
-=== Create Change for review.
+=== Create Change for review
 
 This endpoint is functionally equivalent to
 link:rest-api-changes.html#create-change[create change in the change
@@ -1388,7 +1392,7 @@
 ----
 
 [[create-access-change]]
-=== Create Access Rights Change for review.
+=== Create Access Rights Change for review
 --
 'PUT /projects/link:rest-api-projects.html#project-name[\{project-name\}]/access:review
 --
@@ -1951,6 +1955,55 @@
 If some branches could not be deleted, the response is "`409 Conflict`" and the
 error message is contained in the response body.
 
+[[delete-changes]]
+=== Delete Changes
+--
+'POST /projects/link:#project-name[\{project-name\}]/changes:delete'
+--
+
+Delete one or more changes.
+
+The changes to be deleted must be provided in the request body as a
+link:#delete-changes-input[DeleteChangesInput] entity.
+
+.Request
+----
+  POST /projects/MyProject/changes:delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "changes": [
+      "changeId1",
+      "changeId2",
+      "changeId3",
+      "changeId4"
+    ]
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "FAILURE": [
+      "changeId1"
+    ],
+    "NOT_UNIQUE": [
+      "changeId2"
+    ].
+    "SUCCESS": [
+      "changeId3",
+      "changeId4"
+    ]
+  }
+----
+
+If some changes could not be deleted, the response will be "`200 OK`" with the details in the response body.
+
+
 [[get-content]]
 === Get Content
 --
@@ -3727,7 +3780,7 @@
 ----
 
 [[create-labels-change]]
-=== Create Labels Change for review.
+=== Create Labels Change for review
 --
 'POST /projects/link:#project-name[\{project-name\}]/labels:review'
 --
@@ -3849,6 +3902,7 @@
   {
       "name": "Code-Review",
       "description": "At least one maximum vote for the Code-Review label is required",
+      "project": "My-Project",
       "submittability_expression": "label:Code-Review=+2",
       "allow_override_in_child_projects": false
    }
@@ -3894,6 +3948,7 @@
   {
       "name": "Code-Review",
       "description": "At least one maximum vote for the Code-Review label is required",
+      "project": "My-Project",
       "submittability_expression": "label:Code-Review=+2",
       "allow_override_in_child_projects": false
    }
@@ -3925,6 +3980,7 @@
   {
       "name": "Code-Review",
       "description": "At least one maximum vote for the Code-Review label is required",
+      "project": "All-Projects",
       "submittability_expression": "label:Code-Review=+2",
       "allow_override_in_child_projects": false
    }
@@ -3962,6 +4018,7 @@
     {
       "name": "Code-Review",
       "description": "At least one maximum vote for the Code-Review label is required",
+      "project": "All-Projects",
       "submittability_expression": "label:Code-Review=+2",
       "allow_override_in_child_projects": false
     }
@@ -4037,7 +4094,7 @@
 ----
 
 [[create-submit-requirements-change]]
-=== Create Submit Requirements Change for review.
+=== Create Submit Requirements Change for review
 --
 'POST /projects/link:#project-name[\{project-name\}]/submit_requirements:review'
 --
@@ -4251,6 +4308,12 @@
 The base revision of the new branch. +
 If not set and `create_empty_commit` is `true` the branch is created with an empty initial commit. +
 If not set and `create_empty_commit` is `false` or unset `HEAD` will be used as base revision.
+|`source_ref`                |optional|
+The full name of the source ref where `revision` can be found. +
+Used when `revision` is not a ref name in order to check reachability from a
+specific ref. This ref should be visible to the caller. +
+If not set, then all visible refs under `refs/heads/` and `refs/tags/` are
+searched.
 |`create_empty_commit`|`false` if not set|
 Whether the branch should be created with an empty initial commit. +
 Cannot be used in combination with setting a `revision`. +
@@ -4626,6 +4689,18 @@
 deleted.
 |==========================
 
+[[delete-changes-input]]
+=== DeleteChangesInput
+The `DeleteChangesInput` entity contains information about changes that should
+be deleted.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name   |Description
+|`changes`    |
+A list of changes (Change-Id or Change-Number) that identify the changes that should be deleted.
+|==========================
+
 [[delete-tags-input]]
 === DeleteTagsInput
 The `DeleteTagsInput` entity contains information about tags that should
@@ -4708,8 +4783,9 @@
 The link:config-labels.html#label_name[name] of the label.
 |`description`   |optional|
 The description of the label.
-|`project_name`  ||
-The name of the project in which this label is defined.
+|`project_name`  |optional|
+The name of the project in which this label is defined. Not set for globally
+defined labels.
 |`function`      ||
 The link:config-labels.html#label_function[function] of the label (can be
 `MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`.
@@ -4759,7 +4835,8 @@
 The new description for the label.
 |`function`      |optional|
 The new link:config-labels.html#label_function[function] of the label (can be
-`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`.
+`NoOp`/`NoBlock` and `PatchSetLock`). By default `NoOp` when creating new
+labels.
 |`values`        |optional|
 The new link:config-labels.html#label_value[values] of the label as a map of
 label value to value description. The label values are formatted strings, e.g.
@@ -4841,6 +4918,7 @@
 |`update`        |optional|
 Map of label names to link:#label-definition-input[LabelDefinitionInput]
 entities that describe the updates that should be done for the labels.
+The given inputs only need to set the properties that are being changed.
 |=============================
 
 [[batch-submit-requirement-input]]
@@ -4860,8 +4938,9 @@
 List of link:#submit-requirement-input[SubmitRequirementInput] entities that
 describe submit requirements that should be created.
 |`update`        |optional|
-Map of label names to link:#submit-requirement-input[SubmitRequirementInput]
-entities that describe the updates that should be done for the submit requirements.
+Map of submit requirement names to link:#submit-requirement-input[SubmitRequirementInput]
+entities that describe the updates that should be done for the submit requirements. The
+given inputs must set all properties (including those that are not being changed).
 |=============================
 
 [[project-access-input]]
@@ -5008,6 +5087,16 @@
 |`reject_empty_commit`       |optional|
 Whether empty commits should be rejected when a change is merged
 (`TRUE`, `FALSE`, `INHERIT`).
+|`init_only`                 |optional|
+If set, only the project initialization is being (re-)done and the repository
+creation is skipped. The project initialization consists out of setting `HEAD`,
+creating the `project.config` file in `refs/meta/config` and creating initial
+branches with empty commits. This is useful to retry the project initialization
+after a create project request has failed and as a result the repository was
+created but the project initialization was not done. Note, that this does not
+override any existing project configuration. If a conflicting configuration
+already exists the request is rejected with `409 Conflict`. The default is
+`false`.
 |=========================================
 
 [[project-parent-input]]
@@ -5067,6 +5156,9 @@
 The submit requirement name.
 |`description`|optional|
 Description of the submit requirement.
+|`project_name`  |optional|
+The name of the project in which this submit requirement is defined. Not set
+for globally defined submit requirements.
 |`applicability_expression`|optional|
 Query expression that can be evaluated on any change. If evaluated to true on a
 change, the submit requirement is then applicable for this change.
@@ -5169,6 +5261,8 @@
 specified, the project's `HEAD` will be used.
 |`message`   |optional|The tag message. When set, the tag will be created
 as an annotated tag.
+|`date`      |optional|Creation timestamp for annotated tags. Must not be
+a date in the future.
 |=========================
 
 GERRIT
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index a907e28..287ccb2 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -33,10 +33,10 @@
 results to correspond to what anonymous users can read (which may
 be nothing at all).
 
-Users (and programs) can authenticate with HTTP passwords by prefixing
+Users (and programs) can authenticate with authentication tokens by prefixing
 the endpoint URL with `/a/`. For example to authenticate to
 `/projects/`, request the URL `/a/projects/`. Gerrit will use HTTP basic
-authentication with the HTTP password from the user's account settings
+authentication with the tokens from the user's account settings
 page. This form of authentication bypasses the need for XSRF tokens.
 
 An authorization cookie may be presented in the request URL inside the
diff --git a/Documentation/user-request-cancellation-and-deadlines.txt b/Documentation/user-request-cancellation-and-deadlines.txt
index 7c39f16..487bc13 100644
--- a/Documentation/user-request-cancellation-and-deadlines.txt
+++ b/Documentation/user-request-cancellation-and-deadlines.txt
@@ -36,7 +36,7 @@
 request with a higher deadline or no deadline (deadline = 0) to get unblocked.
 
 Server-side deadlines are meant to protect the Gerrit service against resource
-exhaustion due to performence issues with a particular request. E.g. imagine a
+exhaustion due to performance issues with a particular request. E.g. imagine a
 situation where requests for a certain REST endpoint are very slow. If more and
 more of such requests get stuck and are not being aborted, the Gerrit service
 may run out of threads, causing an outage for the entire Gerrit service.
@@ -48,7 +48,7 @@
 the execution of the request, than letting them wait indefinitely.
 
 Finally server-side deadlines can help ops engineers to detect performance
-issues more reliably and more quicky. For this alerts may be setup that are
+issues more reliably and more quickly. For this alerts may be setup that are
 based on the link:metrics.html#cancellations[cancellation metrics].
 
 [[receive-timeout]]
@@ -88,7 +88,7 @@
 deadline.
 
 [NOTE]
-It is stronly discouraged for clients to permanently override
+It is strongly discouraged for clients to permanently override
 link:#server-side-deadlines[server-side deadlines] with a higher deadline or to
 permanently disable them by always setting the deadline to `0`. If this becomes
 necessary the caller should get in touch with the Gerrit administrators to
@@ -103,7 +103,7 @@
 == FAQs
 
 [[deadline-exceeded-what-to-do]]
-=== My request failed due to an execeeded deadline, what can I do?
+=== My request failed due to an exceeded deadline, what can I do?
 
 To get unblocked, you may repeat the request with deadlines disabled. To do this
 set the deadline to `0` on the request as explained
@@ -152,7 +152,7 @@
 |=======================
 |Request Type   |Cancellation Reason|Response
 |REST over HTTP |Client Disconnected|The response is '499 Client Closed Request'.
-|               |Server-side deadline exceeded|The response is '408 Server Deadline Exceeded'.
+|               |Server-side deadline exceeded|The response is '500 Internal Server Error'.
 |               |Client-provided deadline exceeded|The response is '408 Client Provided Deadline Exceeded'.
 |SSH command    |Client Disconnected|The error message is 'Client Closed Request'.
 |               |Server-side deadline exceeded|The error message is 'Server Deadline Exceeded'.
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 4d9455a..ec2f0d2 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -724,7 +724,7 @@
 Clicking on the `Reply` button opens an editor to type the reply.
 
 Previous comment can be quoted using "Quote" button. A new draft would be open
-on the same comment thread with the text of the previoused comment quoted using
+on the same comment thread with the text of the previous comment quoted using
 markdown syntax.
 
 image::images/user-review-ui-side-by-side-diff-screen-inline-comments.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-inline-comments.png"]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 7575564..e95d823 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -453,12 +453,16 @@
 current patch set. 'FOOTER' can be specified verbatim ('<key>: <value>', must
 be quoted) or as '<key>=<value>'. The matching is done case-insensitive.
 
+Note, the matching doesn't work for the footer keys that contain underscores.
+
 [[hasfooter-operator]]
 hasfooter:'FOOTERNAME'::
 +
 Matches any change that has a commit message with a footer where the footer
 name is equal to 'FOOTERNAME'.The matching is done case-sensitive.
 
+Note, the matching doesn't work for the footer keys that contain underscores.
+
 [[has]]
 has:draft::
 +
@@ -594,6 +598,21 @@
 +
 True if the change is a pure revert.
 
+[[consistent]]
+is:consistent::
++
+True if the change returned by the index is aligned with its underlying Git repository
+and therefore is fully consistent. Can be used to exclude changes that cannot be fully read
+and parsed from the Git repository (e.g.: because of incomplete or broken NoteDB data on the
+`/meta` ref) and therefore would otherwise be dropped during result formatting.
+This allows queries to return the maximum number of valid changes even when some indexed
+entries are corrupt.
++
+The introduction of the 'is:consistent' predicate has a measurable performance cost, especially
+on cold caches. In a benchmark with a query returning 500 changes (250 of which were inconsistent),
+the filter roughly tripled the latency on cold caches, while it increased latency by about 20% on
+warm caches.
+
 [[status]]
 status:open, status:pending, status:new::
 +
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 7cc9e29..415d27c 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -33,20 +33,20 @@
 
 When gitBasicAuthPolicy is set to `LDAP` or `HTTP_LDAP` and the user
 is authenticating with the LDAP username/password, the Git client config
-needs to have `http.cookieFile` set to a local file, otherwise every
-single call would trigger a full LDAP authentication and groups resolution
-which could introduce a noticeable latency on the overall execution
+needs to have `http.cookieFile` set to the local file and `http.saveCookies` set
+to `true`, so that HTTP session cookies can be read and saved. Otherwise
+every single call would trigger a full LDAP authentication and groups
+resolution which could introduce a noticeable latency on the overall execution
 and produce unwanted load to the LDAP server.
 
 When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can
-be regenerated by going to `Settings`, and then accessing the `HTTP
-Password` tab. Revocation can effectively be done by regenerating the
-password and then forgetting it.
+be managed by going to `Settings`, and then navigating to the `HTTP
+Credentials` section.
 
 For Gerrit installations where an link:config-gerrit.html#auth.httpPasswordUrl[HTTP password URL]
 is configured, the password can be obtained by clicking on `Obtain Password`
 and then following the site-specific instructions.  On sites where this URL is
-not configured, the password can be obtained by clicking on `Generate Password`.
+not configured, a token can be obtained by clicking on `Generate Password`.
 
 [[ssh]]
 == SSH
@@ -219,6 +219,20 @@
 
 Additional options may be specified when pushing changes.
 
+[[custom_keyed_values]]
+==== Custom Keyed Values
+
+Uploaders can specify custom keyed values for the change using this push option.
+The value can be set more than once to apply multiple custom keyed values.
+Each key,value pair must be separate by a colon (':') otherwise the push fails.
+
+----
+  git push ssh://bot@git.example.com:29418/kernel/common HEAD:refs/for/master -o custom-keyed-value=foo:bar custom-keyed-value=hello:world
+----
+
+Note: Setting the `custom-keyed-value` option in the refname
+(`%custom-keyed-values=foo:bar`) is **not** supported.
+
 [[notify]]
 ==== Email Notifications
 
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 0000000..3103f18
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,93 @@
+# Gemini Project Profile: gerrit
+
+This document provides a summary of the development environment for the gerrit project.
+
+## Overview
+
+Gerrit is a web-based code review tool, which integrates with Git and allows developers to review, approve, and merge code changes.
+
+- **Primary Language (Backend)**: Java
+- **Primary Language (Frontend)**: TypeScript (in `polygerrit-ui`)
+- **Build Tool**: Bazel
+- **Package Manager**: `yarn` (for frontend dependencies)
+
+## Sub-projects
+
+This repository contains multiple sub-projects. For more detailed information, please refer to the `GEMINI.md` file within each sub-project directory.
+
+- **`polygerrit-ui`**: The frontend web application. See `polygerrit-ui/GEMINI.md` for details on the frontend development environment.
+
+## Backend (Java)
+
+The core backend logic is written in Java and is located in the `java/` directory. The main package is `com.google.gerrit`. Key sub-packages include:
+
+- `acceptance`: Acceptance tests
+- `server`: Core server logic, including servlets, REST API endpoints, and change processing.
+- `git`: Git-related operations and management.
+- `sshd`: SSH server implementation for Git operations and administration.
+- `index`: Indexing and search functionality.
+- `auth`: Authentication and authorization logic.
+
+### Testing (Java)
+
+Java tests are located in the `javatests/` directory, mirroring the structure of the `java/` directory. Key sub-packages include:
+
+- `acceptance`: Acceptance tests
+- `integration`: Integration tests
+- `server`: Tests for the core server logic.
+- `git`: Tests for Git-related operations.
+
+### Running Tests
+
+You can run tests using `bazel test`.
+
+**Run a specific test:**
+```bash
+bazel test //javatests/com/google/gerrit/acceptance/rest/project:ListLabelsIT
+```
+
+**Run a test suite:**
+```bash
+bazel test //javatests/com/google/gerrit/httpd:httpd_tests
+```
+```bash
+bazel test //javatests/com/google/gerrit/acceptance/server/change:server_change
+```
+
+**Run a specific test method:**
+This command runs a single test method and streams the output.
+```bash
+bazel test --test_output=streamed --test_filter=com.google.gerrit.server.fixes.fixCalculator.FixCalculatorVariousTest.intraline //javatests/com/google/gerrit/server:server_tests
+```
+
+**Run tests with a filter:**
+```bash
+bazel test //javatests/com/google/gerrit/acceptance/rest/change:rest_change_other --test_filter=SuggestReviewersIT
+```
+
+### Code Formatting (Java)
+
+The project uses `google-java-format` to format Java code, via the wrapper script `tools/gjf.sh`.
+
+#### Formatting modified Java files
+
+To format all modified Java files:
+```bash
+tools/gjf.sh run
+```
+
+## Documentation
+
+The `Documentation/` directory contains a wealth of information about Gerrit, including:
+
+- **User Guide**: `user-*.txt` files explain how to use Gerrit.
+- **Administrator Guide**: `config-*.txt` and `install-*.txt` files provide information for administrators.
+- **Developer Guide**: `dev-*.txt` files contain information for developers working on Gerrit itself.
+- **REST API**: `rest-api-*.txt` files document the REST API endpoints.
+- **Commands**: `cmd-*.txt` files document the available command-line tools.
+
+## Git
+
+Every commit message must contain a `Release-Notes:` footer. Small changes, fixes or refactorings can just have
+`Release-Notes: skip`. If the change is relevant for being called out in release notes, then append a short
+summary to the `Release-Notes:` footer.
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index 14437da..8a5b785 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -49,6 +49,16 @@
 your patch.  It is virtually impossible to remove a patch once it
 has been applied and pushed out.
 
+In order to contribute to Gerrit a Contributor License Agreement (CLA)
+must be completed before contributions are accepted, see
+https://gerrit-review.googlesource.com/Documentation/dev-cla.html.
+
+All submissions to Google Open Source projects need to follow Google’s
+Contributor License Agreement (CLA), in which contributors agree that
+their contribution is an original work of authorship. This doesn’t
+prohibit the use of coding assistance tools, but what’s submitted does
+need to be a contributor’s original creation.
+
 
 (3) Sending your patches.
 
diff --git a/WORKSPACE b/WORKSPACE
index 63be743..594767e5 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -115,8 +115,19 @@
 
 load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
 
+NODE_20_REPO = {
+    "20.19.5-darwin_arm64": ("node-v20.19.5-darwin-arm64.tar.gz", "node-v20.19.5-darwin-arm64", "cfed7503d8d99fbcf2f52e408ec52f616058eb0867b34dbc3437259993ef5cba"),
+    "20.19.5-darwin_amd64": ("node-v20.19.5-darwin-x64.tar.gz", "node-v20.19.5-darwin-x64", "f9cff058f2766d4d0631dc69b5f7f27664b3a42ff186e25ac7e1ac269af7e696"),
+    "20.19.5-linux_arm64": ("node-v20.19.5-linux-arm64.tar.xz", "node-v20.19.5-linux-arm64", "d462267863ae8ee556039ebdf559055a8ec562c633889ef1403f3adb449ba1dd"),
+    "20.19.5-linux_ppc64le": ("node-v20.19.5-linux-ppc64le.tar.xz", "node-v20.19.5-linux-ppc64le", "ef98025e71d6d498476a95f144e353be074b24431b22eaa81bc64f921ea7d57f"),
+    "20.19.5-linux_s390x": ("node-v20.19.5-linux-s390x.tar.xz", "node-v20.19.5-linux-s390x", "a2e56c4b7fbffd0e6eef3a89e1c5945962fe85b4e2acfa59edc77a9238cc7901"),
+    "20.19.5-linux_amd64": ("node-v20.19.5-linux-x64.tar.xz", "node-v20.19.5-linux-x64", "315046739a513a70e03a4a55a8afda8cf979f30852e576075c340084e3f8ac0f"),
+    "20.19.5-windows_amd64": ("node-v20.19.5-win-x64.zip", "node-v20.19.5-win-x64", "c48159529572a5a947eef2d55d6485dfdc4ce8e67216402e2f6de52ad5d95695"),
+}
+
 node_repositories(
-    node_version = "20.14.0",
+    node_repositories = NODE_20_REPO,
+    node_version = "20.19.5",
     yarn_version = "1.22.19",
 )
 
diff --git a/contrib/git-push-review b/contrib/git-push-review
index 5a7f664..d00b3c3 100755
--- a/contrib/git-push-review
+++ b/contrib/git-push-review
@@ -13,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from __future__ import print_function
-
 import argparse
 import collections
 import os
@@ -24,20 +22,18 @@
 
 def get_config(name):
   args = ['git', 'config', '--get', name]
-  p = subprocess.Popen(args, stdout=subprocess.PIPE)
-  out, _ = p.communicate()
-  ret = p.poll()
-  if ret not in (0, 1):
-    raise subprocess.CalledProcessError(ret, ' '.join(args), output=out)
-  return out.strip()
+  result = subprocess.run(
+      args, capture_output=True, text=True, check=False)
+  if result.returncode not in (0, 1):
+    raise subprocess.CalledProcessError(
+        result.returncode, ' '.join(args), output=result.stdout)
+  return result.stdout.strip()
 
 
 def deref(name):
-  p = subprocess.Popen(
+  return subprocess.run(
       ['git', 'rev-parse', '--symbolic-full-name', name],
-      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-  out, _ = p.communicate()
-  return out.strip()
+      capture_output=True, text=True, check=True).stdout.strip()
 
 
 def main(argv):
@@ -94,14 +90,14 @@
       if v == True:
         opts_strs.append(k)
       elif v != False:
-        opts_strs.append('%s=%s' % (k, v))
+        opts_strs.append(f'{k}={v}')
 
   opts_str = ','.join(opts_strs)
   if opts_str:
     opts_str = '%' + opts_str
 
   git_args = ['git', 'push', args.remote,
-              'HEAD:refs/for/%s%s' % (args.branch, opts_str)]
+              f'HEAD:refs/for/{args.branch}{opts_str}']
   if args.dry_run:
     print(' '.join(git_args))
     return 0
diff --git a/contrib/maintenance/git/gc.py b/contrib/maintenance/git/gc.py
index 7a4c087..fd671e2 100644
--- a/contrib/maintenance/git/gc.py
+++ b/contrib/maintenance/git/gc.py
@@ -18,6 +18,7 @@
 
 from datetime import datetime, timedelta
 from glob import glob
+from pathlib import Path
 
 from .config import GitConfigReader
 from . import repo
@@ -124,13 +125,31 @@
 class DeleteEmptyRefDirsCleanupStep(GCStep):
     def run(self, repo_dir):
         refs_path = os.path.join(repo_dir, "refs")
-        for dir in glob(os.path.join(refs_path, "*/*")):
+        self.to_delete = {}
+        for dir, dirnames, filenames in os.walk(refs_path, topdown=False):
+            relative = os.path.relpath(dir, refs_path)
+            depth = len(relative.split(os.sep))
             if (
-                os.path.isdir(dir)
-                and len(os.listdir(dir)) == 0
+                not self.listdir(dir)
+                and depth >= 2
                 and Util.is_file_stale(dir, MAX_AGE_EMPTY_REF_DIRS)
             ):
-                os.removedirs(dir)
+                LOG.info("Queuing empty ref directory for deletion: %s", dir)
+                self.to_delete[dir] = None
+
+        for d in self.to_delete:
+            LOG.info("Deleting %s", d)
+            self.rmdir(d)
+
+    def listdir(self, dir):
+        children = (str(e) for e in Path(dir).iterdir())
+        return set(children) - self.to_delete.keys()
+
+    def rmdir(self, dir):
+        try:
+            os.rmdir(dir)
+        except (FileNotFoundError, OSError) as e:
+            LOG.warning("Couldn't delete %s: %s", dir, e)
 
 
 class DeleteStaleIncomingPacksCleanupStep(GCStep):
diff --git a/contrib/maintenance/tests/git/test_gc.py b/contrib/maintenance/tests/git/test_gc.py
index a7d831f..83c4fda 100644
--- a/contrib/maintenance/tests/git/test_gc.py
+++ b/contrib/maintenance/tests/git/test_gc.py
@@ -20,6 +20,9 @@
 from datetime import datetime, timedelta
 from pathlib import Path
 from git.gc import (
+    MAX_AGE_EMPTY_REF_DIRS,
+    MAX_AGE_INCOMING_PACKS,
+    MAX_AGE_GC_LOCK,
     DeleteStaleIncomingPacksCleanupStep,
     DeleteEmptyRefDirsCleanupStep,
     GCLockHandlingInitStep,
@@ -29,6 +32,10 @@
 )
 from git.config import GitConfigWriter
 
+DOUBLE_MAX_AGE_EMPTY_REF_DIRS = 2 * MAX_AGE_EMPTY_REF_DIRS
+DOUBLE_MAX_AGE_INCOMING_PACKS = 2 * MAX_AGE_INCOMING_PACKS
+DOUBLE_MAX_AGE_GC_LOCK = 2 * MAX_AGE_GC_LOCK
+
 
 def test_GCLockHandlingInitStep(repo):
     lock_file = os.path.join(repo, "gc.pid")
@@ -40,7 +47,7 @@
     task.run(repo)
     assert os.path.exists(lock_file)
 
-    _mofify_last_modified(lock_file, timedelta(hours=13))
+    _modify_last_modified(lock_file, DOUBLE_MAX_AGE_GC_LOCK)
 
     task.run(repo)
     assert not os.path.exists(lock_file)
@@ -97,6 +104,8 @@
 def test_DeleteEmptyRefDirsCleanupStep(repo):
     delete_path = os.path.join(repo, "refs", "heads", "delete")
     os.makedirs(delete_path)
+    delete_change_path = os.path.join(repo, "refs", "changes", "01", "101", "meta")
+    os.makedirs(delete_change_path)
     keep_path = os.path.join(repo, "refs", "heads", "keep")
     os.makedirs(keep_path)
     Path(os.path.join(keep_path, "abcd1234")).touch()
@@ -107,9 +116,35 @@
     assert os.path.exists(delete_path)
     assert os.path.exists(keep_path)
 
-    _mofify_last_modified(delete_path, timedelta(hours=2))
+    _modify_last_modified(keep_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+    _modify_last_modified(delete_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+    _modify_last_modified(delete_change_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+    delete_change_path_parent = Path(delete_change_path).parent
+    _modify_last_modified(delete_change_path_parent, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
     task.run(repo)
     assert not os.path.exists(delete_path)
+    assert not os.path.exists(delete_change_path)
+    assert not os.path.exists(delete_change_path_parent)
+
+
+def test_DeleteEmptyRefDirsCleanupStep_keeps_ref_dir(repo):
+    refs_path = os.path.join(repo, "refs")
+    heads_path = os.path.join(refs_path, "heads")
+    tags_path = os.path.join(refs_path, "tags")
+    delete_change_path = os.path.join(refs_path, "changes", "01", "101", "meta")
+    os.makedirs(delete_change_path)
+    _modify_last_modified(refs_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+    _modify_last_modified(heads_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+    _modify_last_modified(tags_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+    _modify_last_modified(delete_change_path, DOUBLE_MAX_AGE_EMPTY_REF_DIRS)
+
+    task = DeleteEmptyRefDirsCleanupStep()
+    task.run(repo)
+
+    assert not os.path.exists(delete_change_path)
+    assert os.path.exists(refs_path)
+    assert os.path.exists(heads_path)
+    assert os.path.exists(tags_path)
 
 
 def test_DeleteStaleIncomingPacksCleanupStep(repo):
@@ -132,9 +167,9 @@
     assert os.path.exists(object_file)
     assert os.path.exists(incoming_pack_file)
 
-    _mofify_last_modified(pack_file, timedelta(days=2))
-    _mofify_last_modified(object_file, timedelta(days=2))
-    _mofify_last_modified(incoming_pack_file, timedelta(days=2))
+    _modify_last_modified(pack_file, DOUBLE_MAX_AGE_INCOMING_PACKS)
+    _modify_last_modified(object_file, DOUBLE_MAX_AGE_INCOMING_PACKS)
+    _modify_last_modified(incoming_pack_file, DOUBLE_MAX_AGE_INCOMING_PACKS)
 
     task.run(repo)
 
@@ -185,7 +220,7 @@
     assert mock_subproc_run.call_count == 1
 
 
-def _mofify_last_modified(file, time_delta):
+def _modify_last_modified(file, time_delta):
     file_stat = os.stat(file)
     new_mod_timestamp = datetime.timestamp(
         datetime.fromtimestamp(file_stat.st_mtime) - time_delta
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 975567f..a6e659b 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -409,6 +409,7 @@
             RefNames.REFS_GROUPS + "*",
             RefNames.REFS_STARRED_CHANGES + "*",
             RefNames.REFS_DRAFT_COMMENTS + "*")
+        .deleteNewProjects()
         .build();
   }
 
@@ -1046,12 +1047,7 @@
   }
 
   protected void setRequireChangeId(InheritableBoolean value) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = projectConfigFactory.read(md);
-      config.updateProject(p -> p.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value));
-      config.commit(md);
-      projectCache.evictAndReindex(config.getProject());
-    }
+    projectOperations.project(project).forUpdate().requireChangeId(value).update();
   }
 
   protected void setSkipAddingAuthorAndCommitterAsReviewers(InheritableBoolean value)
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index 3dced64..1d3cfa1 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -220,8 +220,8 @@
 
   protected void getChangesByCommitMessageWithPluginDefinedBulkAttribute(
       BulkPluginInfoGetter getter) throws Exception {
-    Change.Id changeWithNoInfo = changeOperations.newChange().commitMessage("no-info").create();
-    Change.Id changeWithInfo = changeOperations.newChange().commitMessage("info").create();
+    Change.Id changeWithNoInfo = changeOperations.newChange().commitMessage("no-info").createV1();
+    Change.Id changeWithInfo = changeOperations.newChange().commitMessage("info").createV1();
 
     Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
     assertThat(pluginInfos.get(changeWithNoInfo)).isNull();
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginLogFileTest.java b/java/com/google/gerrit/acceptance/AbstractPluginLogFileTest.java
index f22ddea..8ce8c16 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginLogFileTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginLogFileTest.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.PluginLogFile;
 import com.google.gerrit.server.util.SystemLog;
@@ -85,7 +86,7 @@
 
     @Inject
     public MySystemLog(SitePaths site, Config config, InvocationCounter invocationCounter) {
-      super(site, config);
+      super(site, config, new LogConfig(config));
       this.invocationCounter = invocationCounter;
     }
 
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index 8654f82..c93c5ad 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -127,6 +127,17 @@
     return account;
   }
 
+  public synchronized void delete(String username) throws IOException, ConfigInvalidException {
+    accountsUpdateProvider.get().delete("Delete test account", accounts.get(username).id());
+  }
+
+  public synchronized TestAccount recreate(String username) throws Exception {
+    TestAccount account = accounts.get(username);
+    delete(account.username());
+    evict(account.id());
+    return create(account.username(), account.email(), account.fullName(), account.displayName());
+  }
+
   protected void addUserToGroups(Account.Id id, String... groupNames) throws Exception {
     if (groupNames != null) {
       for (String n : groupNames) {
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 8b2160c..3fc33e3 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -26,6 +26,7 @@
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/server",
     "//java/com/google/gerrit/server/audit",
+    "//java/com/google/gerrit/server/flow",
     "//java/com/google/gerrit/server/git/receive",
     "//java/com/google/gerrit/server/logging",
     "//java/com/google/gerrit/server/restapi",
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index f094b48..a6a4705 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance;
 
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
@@ -34,6 +35,7 @@
 import com.google.gerrit.extensions.events.RevisionCreatedListener;
 import com.google.gerrit.extensions.events.TopicEditedListener;
 import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
@@ -52,7 +54,9 @@
 import com.google.gerrit.server.change.FilterIncludedIn;
 import com.google.gerrit.server.change.ReviewerSuggestion;
 import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.flow.FlowService;
 import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.gerrit.server.git.receive.PushOptionsValidator;
 import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
@@ -83,6 +87,7 @@
   private final DynamicSet<ExceptionHook> exceptionHooks;
   private final DynamicSet<PerformanceLogger> performanceLoggers;
   private final DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
+  private final DynamicSet<LabelType> labelTypes;
   private final DynamicSet<SubmitRule> submitRules;
   private final DynamicSet<SubmitRequirement> submitRequirements;
   private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
@@ -118,12 +123,15 @@
   private final DynamicSet<ValidationOptionsListener> validationOptionsListeners;
   private final DynamicSet<CommitValidationInfoListener> commitValidationInfoListeners;
   private final DynamicSet<RetryListener> retryListeners;
+  private final DynamicSet<PushOptionsValidator> pushOptionsValidators;
 
   private final DynamicMap<ChangeHasOperandFactory> hasOperands;
   private final DynamicMap<ChangeIsOperandFactory> isOperands;
   private final DynamicMap<UserInOperandFactory> userInOperands;
   private final DynamicMap<ReviewerSuggestion> reviewerSuggestions;
 
+  private final DynamicItem<FlowService> flowService;
+
   @Inject
   ExtensionRegistry(
       DynamicSet<AccountIndexedListener> accountIndexedListeners,
@@ -135,6 +143,7 @@
       DynamicSet<ExceptionHook> exceptionHooks,
       DynamicSet<PerformanceLogger> performanceLoggers,
       DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
+      DynamicSet<LabelType> labelTypes,
       DynamicSet<SubmitRule> submitRules,
       DynamicSet<SubmitRequirement> submitRequirements,
       DynamicSet<ChangeMessageModifier> changeMessageModifiers,
@@ -172,7 +181,9 @@
       DynamicSet<ValidationOptionsListener> validationOptionsListeners,
       DynamicSet<CommitValidationInfoListener> commitValidationInfoListeners,
       DynamicSet<RetryListener> retryListeners,
-      DynamicMap<ReviewerSuggestion> reviewerSuggestions) {
+      DynamicSet<PushOptionsValidator> pushOptionsValidator,
+      DynamicMap<ReviewerSuggestion> reviewerSuggestions,
+      DynamicItem<FlowService> flowService) {
     this.accountIndexedListeners = accountIndexedListeners;
     this.changeIndexedListeners = changeIndexedListeners;
     this.groupIndexedListeners = groupIndexedListeners;
@@ -182,6 +193,7 @@
     this.exceptionHooks = exceptionHooks;
     this.performanceLoggers = performanceLoggers;
     this.projectCreationValidationListeners = projectCreationValidationListeners;
+    this.labelTypes = labelTypes;
     this.submitRules = submitRules;
     this.submitRequirements = submitRequirements;
     this.changeMessageModifiers = changeMessageModifiers;
@@ -219,7 +231,9 @@
     this.validationOptionsListeners = validationOptionsListeners;
     this.commitValidationInfoListeners = commitValidationInfoListeners;
     this.retryListeners = retryListeners;
+    this.pushOptionsValidators = pushOptionsValidator;
     this.reviewerSuggestions = reviewerSuggestions;
+    this.flowService = flowService;
   }
 
   public Registration newRegistration() {
@@ -276,6 +290,11 @@
     }
 
     @CanIgnoreReturnValue
+    public Registration add(LabelType labelType) {
+      return add(labelTypes, labelType);
+    }
+
+    @CanIgnoreReturnValue
     public Registration add(SubmitRule submitRule) {
       return add(submitRules, submitRule);
     }
@@ -432,6 +451,11 @@
     }
 
     @CanIgnoreReturnValue
+    public Registration add(PushOptionsValidator pushOptionsValidator) {
+      return add(pushOptionsValidators, pushOptionsValidator);
+    }
+
+    @CanIgnoreReturnValue
     public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
       return add(capabilityDefinitions, capabilityDefinition, exportName);
     }
@@ -472,6 +496,11 @@
       return add(reviewerSuggestions, reviewerSuggestion, exportName);
     }
 
+    @CanIgnoreReturnValue
+    public Registration set(FlowService flowService) {
+      return set(ExtensionRegistry.this.flowService, flowService);
+    }
+
     private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
       return add(dynamicSet, extension, "gerrit");
     }
@@ -490,6 +519,12 @@
       return this;
     }
 
+    private <T> Registration set(DynamicItem<T> dynamicItem, T extension) {
+      RegistrationHandle registrationHandle = dynamicItem.set(extension, "gerrit");
+      registrationHandles.add(registrationHandle);
+      return this;
+    }
+
     @Override
     public void close() {
       registrationHandles.forEach(h -> h.remove());
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index ce3a890..a8b01f9 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -43,7 +43,6 @@
 import com.google.gerrit.acceptance.testsuite.change.PerCommentOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.change.PerDraftCommentOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.change.PerPatchsetOperationsImpl;
-import com.google.gerrit.acceptance.testsuite.change.PerRobotCommentOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -636,7 +635,6 @@
             factory(PerPatchsetOperationsImpl.Factory.class);
             factory(PerCommentOperationsImpl.Factory.class);
             factory(PerDraftCommentOperationsImpl.Factory.class);
-            factory(PerRobotCommentOperationsImpl.Factory.class);
             install(new PushOneCommit.Module());
             install(InProcessProtocol.module());
             install(new NoSshModule());
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index c71641b..0da9d30 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.RefPatternMatcher;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Arrays;
@@ -49,6 +50,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Stream;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -134,28 +136,35 @@
           groupIncludeCache,
           groupIndexer,
           projectCache,
-          input.refsByProject);
+          input.refsByProject,
+          input.deleteNewProjects);
     }
   }
 
   public static class Config {
     private final ImmutableMultimap<Project.NameKey, String> refsByProject;
+    private final boolean deleteNewProjects;
 
-    private Config(ImmutableMultimap<Project.NameKey, String> refsByProject) {
+    private Config(
+        ImmutableMultimap<Project.NameKey, String> refsByProject, boolean deleteNewProjects) {
       this.refsByProject = refsByProject;
+      this.deleteNewProjects = deleteNewProjects;
     }
 
     public Builder toBuilder() {
       Builder builder = new Builder();
       builder.refsByProject.putAll(refsByProject);
+      builder.deleteNewProjects.set(deleteNewProjects);
       return builder;
     }
 
     public static class Builder {
       private final ListMultimap<Project.NameKey, String> refsByProject;
+      private final AtomicBoolean deleteNewProjects;
 
       public Builder() {
         this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
+        this.deleteNewProjects = new AtomicBoolean(false);
       }
 
       public Builder reset(Project.NameKey project, String... refPatterns) {
@@ -167,26 +176,33 @@
         return this;
       }
 
+      public Builder deleteNewProjects() {
+        deleteNewProjects.set(true);
+        return this;
+      }
+
       public Config build() {
-        return new Config(ImmutableMultimap.copyOf(refsByProject));
+        return new Config(ImmutableMultimap.copyOf(refsByProject), deleteNewProjects.get());
       }
     }
   }
 
-  @Inject private GitRepositoryManager repoManager;
-  @Inject private AllUsersName allUsersName;
-  @Inject @Nullable private AccountCreator accountCreator;
-  @Inject @Nullable private AccountCache accountCache;
-  @Inject @Nullable private GroupCache groupCache;
-  @Inject @Nullable private GroupIncludeCache groupIncludeCache;
-  @Inject @Nullable private GroupIndexer groupIndexer;
-  @Inject @Nullable private AccountIndexer accountIndexer;
-  @Inject @Nullable private ProjectCache projectCache;
+  private GitRepositoryManager repoManager;
+  private AllUsersName allUsersName;
+  @Nullable private AccountCreator accountCreator;
+  @Nullable private AccountCache accountCache;
+  @Nullable private GroupCache groupCache;
+  @Nullable private GroupIncludeCache groupIncludeCache;
+  @Nullable private GroupIndexer groupIndexer;
+  @Nullable private AccountIndexer accountIndexer;
+  @Nullable private ProjectCache projectCache;
 
   private final Multimap<Project.NameKey, String> refsPatternByProject;
+  private final boolean deleteNewProjects;
 
   // State to which to reset to.
   private final ListMultimap<Project.NameKey, RefState> savedRefStatesByProject;
+  private final Set<Project.NameKey> savedProjectSet;
 
   // Results of the resetting
   private ListMultimap<Project.NameKey, String> keptRefsByProject;
@@ -203,7 +219,8 @@
       @Nullable GroupIncludeCache groupIncludeCache,
       @Nullable GroupIndexer groupIndexer,
       @Nullable ProjectCache projectCache,
-      Multimap<Project.NameKey, String> refPatternByProject)
+      Multimap<Project.NameKey, String> refPatternByProject,
+      boolean deleteNewProjects)
       throws IOException {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
@@ -215,7 +232,10 @@
     this.groupIncludeCache = groupIncludeCache;
     this.projectCache = projectCache;
     this.refsPatternByProject = refPatternByProject;
+    this.deleteNewProjects = deleteNewProjects;
     this.savedRefStatesByProject = readRefStates();
+    this.savedProjectSet =
+        repoManager instanceof InMemoryRepositoryManager ? repoManager.list() : projectCache.all();
   }
 
   @Override
@@ -227,6 +247,7 @@
         () -> {
           restoreRefs();
           deleteNewlyCreatedRefs();
+          deleteNewlyCreatedProjects();
           evictCachesAndReindex();
         });
   }
@@ -312,6 +333,18 @@
     }
   }
 
+  private void deleteNewlyCreatedProjects() {
+    if (deleteNewProjects && repoManager instanceof InMemoryRepositoryManager) {
+      InMemoryRepositoryManager inMemoryRepoManager = (InMemoryRepositoryManager) repoManager;
+      Sets.difference(inMemoryRepoManager.list(), savedProjectSet)
+          .forEach(
+              project -> {
+                inMemoryRepoManager.deleteRepository(project);
+                projectCache.evictAndReindex(project);
+              });
+    }
+  }
+
   private void evictCachesAndReindex() throws IOException {
     evictAndReindexProjects();
     evictAndReindexAccounts();
diff --git a/java/com/google/gerrit/acceptance/TestExtensions.java b/java/com/google/gerrit/acceptance/TestExtensions.java
index e5d9b26..97fcf0e 100644
--- a/java/com/google/gerrit/acceptance/TestExtensions.java
+++ b/java/com/google/gerrit/acceptance/TestExtensions.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.DIRECT_PUSH;
 import static java.util.Objects.requireNonNull;
@@ -24,21 +25,43 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PluginPushOption;
 import com.google.gerrit.server.ValidationOptionsListener;
+import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowExpression;
+import com.google.gerrit.server.flow.FlowKey;
+import com.google.gerrit.server.flow.FlowPermissionDeniedException;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.gerrit.server.flow.FlowStage;
+import com.google.gerrit.server.flow.FlowStageEvaluationStatus;
+import com.google.gerrit.server.flow.InvalidFlowException;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationInfo;
 import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.update.RetryListener;
 import com.google.gerrit.server.update.context.RefUpdateContext;
+import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
 /**
  * Class to host common test extension implementations.
@@ -151,6 +174,184 @@
     }
   }
 
+  /** Test implementation of a {@link FlowService} to be used by the flow integration tests. */
+  public static class TestFlowService implements FlowService {
+    public static final String INVALID_CONDITION = "invalid";
+
+    private final Map<FlowKey, Flow> flows = new HashMap<>();
+
+    /**
+     * Whether any flow creation should be rejected with a {@link FlowPermissionDeniedException}.
+     */
+    private boolean rejectFlowCreation;
+
+    /**
+     * Whether any flow deletion should be rejected with a {@link FlowPermissionDeniedException}.
+     */
+    private boolean rejectFlowDeletion;
+
+    /** Makes the flow service reject all flow creations. */
+    public void rejectFlowCreation() {
+      this.rejectFlowCreation = true;
+    }
+
+    /** Makes the flow service reject all flow deletions. */
+    public void rejectFlowDeletion() {
+      this.rejectFlowDeletion = true;
+    }
+
+    @Override
+    public Flow createFlow(FlowCreation flowCreation)
+        throws FlowPermissionDeniedException, InvalidFlowException, StorageException {
+      if (rejectFlowCreation) {
+        throw new FlowPermissionDeniedException("not allowed to create flow");
+      }
+
+      if (flowCreation.stageExpressions().stream()
+          .map(FlowExpression::condition)
+          .anyMatch(condition -> condition.endsWith(INVALID_CONDITION))) {
+        throw new InvalidFlowException(String.format("invalid condition: %s", INVALID_CONDITION));
+      }
+
+      FlowKey flowKey =
+          FlowKey.builder()
+              .projectName(flowCreation.projectName())
+              .changeId(flowCreation.changeId())
+              .uuid(ChangeUtil.messageUuid())
+              .build();
+      Flow flow =
+          Flow.builder(flowKey)
+              .createdOn(Instant.now())
+              .ownerId(flowCreation.ownerId())
+              .stages(
+                  flowCreation.stageExpressions().stream()
+                      .map(
+                          stageExpression ->
+                              FlowStage.builder().expression(stageExpression).build())
+                      .collect(toImmutableList()))
+              .build();
+      flows.put(flowKey, flow);
+      return flow;
+    }
+
+    @Override
+    public Boolean isFlowsEnabled(Project.NameKey projectName, Change.Id changeId)
+        throws RestApiException {
+      // Always return true for testing purposes.
+      return true;
+    }
+
+    @Override
+    public Optional<Flow> getFlow(FlowKey flowKey) throws StorageException {
+      return Optional.ofNullable(flows.get(flowKey));
+    }
+
+    @Override
+    public Optional<Flow> deleteFlow(FlowKey flowKey)
+        throws FlowPermissionDeniedException, StorageException {
+      if (rejectFlowDeletion) {
+        throw new FlowPermissionDeniedException("not allowed to delete flow");
+      }
+
+      return Optional.ofNullable(flows.remove(flowKey));
+    }
+
+    @Override
+    public ImmutableList<Flow> listFlows(Project.NameKey projectName, Change.Id changeId)
+        throws StorageException {
+      return flows.entrySet().stream()
+          .filter(
+              e ->
+                  e.getKey().projectName().equals(projectName)
+                      && e.getKey().changeId().equals(changeId))
+          .map(Map.Entry::getValue)
+          .collect(toImmutableList());
+    }
+
+    /**
+     * Updates the specified flow.
+     *
+     * <p>Sets the {@code lastEvaluatedOn} timestamp in the flow and updates the statuses of the
+     * stages.
+     *
+     * @param flowKey the key of the flow that should be updated
+     * @param stageStates states to be set for the stages
+     * @param stageMessages messages to be set for the stages
+     * @throws IllegalStateException thrown if the specified flow is not found, or if the number of
+     *     given states/messages doesn't match with the number of stages in the flow
+     * @return the updated flow
+     */
+    public Flow evaluate(
+        FlowKey flowKey,
+        ImmutableList<FlowStageEvaluationStatus.State> stageStates,
+        ImmutableList<Optional<String>> stageMessages)
+        throws IllegalStateException {
+      Optional<Flow> flow = getFlow(flowKey);
+      if (flow.isEmpty()) {
+        throw new IllegalStateException(String.format("Flow %s not found.", flowKey));
+      }
+      if (stageStates.size() != flow.get().stages().size()) {
+        throw new IllegalStateException(
+            String.format(
+                "Invalid number of stage states: got %s, expected %s",
+                stageStates.size(), flow.get().stages().size()));
+      }
+      if (stageMessages.size() != flow.get().stages().size()) {
+        throw new IllegalStateException(
+            String.format(
+                "Invalid number of stage messages: got %s, expected %s",
+                stageMessages.size(), flow.get().stages().size()));
+      }
+
+      List<FlowStage> stages = new ArrayList<>(flow.get().stages());
+      for (int i = 0; i < flow.get().stages().size(); i++) {
+        FlowStageEvaluationStatus.Builder updatedStatus =
+            stages.get(i).status().toBuilder().state(stageStates.get(i));
+        if (stageMessages.get(i).isPresent()) {
+          updatedStatus.message(stageMessages.get(i).get());
+        }
+        FlowStage updatedStage = stages.get(i).toBuilder().status(updatedStatus.build()).build();
+        stages.set(i, updatedStage);
+      }
+
+      Flow updatedFlow =
+          flow.get().toBuilder()
+              .lastEvaluatedOn(Instant.now())
+              .stages(ImmutableList.copyOf(stages))
+              .build();
+      flows.put(flowKey, updatedFlow);
+      return updatedFlow;
+    }
+  }
+
+  /**
+   * Test submit rule that counts how often it is invoked.
+   *
+   * <p>Using this submit rule tests can verify that submit rules are not unexpectedly invoked.
+   * Checking this is important since executing submit rules is rather expensive.
+   */
+  public static class TestSubmitRule implements SubmitRule {
+    private int count;
+
+    public int count() {
+      return count;
+    }
+
+    @Override
+    public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+      if (!isAsyncCallForSendingReviewCommentsEmail()) {
+        count++;
+      }
+      return Optional.empty();
+    }
+
+    private boolean isAsyncCallForSendingReviewCommentsEmail() {
+      return Arrays.stream(Thread.currentThread().getStackTrace())
+          .map(StackTraceElement::getClassName)
+          .anyMatch(className -> EmailReviewComments.class.getName().equals(className));
+    }
+  }
+
   /**
    * Private constructor to prevent instantiation of this class.
    *
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
index c4e4192..3061dc2 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 
 /**
  * An aggregation of operations on changes for test purposes.
@@ -32,10 +33,21 @@
    * PerChangeOperations} for details on possible operations.
    *
    * @return an aggregation of operations on a specific change
+   * @deprecated use {@link #change(ChangeIdentifier)} instead
    */
+  // TODO: Drop this method once all callers have been migrated to change(ChangeIdentifier).
+  @Deprecated
   PerChangeOperations change(Change.Id changeId);
 
   /**
+   * Starts the fluent chain for querying or modifying a change. Please see the methods of {@link
+   * PerChangeOperations} for details on possible operations.
+   *
+   * @return an aggregation of operations on a specific change
+   */
+  PerChangeOperations change(ChangeIdentifier changeIdentifier);
+
+  /**
    * Starts the fluent chain to create a change. The returned builder can be used to specify the
    * attributes of the new change. To create the change for real, {@link
    * TestChangeCreation.Builder#create()} must be called.
@@ -47,10 +59,12 @@
    *     .newChange()
    *     .file("file1")
    *     .content("Line 1\nLine2\n")
-   *     .create();
+   *     .createV2();
    * </pre>
    *
-   * <p><strong>Note:</strong> There must be at least one existing user and repository.
+   * <p><strong>Note:</strong> There must be at least one existing user.
+   *
+   * <p><strong>Note:</strong> If a project is not specified the change is created in a new project.
    *
    * @return a builder to create the new change
    */
@@ -113,6 +127,27 @@
     PerPatchsetOperations currentPatchset();
 
     /**
+     * Starts the fluent chain to create a new vote. The returned builder can be used to specify the
+     * attributes of the new vote. To create the vote for real, {@link
+     * TestVoteCreation.Builder#create()} must be called.
+     *
+     * <p>Example:
+     *
+     * <pre>
+     * TestVote testVote = changeOperations
+     *     .change(changeId)
+     *     .newVote()
+     *     .user(user.id())
+     *     .label("Code-Review")
+     *     .value(2)
+     *     .create();
+     * </pre>
+     *
+     * @return builder to create a new vote
+     */
+    TestVoteCreation.Builder newVote();
+
+    /**
      * Starts the fluent chain for querying or modifying a published comment. Please see the methods
      * of {@link PerCommentOperations} for details on possible operations.
      *
@@ -127,13 +162,5 @@
      * @return an aggregation of operations on a specific draft comment
      */
     PerDraftCommentOperations draftComment(String commentUuid);
-
-    /**
-     * Starts the fluent chain for querying or modifying a robot comment. Please see the methods of
-     * {@link PerRobotCommentOperations} for details on possible operations.
-     *
-     * @return an aggregation of operations on a specific robot comment
-     */
-    PerRobotCommentOperations robotComment(String commentUuid);
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index a4f77a1..ad3dceb 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -16,23 +16,34 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelTypes;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeInserter;
@@ -42,7 +53,10 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
@@ -51,7 +65,6 @@
 import java.io.IOException;
 import java.time.Instant;
 import java.util.Arrays;
-import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -83,12 +96,13 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final PersonIdent serverIdent;
   private final BatchUpdate.Factory batchUpdateFactory;
-  private final ProjectCache projectCache;
   private final ChangeFinder changeFinder;
   private final PerPatchsetOperationsImpl.Factory perPatchsetOperationsFactory;
   private final PerCommentOperationsImpl.Factory perCommentOperationsFactory;
   private final PerDraftCommentOperationsImpl.Factory perDraftCommentOperationsFactory;
-  private final PerRobotCommentOperationsImpl.Factory perRobotCommentOperationsFactory;
+  private final ProjectOperations projectOperations;
+  private final ProjectCache projectCache;
+  private final AccountCache accountCache;
 
   @Inject
   public ChangeOperationsImpl(
@@ -100,12 +114,13 @@
       IdentifiedUser.GenericFactory userFactory,
       @GerritPersonIdent PersonIdent serverIdent,
       BatchUpdate.Factory batchUpdateFactory,
-      ProjectCache projectCache,
       ChangeFinder changeFinder,
       PerPatchsetOperationsImpl.Factory perPatchsetOperationsFactory,
       PerCommentOperationsImpl.Factory perCommentOperationsFactory,
       PerDraftCommentOperationsImpl.Factory perDraftCommentOperationsFactory,
-      PerRobotCommentOperationsImpl.Factory perRobotCommentOperationsFactory) {
+      ProjectOperations projectOperations,
+      ProjectCache projectCache,
+      AccountCache accountCache) {
     this.seq = seq;
     this.changeInserterFactory = changeInserterFactory;
     this.patchsetInserterFactory = patchsetInserterFactory;
@@ -114,22 +129,28 @@
     this.userFactory = userFactory;
     this.serverIdent = serverIdent;
     this.batchUpdateFactory = batchUpdateFactory;
-    this.projectCache = projectCache;
     this.changeFinder = changeFinder;
     this.perPatchsetOperationsFactory = perPatchsetOperationsFactory;
     this.perCommentOperationsFactory = perCommentOperationsFactory;
     this.perDraftCommentOperationsFactory = perDraftCommentOperationsFactory;
-    this.perRobotCommentOperationsFactory = perRobotCommentOperationsFactory;
+    this.projectOperations = projectOperations;
+    this.projectCache = projectCache;
+    this.accountCache = accountCache;
   }
 
   @Override
   public PerChangeOperations change(Change.Id changeId) {
-    return new PerChangeOperationsImpl(changeId);
+    return new PerChangeOperationsImpl(ChangeIdentifier.byNumericChangeId(changeId.get()));
+  }
+
+  @Override
+  public PerChangeOperations change(ChangeIdentifier changeIdentifier) {
+    return new PerChangeOperationsImpl(changeIdentifier);
   }
 
   @Override
   public TestChangeCreation.Builder newChange() {
-    return TestChangeCreation.builder(this::createChange);
+    return TestChangeCreation.builder(this::createChange, changeId -> this.change(changeId).get());
   }
 
   private Change.Id createChange(TestChangeCreation changeCreation) throws Exception {
@@ -140,10 +161,10 @@
       try (Repository repository = repositoryManager.openRepository(project);
           ObjectInserter objectInserter = repository.newObjectInserter();
           RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-        Instant now = TimeUtil.now();
+        Instant creationTimestamp = changeCreation.createdOn().orElse(TimeUtil.now());
         IdentifiedUser changeOwner = getChangeOwner(changeCreation);
-        PersonIdent author = getAuthorIdent(now, changeCreation);
-        PersonIdent committer = getCommitterIdent(now, changeCreation);
+        PersonIdent author = getAuthorIdent(creationTimestamp, changeCreation);
+        PersonIdent committer = getCommitterIdent(creationTimestamp, changeCreation);
         ObjectId commitId =
             createCommit(repository, revWalk, objectInserter, changeCreation, author, committer);
 
@@ -154,7 +175,8 @@
         inserter.setApprovals(changeCreation.approvals());
         inserter.setValidationOptions(changeCreation.validationOptions());
 
-        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
+        try (BatchUpdate batchUpdate =
+            batchUpdateFactory.create(project, changeOwner, creationTimestamp)) {
           batchUpdate.setRepository(repository, revWalk, objectInserter);
           batchUpdate.insertChange(inserter);
           batchUpdate.execute();
@@ -169,22 +191,7 @@
       return changeCreation.project().get();
     }
 
-    return getArbitraryProject();
-  }
-
-  private Project.NameKey getArbitraryProject() {
-    Project.NameKey allProjectsName = projectCache.getAllProjects().getNameKey();
-    Project.NameKey allUsersName = projectCache.getAllUsers().getNameKey();
-    Optional<Project.NameKey> arbitraryProject =
-        projectCache.all().stream()
-            .filter(
-                name ->
-                    !Objects.equals(name, allProjectsName) && !Objects.equals(name, allUsersName))
-            .findFirst();
-    checkState(
-        arbitraryProject.isPresent(),
-        "At least one repository must be available on the Gerrit server");
-    return arbitraryProject.get();
+    return projectOperations.newProject().create();
   }
 
   private IdentifiedUser getChangeOwner(TestChangeCreation changeCreation)
@@ -221,7 +228,15 @@
   }
 
   private IdentifiedUser getArbitraryUser() throws ConfigInvalidException, IOException {
-    ImmutableSet<Account.Id> foundAccounts = resolver.resolveIgnoreVisibility("").asIdSet();
+    return getArbitraryUser(Predicates.alwaysTrue());
+  }
+
+  private IdentifiedUser getArbitraryUser(Predicate<Account.Id> filter)
+      throws ConfigInvalidException, IOException {
+    ImmutableSet<Account.Id> foundAccounts =
+        resolver.resolveIgnoreVisibility("").asIdSet().stream()
+            .filter(filter)
+            .collect(toImmutableSet());
     checkState(
         !foundAccounts.isEmpty(),
         "At least one user account must be available on the Gerrit server");
@@ -262,6 +277,7 @@
     return switch (parentCommit.getKind()) {
       case BRANCH -> ImmutableList.of();
       case CHANGE_ID -> getGroupsFromChange(parentCommit.changeId());
+      case CHANGE_IDENTIFIER -> getGroupsFromChange(parentCommit.changeIdentifier());
       case COMMIT_SHA_1 -> ImmutableList.of();
       case PATCHSET_ID -> getGroupsFromPatchset(parentCommit.patchsetId());
       default ->
@@ -270,6 +286,24 @@
     };
   }
 
+  private ImmutableList<String> getGroupsFromChange(ChangeIdentifier changeIdentifier) {
+    Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeIdentifier.id());
+
+    if (changeNotes.isPresent() && changeNotes.get().getChange().isClosed()) {
+      return ImmutableList.of();
+    }
+
+    return changeNotes
+        .map(ChangeNotes::getCurrentPatchSet)
+        .map(PatchSet::groups)
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    String.format(
+                        "Change %s not found and hence can't be used as parent.",
+                        changeIdentifier)));
+  }
+
   private ImmutableList<String> getGroupsFromChange(Change.Id changeId) {
     Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId);
 
@@ -325,6 +359,7 @@
     return switch (parentCommit.getKind()) {
       case BRANCH -> resolveBranchTip(repository, parentCommit.branch());
       case CHANGE_ID -> resolveChange(parentCommit.changeId());
+      case CHANGE_IDENTIFIER -> resolveChange(parentCommit.changeIdentifier());
       case COMMIT_SHA_1 -> resolveCommitFromSha1(revWalk, parentCommit.commitSha1());
       case PATCHSET_ID -> resolvePatchset(parentCommit.patchsetId());
       default ->
@@ -352,6 +387,19 @@
     }
   }
 
+  private ObjectId resolveChange(ChangeIdentifier changeIdentifier) {
+    Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeIdentifier.id());
+    return changeNotes
+        .map(ChangeNotes::getCurrentPatchSet)
+        .map(PatchSet::commitId)
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    String.format(
+                        "Change %s not found and hence can't be used as parent.",
+                        changeIdentifier)));
+  }
+
   private ObjectId resolveChange(Change.Id changeId) {
     Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId);
     return changeNotes
@@ -471,21 +519,21 @@
 
   private ChangeInserter getChangeInserter(Change.Id changeId, String refName, ObjectId commitId) {
     ChangeInserter inserter = changeInserterFactory.create(changeId, commitId, refName);
-    inserter.setMessage(String.format("Uploaded patchset %d.", inserter.getPatchSetId().get()));
+    inserter.setMessage(String.format("Uploaded patch set %d.", inserter.getPatchSetId().get()));
     return inserter;
   }
 
   private class PerChangeOperationsImpl implements PerChangeOperations {
 
-    private final Change.Id changeId;
+    private final ChangeIdentifier changeIdentifier;
 
-    public PerChangeOperationsImpl(Change.Id changeId) {
-      this.changeId = changeId;
+    public PerChangeOperationsImpl(ChangeIdentifier changeIdentifier) {
+      this.changeIdentifier = changeIdentifier;
     }
 
     @Override
     public boolean exists() {
-      return changeFinder.findOne(changeId).isPresent();
+      return changeFinder.findOne(changeIdentifier.id()).isPresent();
     }
 
     @Override
@@ -494,7 +542,7 @@
     }
 
     private ChangeNotes getChangeNotes() {
-      Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId);
+      Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeIdentifier.id());
       checkState(changeNotes.isPresent(), "Tried to get non-existing test change.");
       return changeNotes.get();
     }
@@ -503,6 +551,12 @@
       return TestChange.builder()
           .numericChangeId(change.getId())
           .changeId(change.getKey().get())
+          .project(change.getProject())
+          .dest(change.getDest())
+          .subject(change.getSubject())
+          .owner(change.getOwner())
+          .createdOn(change.getCreatedOn())
+          .lastUpdatedOn(change.getLastUpdatedOn())
           .build();
     }
 
@@ -543,7 +597,7 @@
           IdentifiedUser uploader = userFactory.create(uploaderId);
           try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, uploader, now)) {
             batchUpdate.setRepository(repository, revWalk, objectInserter);
-            batchUpdate.addOp(changeId, patchSetInserter);
+            batchUpdate.addOp(changeNotes.getChangeId(), patchSetInserter);
             batchUpdate.execute();
           }
           return patchsetId;
@@ -667,7 +721,7 @@
       PatchSetInserter patchSetInserter =
           patchsetInserterFactory.create(changeNotes, patchsetId, newPatchsetCommit);
       patchSetInserter.setCheckAddPatchSetPermission(false);
-      patchSetInserter.setMessage(String.format("Uploaded patchset %d.", patchsetId.get()));
+      patchSetInserter.setMessage(String.format("Uploaded patch set %d.", patchsetId.get()));
       return patchSetInserter;
     }
 
@@ -684,6 +738,89 @@
     }
 
     @Override
+    public TestVoteCreation.Builder newVote() {
+      return TestVoteCreation.builder(this::createVote);
+    }
+
+    private TestVote createVote(TestVoteCreation voteCreation)
+        throws IOException, RestApiException, UpdateException, ConfigInvalidException {
+      ChangeNotes changeNotes = getChangeNotes();
+      Project.NameKey project = changeNotes.getProjectName();
+      IdentifiedUser voter = getVoter(changeNotes.getChange().getOwner(), voteCreation);
+      LabelType label = getLabel(changeNotes.getChange().getDest(), voteCreation);
+      short value = getLabelValue(label, voteCreation);
+      try (RefUpdateContext ctx = openTestRefUpdateContext();
+          Repository repository = repositoryManager.openRepository(project);
+          ObjectInserter objectInserter = repository.newObjectInserter();
+          RevWalk revWalk = new RevWalk(objectInserter.newReader());
+          BatchUpdate batchUpdate = batchUpdateFactory.create(project, voter, TimeUtil.now())) {
+        batchUpdate.setRepository(repository, revWalk, objectInserter);
+        batchUpdate.addOp(
+            changeNotes.getChangeId(),
+            new BatchUpdateOp() {
+              @Override
+              public boolean updateChange(ChangeContext ctx) throws Exception {
+                ctx.getUpdate(changeNotes.getCurrentPatchSet().id())
+                    .putApproval(label.getName(), value);
+                return true;
+              }
+            });
+        batchUpdate.execute();
+
+        return TestVote.builder()
+            .userId(voter.getAccountId())
+            .label(label.getName())
+            .value(value)
+            .build();
+      }
+    }
+
+    private IdentifiedUser getVoter(Account.Id changeOwnerId, TestVoteCreation voteCreation)
+        throws IOException, ConfigInvalidException {
+      if (voteCreation.user().isPresent()) {
+        checkState(
+            accountCache.get(voteCreation.user().get()).isPresent(),
+            "account %s not found",
+            voteCreation.user().get());
+        return userFactory.create(voteCreation.user().get());
+      }
+
+      return getArbitraryUser(accountId -> !accountId.equals(changeOwnerId));
+    }
+
+    private LabelType getLabel(BranchNameKey dest, TestVoteCreation voteCreation) {
+      ProjectState state =
+          projectCache.get(dest.project()).orElseThrow(illegalState(dest.project()));
+      LabelTypes labelTypes = state.getLabelTypes(dest);
+      if (voteCreation.label().isPresent()) {
+        Optional<LabelType> labelType = labelTypes.byLabel(voteCreation.label().get());
+        checkState(labelType.isPresent(), "label %s not found", voteCreation.label().get());
+        return labelType.get();
+      }
+
+      return labelTypes.getLabelTypes().stream()
+          .filter(labelType -> !LabelFunction.PATCH_SET_LOCK.equals(labelType.getFunction()))
+          .findAny()
+          .orElseThrow(() -> new IllegalStateException("At least one label must be available"));
+    }
+
+    private short getLabelValue(LabelType labelType, TestVoteCreation voteCreation) {
+      if (voteCreation.value().isPresent()) {
+        checkState(
+            labelType.getByValue().keySet().contains(voteCreation.value().get().shortValue()),
+            "value %s not found for label %s",
+            voteCreation.value().get(),
+            labelType.getName());
+        return voteCreation.value().get().shortValue();
+      }
+
+      return labelType.getByValue().keySet().stream()
+          .filter(value -> value != 0)
+          .findAny()
+          .orElseThrow(() -> new IllegalStateException("At least one value must be available"));
+    }
+
+    @Override
     public PerCommentOperations comment(String commentUuid) {
       ChangeNotes changeNotes = getChangeNotes();
       return perCommentOperationsFactory.create(changeNotes, commentUuid);
@@ -694,11 +831,5 @@
       ChangeNotes changeNotes = getChangeNotes();
       return perDraftCommentOperationsFactory.create(changeNotes, commentUuid);
     }
-
-    @Override
-    public PerRobotCommentOperations robotComment(String commentUuid) {
-      ChangeNotes changeNotes = getChangeNotes();
-      return perRobotCommentOperationsFactory.create(changeNotes, commentUuid);
-    }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java b/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java
index b7e720b..29737bb 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/CommentSide.java
@@ -16,7 +16,7 @@
 
 /**
  * Marks the commit that contains the comment (also known as side). Used by {@link
- * TestCommentCreation} and {@link TestRobotCommentCreation}.
+ * TestCommentCreation}.
  */
 enum CommentSide {
   PATCHSET_COMMIT(1),
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java
index 6b96ff9..b9856c1 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/FileBuilder.java
@@ -17,8 +17,7 @@
 import java.util.function.Function;
 
 /**
- * Builder for the file specification of line/range comments. Used by {@link TestCommentCreation}
- * and {@link TestRobotCommentCreation}.
+ * Builder for the file specification of line/range comments. Used by {@link TestCommentCreation}.
  */
 public class FileBuilder<T> {
   private final Function<String, T> nextStepProvider;
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java
index b57aa6d..0c4b29d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ParentBuilder.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import java.util.function.Function;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -40,11 +41,22 @@
     return parentToBuilderAdder.apply(TestCommitIdentifier.ofBranch(branchName));
   }
 
-  /** Use the current patchset commit of the indicated change. */
+  /**
+   * Use the current patchset commit of the indicated change.
+   *
+   * @deprecated Use {@link #change(ChangeIdentifier)} instead
+   */
+  // TODO Drop this method when all callers have been migrated to change(ChangeIdentifier)
+  @Deprecated
   public T change(Change.Id changeId) {
     return parentToBuilderAdder.apply(TestCommitIdentifier.ofChangeId(changeId));
   }
 
+  /** Use the current patchset commit of the indicated change. */
+  public T change(ChangeIdentifier changeIdentifier) {
+    return parentToBuilderAdder.apply(TestCommitIdentifier.ofChangeIdentifier(changeIdentifier));
+  }
+
   /** Use the commit identified by the specified patchset. */
   public T patchset(PatchSet.Id patchsetId) {
     return parentToBuilderAdder.apply(TestCommitIdentifier.ofPatchsetId(patchsetId));
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
index f4c70bd..33d2d43 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
@@ -67,25 +67,4 @@
    * @return builder to create a new comment
    */
   TestCommentCreation.Builder newDraftComment();
-
-  /**
-   * Starts the fluent chain to create a new robot comment. The returned builder can be used to
-   * specify the attributes of the robot comment. To create the robot comment for real, {@link
-   * TestRobotCommentCreation.Builder#create()} must be called.
-   *
-   * <p>Example:
-   *
-   * <pre>
-   * String createdRobotCommentUuid = changeOperations
-   *     .change(changeId)
-   *     .currentPatchset()
-   *     .newRobotComment()
-   *     .onLine(2)
-   *     .ofFile("file1")
-   *     .create();
-   * </pre>
-   *
-   * @return builder to create a new comment
-   */
-  TestRobotCommentCreation.Builder newRobotComment();
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index 1fd780f..3efc4c0 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -44,6 +43,7 @@
 import java.time.Instant;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
@@ -83,8 +83,25 @@
 
   @Override
   public TestPatchset get() {
-    PatchSet patchset = changeNotes.getPatchSets().get(patchsetId);
-    return TestPatchset.builder().patchsetId(patchsetId).commitId(patchset.commitId()).build();
+    PatchSet patchSet = changeNotes.getPatchSets().get(patchsetId);
+    RevCommit commit = getCommit(patchSet);
+    return TestPatchset.builder()
+        .patchsetId(patchsetId)
+        .commitId(patchSet.commitId())
+        .commit(commit)
+        .build();
+  }
+
+  private RevCommit getCommit(PatchSet patchSet) {
+    try (Repository repository = repositoryManager.openRepository(changeNotes.getProjectName())) {
+      return repository.parseCommit(patchSet.commitId());
+    } catch (Exception e) {
+      throw new IllegalStateException(
+          String.format(
+              "getting commit of patch set %s of change %s failed",
+              patchSet.id().get(), patchSet.id().changeId().get()),
+          e);
+    }
   }
 
   @Override
@@ -97,11 +114,6 @@
     return TestCommentCreation.builder(this::createComment, Status.DRAFT);
   }
 
-  @Override
-  public TestRobotCommentCreation.Builder newRobotComment() {
-    return TestRobotCommentCreation.builder(this::createRobotComment);
-  }
-
   private String createComment(TestCommentCreation commentCreation)
       throws IOException, RestApiException, UpdateException {
 
@@ -129,11 +141,6 @@
     return userFactory.create(authorId);
   }
 
-  private IdentifiedUser getAuthor(TestRobotCommentCreation robotCommentCreation) {
-    Account.Id authorId = robotCommentCreation.author().orElse(changeNotes.getChange().getOwner());
-    return userFactory.create(authorId);
-  }
-
   private static Comment.Range toCommentRange(TestRange range) {
     Comment.Range commentRange = new Range();
     commentRange.startLine = range.start().line();
@@ -199,82 +206,4 @@
       return newComment;
     }
   }
-
-  private String createRobotComment(TestRobotCommentCreation robotCommentCreation)
-      throws IOException, RestApiException, UpdateException {
-    Project.NameKey project = changeNotes.getProjectName();
-    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
-      try (Repository repository = repositoryManager.openRepository(project);
-          ObjectInserter objectInserter = repository.newObjectInserter();
-          RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-        Instant now = TimeUtil.now();
-
-        IdentifiedUser author = getAuthor(robotCommentCreation);
-        RobotCommentAdditionOp robotCommentAdditionOp =
-            new RobotCommentAdditionOp(robotCommentCreation);
-        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
-          batchUpdate.setRepository(repository, revWalk, objectInserter);
-          batchUpdate.addOp(changeNotes.getChangeId(), robotCommentAdditionOp);
-          batchUpdate.execute();
-        }
-        return robotCommentAdditionOp.createdRobotCommentUuid;
-      }
-    }
-  }
-
-  private class RobotCommentAdditionOp implements BatchUpdateOp {
-    private String createdRobotCommentUuid;
-    private final TestRobotCommentCreation robotCommentCreation;
-
-    public RobotCommentAdditionOp(TestRobotCommentCreation robotCommentCreation) {
-      this.robotCommentCreation = robotCommentCreation;
-    }
-
-    @Override
-    public boolean updateChange(ChangeContext context) {
-      RobotComment robotComment = toNewRobotComment(context, robotCommentCreation);
-      ChangeUpdate changeUpdate = context.getUpdate(patchsetId);
-      changeUpdate.putRobotComment(robotComment);
-      // For robot comments, only the tag set on the ChangeUpdate (and not on the RobotComment)
-      // matters.
-      robotCommentCreation.tag().ifPresent(changeUpdate::setTag);
-      createdRobotCommentUuid = robotComment.key.uuid;
-      return true;
-    }
-
-    private RobotComment toNewRobotComment(
-        ChangeContext context, TestRobotCommentCreation robotCommentCreation) {
-      String message = robotCommentCreation.message().orElse("The text of a test robot comment.");
-
-      String filePath = robotCommentCreation.file().orElse(Patch.PATCHSET_LEVEL);
-      short side = robotCommentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide();
-      String robotId = robotCommentCreation.robotId().orElse("robot");
-      String robotRunId = robotCommentCreation.robotId().orElse("1");
-      RobotComment newRobotComment =
-          commentsUtil.newRobotComment(
-              context, filePath, patchsetId, side, message, robotId, robotRunId);
-
-      // TODO(paiking): This should not be needed, as the tag only matters in ChangeUpdate.
-      robotCommentCreation.tag().ifPresent(tag -> newRobotComment.tag = tag);
-
-      robotCommentCreation.line().ifPresent(line -> newRobotComment.setLineNbrAndRange(line, null));
-      // Specification of range trumps explicit line specification.
-      robotCommentCreation
-          .range()
-          .map(PerPatchsetOperationsImpl::toCommentRange)
-          .ifPresent(range -> newRobotComment.setLineNbrAndRange(null, range));
-
-      robotCommentCreation
-          .parentUuid()
-          .ifPresent(parentUuid -> newRobotComment.parentUuid = parentUuid);
-      robotCommentCreation.url().ifPresent(url -> newRobotComment.url = url);
-      if (!robotCommentCreation.properties().isEmpty()) {
-        newRobotComment.properties = robotCommentCreation.properties();
-      }
-
-      commentsUtil.setCommentCommitId(
-          newRobotComment, context.getChange(), changeNotes.getPatchSets().get(patchsetId));
-      return newRobotComment;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperations.java
deleted file mode 100644
index c9718aa..0000000
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperations.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.testsuite.change;
-
-/** An aggregation of methods on a specific, robot comment. */
-public interface PerRobotCommentOperations {
-
-  /**
-   * Retrieves the robot comment.
-   *
-   * <p><strong>Note:</strong> This call will fail with an exception if the requested comment
-   * doesn't exist or if it is a comment of another type.
-   *
-   * @return the corresponding {@code TestRobotComment}
-   */
-  TestRobotComment get();
-}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperationsImpl.java
deleted file mode 100644
index 075c451..0000000
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerRobotCommentOperationsImpl.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.testsuite.change;
-
-import static com.google.common.collect.MoreCollectors.onlyElement;
-
-import com.google.gerrit.entities.RobotComment;
-import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-/**
- * The implementation of {@link PerRobotCommentOperations}.
- *
- * <p>There is only one implementation of {@link PerRobotCommentOperations}. Nevertheless, we keep
- * the separation between interface and implementation to enhance clarity.
- */
-public class PerRobotCommentOperationsImpl implements PerRobotCommentOperations {
-  private final CommentsUtil commentsUtil;
-
-  private final ChangeNotes changeNotes;
-  private final String commentUuid;
-
-  public interface Factory {
-    PerRobotCommentOperationsImpl create(ChangeNotes changeNotes, String commentUuid);
-  }
-
-  @Inject
-  public PerRobotCommentOperationsImpl(
-      CommentsUtil commentsUtil, @Assisted ChangeNotes changeNotes, @Assisted String commentUuid) {
-    this.commentsUtil = commentsUtil;
-    this.changeNotes = changeNotes;
-    this.commentUuid = commentUuid;
-  }
-
-  @Override
-  public TestRobotComment get() {
-    RobotComment comment =
-        commentsUtil.robotCommentsByChange(changeNotes).stream()
-            .filter(foundComment -> foundComment.key.uuid.equals(commentUuid))
-            .collect(onlyElement());
-    return toTestRobotComment(comment);
-  }
-
-  static TestRobotComment toTestRobotComment(RobotComment robotComment) {
-    return TestRobotComment.builder()
-        .uuid(robotComment.key.uuid)
-        .parentUuid(robotComment.parentUuid)
-        .build();
-  }
-}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java
index b061c81..375ecb6 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PositionBuilder.java
@@ -16,10 +16,7 @@
 
 import java.util.function.IntFunction;
 
-/**
- * Builder to simplify a position specification. Used by {@link TestCommentCreation} and {@link
- * TestRobotCommentCreation}.
- */
+/** Builder to simplify a position specification. Used by {@link TestCommentCreation}. */
 public class PositionBuilder<T> {
   private final IntFunction<T> nextStepProvider;
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java
index b9639f5..57a49c3 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/StartAwarePositionBuilder.java
@@ -17,10 +17,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-/**
- * Builder for the end position of a range. Used by {@link TestCommentCreation} and {@link
- * TestRobotCommentCreation}.
- */
+/** Builder for the end position of a range. Used by {@link TestCommentCreation}. */
 public class StartAwarePositionBuilder<T> {
   private final TestRange.Builder testRangeBuilder;
   private final Consumer<TestRange> rangeConsumer;
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
index ea2acaa6a..391be75 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
@@ -15,12 +15,16 @@
 package com.google.gerrit.acceptance.testsuite.change;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import java.time.Instant;
 
 /** Representation of a change used for testing purposes. */
 @AutoValue
 public abstract class TestChange {
-
   /**
    * The numeric change ID, sometimes also called change number or legacy change ID. Unique per
    * host.
@@ -33,6 +37,33 @@
    */
   public abstract String changeId();
 
+  /** The project of the change. */
+  public abstract Project.NameKey project();
+
+  /** The destination branch of the change. */
+  public abstract BranchNameKey dest();
+
+  /** The subject of the change (first line of the commit message). */
+  public abstract String subject();
+
+  /** The owner of the change. */
+  public abstract Account.Id owner();
+
+  /** The creation timestamp of the change. */
+  public abstract Instant createdOn();
+
+  /** The last updated timestamp of the change. */
+  public abstract Instant lastUpdatedOn();
+
+  /**
+   * Get change identifier to be used in the {@link
+   * com.google.gerrit.extensions.api.changes.Changes} API to get the {@link
+   * com.google.gerrit.extensions.api.changes.ChangeApi}.
+   */
+  public ChangeIdentifier id() {
+    return ChangeIdentifier.byProjectAndNumericChangeId(project().get(), numericChangeId().get());
+  }
+
   static Builder builder() {
     return new AutoValue_TestChange.Builder();
   }
@@ -43,6 +74,18 @@
 
     abstract Builder changeId(String changeId);
 
+    abstract Builder project(Project.NameKey project);
+
+    abstract Builder dest(BranchNameKey dest);
+
+    abstract Builder subject(String subject);
+
+    abstract Builder owner(Account.Id owner);
+
+    abstract Builder createdOn(Instant created);
+
+    abstract Builder lastUpdatedOn(Instant lastUpdated);
+
     abstract TestChange build();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
index 9f51510..a5cbf6d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -26,7 +26,9 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.server.edit.tree.TreeModification;
+import java.time.Instant;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -42,6 +44,8 @@
 
   public abstract String branch();
 
+  public abstract Optional<Instant> createdOn();
+
   public abstract Optional<Account.Id> owner();
 
   public abstract Optional<Account.Id> author();
@@ -68,9 +72,14 @@
 
   abstract ThrowingFunction<TestChangeCreation, Change.Id> changeCreator();
 
-  public static Builder builder(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator) {
+  abstract ThrowingFunction<Change.Id, TestChange> changeGetter();
+
+  public static Builder builder(
+      ThrowingFunction<TestChangeCreation, Change.Id> changeCreator,
+      ThrowingFunction<Change.Id, TestChange> changeGetter) {
     return new AutoValue_TestChangeCreation.Builder()
         .changeCreator(changeCreator)
+        .changeGetter(changeGetter)
         .branch(Constants.R_HEADS + Constants.MASTER)
         .commitMessage("A test change")
         // Which value we choose here doesn't matter. All relevant code paths set the desired value.
@@ -93,6 +102,9 @@
      */
     public abstract Builder branch(String branch);
 
+    /** Creation timestamp of the change. */
+    public abstract Builder createdOn(Instant createdOn);
+
     /**
      * The change owner.
      *
@@ -238,6 +250,8 @@
 
     abstract Builder changeCreator(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator);
 
+    abstract Builder changeGetter(ThrowingFunction<Change.Id, TestChange> changeGetter);
+
     abstract TestChangeCreation autoBuild();
 
     public TestChangeCreation build() {
@@ -254,11 +268,26 @@
      * Creates the change.
      *
      * @return the {@code Change.Id} of the created change
+     * @deprecated use {@link #create()} instead
      */
     @CanIgnoreReturnValue
-    public Change.Id create() {
+    @Deprecated
+    // TODO: Remove this method once all caller have been migrated to the new create() method
+    public Change.Id createV1() {
       TestChangeCreation changeUpdate = build();
       return changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate);
     }
+
+    public ChangeIdentifier create() {
+      // TODO: Update changeCreator to type ThrowingFunction<TestChangeCreation, ChangeIdentifier>
+      // and then change this to 'changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate)'.
+      return createAndGet().id();
+    }
+
+    public TestChange createAndGet() {
+      TestChangeCreation changeUpdate = build();
+      Change.Id changeId = changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate);
+      return changeUpdate.changeGetter().applyAndThrowSilently(changeId);
+    }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
index 9828e6c..3e31091 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestCommentCreation.java
@@ -27,11 +27,7 @@
 import java.time.ZoneOffset;
 import java.util.Optional;
 
-/**
- * Attributes of the human comment. If not provided, arbitrary values will be used. This class is
- * very similar to {@link TestRobotCommentCreation} to allow separation between robot and human
- * comments.
- */
+/** Attributes of the human comment. If not provided, arbitrary values will be used.. */
 @AutoValue
 public abstract class TestCommentCreation {
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java b/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java
index a352607..57eb94f 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestCommitIdentifier.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoOneOf;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Attributes, each one uniquely identifying a commit. */
@@ -26,6 +27,7 @@
     COMMIT_SHA_1,
     BRANCH,
     CHANGE_ID,
+    CHANGE_IDENTIFIER,
     PATCHSET_ID
   }
 
@@ -40,6 +42,9 @@
   /** Numeric ID of the change whose current patchset points to the desired commit. */
   public abstract Change.Id changeId();
 
+  /** Identifier of the change whose current patchset points to the desired commit. */
+  public abstract ChangeIdentifier changeIdentifier();
+
   /** ID of the patchset representing the desired commit. */
   public abstract PatchSet.Id patchsetId();
 
@@ -55,6 +60,10 @@
     return AutoOneOf_TestCommitIdentifier.changeId(changeId);
   }
 
+  public static TestCommitIdentifier ofChangeIdentifier(ChangeIdentifier changeIdentifier) {
+    return AutoOneOf_TestCommitIdentifier.changeIdentifier(changeIdentifier);
+  }
+
   public static TestCommitIdentifier ofPatchsetId(PatchSet.Id patchsetId) {
     return AutoOneOf_TestCommitIdentifier.patchsetId(patchsetId);
   }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
index 1ba242a..a038ea0 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.PatchSet;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 
 /** Representation of a patchset used for testing purposes. */
 @AutoValue
@@ -28,6 +29,9 @@
   /** The commit SHA-1 of the patchset. */
   public abstract ObjectId commitId();
 
+  /** The commit of the patchset. */
+  public abstract RevCommit commit();
+
   static Builder builder() {
     return new AutoValue_TestPatchset.Builder();
   }
@@ -38,6 +42,8 @@
 
     abstract Builder commitId(ObjectId commitId);
 
+    abstract Builder commit(RevCommit commit);
+
     abstract TestPatchset build();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestRobotComment.java b/java/com/google/gerrit/acceptance/testsuite/change/TestRobotComment.java
deleted file mode 100644
index 76fb52f..0000000
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestRobotComment.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.testsuite.change;
-
-import com.google.auto.value.AutoValue;
-import com.google.gerrit.common.Nullable;
-import java.util.Optional;
-
-/** Representation of a robot comment used for testing purposes. */
-@AutoValue
-public abstract class TestRobotComment {
-
-  /** The UUID of the comment. Should be unique. */
-  public abstract String uuid();
-
-  /** UUID of another comment to which this comment is a reply. */
-  public abstract Optional<String> parentUuid();
-
-  static Builder builder() {
-    return new AutoValue_TestRobotComment.Builder();
-  }
-
-  @AutoValue.Builder
-  abstract static class Builder {
-    abstract Builder uuid(String uuid);
-
-    abstract Builder parentUuid(@Nullable String parentUuid);
-
-    abstract TestRobotComment build();
-  }
-}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestRobotCommentCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestRobotCommentCreation.java
deleted file mode 100644
index 558af3f..0000000
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestRobotCommentCreation.java
+++ /dev/null
@@ -1,200 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.testsuite.change;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
-import com.google.gerrit.acceptance.testsuite.change.TestRange.Position;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Patch;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * Attributes of the robot comment. If not provided, arbitrary values will be used. This class is
- * very similar to {@link TestCommentCreation} to allow separation between robot and human comments.
- */
-@AutoValue
-public abstract class TestRobotCommentCreation {
-
-  public abstract Optional<String> message();
-
-  public abstract Optional<String> file();
-
-  public abstract Optional<Integer> line();
-
-  public abstract Optional<TestRange> range();
-
-  public abstract Optional<CommentSide> side();
-
-  public abstract Optional<String> parentUuid();
-
-  public abstract Optional<String> tag();
-
-  public abstract Optional<Account.Id> author();
-
-  public abstract Optional<String> robotId();
-
-  public abstract Optional<String> robotRunId();
-
-  public abstract Optional<String> url();
-
-  public abstract ImmutableMap<String, String> properties();
-
-  abstract ThrowingFunction<TestRobotCommentCreation, String> commentCreator();
-
-  public static Builder builder(ThrowingFunction<TestRobotCommentCreation, String> commentCreator) {
-    return new AutoValue_TestRobotCommentCreation.Builder().commentCreator(commentCreator);
-  }
-
-  @AutoValue.Builder
-  public abstract static class Builder {
-
-    public Builder noMessage() {
-      return message("");
-    }
-
-    /** Message text of the comment. */
-    public abstract Builder message(String message);
-
-    /** Indicates a patchset-level comment. */
-    public Builder onPatchsetLevel() {
-      return file(Patch.PATCHSET_LEVEL);
-    }
-
-    /** Indicates a file comment. The comment will be on the specified file. */
-    public Builder onFileLevelOf(String filePath) {
-      return file(filePath).line(null).range(null);
-    }
-
-    /**
-     * Starts the fluent change to create a line comment. The line comment will be at the indicated
-     * line. Lines start with 1.
-     */
-    public FileBuilder<Builder> onLine(int line) {
-      return new FileBuilder<>(file -> file(file).line(line).range(null));
-    }
-
-    /**
-     * Starts the fluent chain to create a range comment. The range begins at the specified line.
-     * Lines start at 1. The start position (line, charOffset) is inclusive, the end position (line,
-     * charOffset) is exclusive.
-     */
-    public PositionBuilder<StartAwarePositionBuilder<Builder>> fromLine(int startLine) {
-      return new PositionBuilder<>(
-          startCharOffset -> {
-            Position start = Position.builder().line(startLine).charOffset(startCharOffset).build();
-            TestRange.Builder testRangeBuilder = TestRange.builder().setStart(start);
-            return new StartAwarePositionBuilder<>(testRangeBuilder, this::range, this::file);
-          });
-    }
-
-    /** File on which the comment should be added. */
-    abstract Builder file(String filePath);
-
-    /** Line on which the comment should be added. */
-    abstract Builder line(@Nullable Integer line);
-
-    /** Range on which the comment should be added. */
-    abstract Builder range(@Nullable TestRange range);
-
-    /**
-     * Indicates that the comment refers to a file, line, range, ... in the commit of the patchset.
-     *
-     * <p>On the UI, such comments are shown on the right side of a diff view when a diff against
-     * base is selected. See {@link #onParentCommit()} for comments shown on the left side.
-     */
-    public Builder onPatchsetCommit() {
-      return side(CommentSide.PATCHSET_COMMIT);
-    }
-
-    /**
-     * Indicates that the comment refers to a file, line, range, ... in the parent commit of the
-     * patchset.
-     *
-     * <p>On the UI, such comments are shown on the left side of a diff view when a diff against
-     * base is selected. See {@link #onPatchsetCommit()} for comments shown on the right side.
-     *
-     * <p>For merge commits, this indicates the first parent commit.
-     */
-    public Builder onParentCommit() {
-      return side(CommentSide.PARENT_COMMIT);
-    }
-
-    /** Like {@link #onParentCommit()} but for the second parent of a merge commit. */
-    public Builder onSecondParentCommit() {
-      return side(CommentSide.SECOND_PARENT_COMMIT);
-    }
-
-    /**
-     * Like {@link #onParentCommit()} but for the AutoMerge commit created from the parents of a
-     * merge commit.
-     */
-    public Builder onAutoMergeCommit() {
-      return side(CommentSide.AUTO_MERGE_COMMIT);
-    }
-
-    abstract Builder side(CommentSide side);
-
-    /**
-     * UUID of another comment to which this comment is a reply. This comment must have similar
-     * attributes (e.g. file, line, side) as the parent comment. The parent comment must be a
-     * published comment.
-     */
-    public abstract Builder parentUuid(String parentUuid);
-
-    /** Tag to attach to the comment. */
-    public abstract Builder tag(String value);
-
-    /** Author of the comment. Must be an existing user account. */
-    public abstract Builder author(Account.Id accountId);
-
-    /** Id of the robot that created the comment. */
-    public abstract Builder robotId(String robotId);
-
-    /** An ID of the run of the robot that created the comment. */
-    public abstract Builder robotRunId(String robotRunId);
-
-    /** Url for more information for the robot comment. */
-    public abstract Builder url(String url);
-
-    /** Robot specific properties as map that maps arbitrary keys to values. */
-    public abstract Builder properties(Map<String, String> properties);
-
-    abstract ImmutableMap.Builder<String, String> propertiesBuilder();
-
-    public Builder addProperty(String key, String value) {
-      propertiesBuilder().put(key, value);
-      return this;
-    }
-
-    abstract Builder commentCreator(
-        ThrowingFunction<TestRobotCommentCreation, String> commentCreator);
-
-    abstract TestRobotCommentCreation autoBuild();
-
-    /**
-     * Creates the robot comment.
-     *
-     * @return the UUID of the created robot comment
-     */
-    public String create() {
-      TestRobotCommentCreation commentCreation = autoBuild();
-      return commentCreation.commentCreator().applyAndThrowSilently(commentCreation);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestVote.java b/java/com/google/gerrit/acceptance/testsuite/change/TestVote.java
new file mode 100644
index 0000000..d76ef63
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestVote.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Account;
+
+/** Representation of a vote used for testing purposes. */
+@AutoValue
+public abstract class TestVote {
+  /** The user that applied the vote. */
+  public abstract Account.Id userId();
+
+  /** The label of the vote. */
+  public abstract String label();
+
+  /** The value of the vote. */
+  public abstract int value();
+
+  static Builder builder() {
+    return new AutoValue_TestVote.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+    abstract Builder userId(Account.Id userId);
+
+    abstract Builder label(String label);
+
+    abstract Builder value(int value);
+
+    abstract TestVote build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestVoteCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestVoteCreation.java
new file mode 100644
index 0000000..cc977e5
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestVoteCreation.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.LabelId;
+import java.util.Optional;
+
+/** Initial attributes of the vote. If not provided, arbitrary values will be used. */
+@AutoValue
+public abstract class TestVoteCreation {
+  public abstract Optional<Account.Id> user();
+
+  public abstract Optional<String> label();
+
+  public abstract Optional<Integer> value();
+
+  abstract ThrowingFunction<TestVoteCreation, TestVote> voteCreator();
+
+  public static TestVoteCreation.Builder builder(
+      ThrowingFunction<TestVoteCreation, TestVote> voteCreator) {
+    return new AutoValue_TestVoteCreation.Builder().voteCreator(voteCreator);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /**
+     * The user for which the vote should be created.
+     *
+     * <p>Must be an existing user account.
+     *
+     * <p>If not set the new vote is applied by an arbitrary user that is not the change owner.
+     */
+    public abstract Builder user(Account.Id user);
+
+    /**
+     * The label on which the vote should be created.
+     *
+     * <p>Must be an existing label.
+     *
+     * <p>If not set the new vote is applied on an arbitrary label.
+     */
+    public abstract Builder label(String label);
+
+    /**
+     * The value with which the vote should be created.
+     *
+     * <p>Must be a value that is allowed for the label.
+     *
+     * <p>If not set the new vote is applied with an arbitrary non-zero value.
+     */
+    public abstract Builder value(int value);
+
+    public Builder codeReviewApproval() {
+      return label(LabelId.CODE_REVIEW).value(2);
+    }
+
+    public Builder codeReviewVeto() {
+      return label(LabelId.CODE_REVIEW).value(-2);
+    }
+
+    abstract TestVoteCreation.Builder voteCreator(
+        ThrowingFunction<TestVoteCreation, TestVote> voteCreator);
+
+    abstract TestVoteCreation build();
+
+    /**
+     * Creates the vote.
+     *
+     * @return the {@link TestVote} representing the created vote
+     */
+    @CanIgnoreReturnValue
+    public TestVote create() {
+      TestVoteCreation voteCreation = build();
+      return voteCreation.voteCreator().applyAndThrowSilently(voteCreation);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index a3112f8..ce9b44d 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -149,6 +149,11 @@
           addPermissions(projectConfig, projectUpdate.addedPermissions());
           addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions());
           setExclusiveGroupPermissions(projectConfig, projectUpdate.exclusiveGroupPermissions());
+          projectConfig.updateProject(
+              update ->
+                  projectUpdate
+                      .booleanProjectConfigs()
+                      .forEach((name, value) -> update.setBooleanConfig(name, value)));
           projectConfig.commit(metaDataUpdate);
         }
         projectCache.evictAndReindex(nameKey);
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index cc57ba6..a43a12a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -25,11 +25,13 @@
 import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.PermissionRange;
 import com.google.gerrit.entities.PermissionRule;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.server.config.AllProjectsName;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Constants;
@@ -351,6 +353,150 @@
 
     abstract Builder removeAllAccessSections(boolean value);
 
+    abstract ImmutableMap.Builder<BooleanProjectConfig, InheritableBoolean>
+        booleanProjectConfigsBuilder();
+
+    public Builder useContributorAgreements(InheritableBoolean useContributorAgreements) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, useContributorAgreements);
+      return this;
+    }
+
+    public Builder useContributorAgreements() {
+      return useContributorAgreements(InheritableBoolean.TRUE);
+    }
+
+    public Builder useSignedOffBy(InheritableBoolean useSignedOffBy) {
+      booleanProjectConfigsBuilder().put(BooleanProjectConfig.USE_SIGNED_OFF_BY, useSignedOffBy);
+      return this;
+    }
+
+    public Builder useSignedOffBy() {
+      return useSignedOffBy(InheritableBoolean.TRUE);
+    }
+
+    public Builder useContentMerge(InheritableBoolean useContentMerge) {
+      booleanProjectConfigsBuilder().put(BooleanProjectConfig.USE_CONTENT_MERGE, useContentMerge);
+      return this;
+    }
+
+    public Builder useContentMerge() {
+      return useContentMerge(InheritableBoolean.TRUE);
+    }
+
+    public Builder requireChangeId(InheritableBoolean requireChangeId) {
+      booleanProjectConfigsBuilder().put(BooleanProjectConfig.REQUIRE_CHANGE_ID, requireChangeId);
+      return this;
+    }
+
+    public Builder requireChangeId() {
+      return requireChangeId(InheritableBoolean.TRUE);
+    }
+
+    public Builder createNewChangeForAllNotInTarget(
+        InheritableBoolean createNewChangeForAllNotInTarget) {
+      booleanProjectConfigsBuilder()
+          .put(
+              BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+              createNewChangeForAllNotInTarget);
+      return this;
+    }
+
+    public Builder createNewChangeForAllNotInTarget() {
+      return createNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
+    }
+
+    public Builder enableSignedPush(InheritableBoolean enableSignedPush) {
+      booleanProjectConfigsBuilder().put(BooleanProjectConfig.ENABLE_SIGNED_PUSH, enableSignedPush);
+      return this;
+    }
+
+    public Builder enableSignedPush() {
+      return enableSignedPush(InheritableBoolean.TRUE);
+    }
+
+    public Builder requireSignedPush(InheritableBoolean requireSignedPush) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.REQUIRE_SIGNED_PUSH, requireSignedPush);
+      return this;
+    }
+
+    public Builder requireSignedPush() {
+      return requireSignedPush(InheritableBoolean.TRUE);
+    }
+
+    public Builder rejectImplicitMerges(InheritableBoolean rejectImplicitMerges) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.REJECT_IMPLICIT_MERGES, rejectImplicitMerges);
+      return this;
+    }
+
+    public Builder rejectImplicitMerges() {
+      return rejectImplicitMerges(InheritableBoolean.TRUE);
+    }
+
+    public Builder privateByDefault(InheritableBoolean privateByDefault) {
+      booleanProjectConfigsBuilder().put(BooleanProjectConfig.PRIVATE_BY_DEFAULT, privateByDefault);
+      return this;
+    }
+
+    public Builder privateByDefault() {
+      return privateByDefault(InheritableBoolean.TRUE);
+    }
+
+    public Builder enableReviewerByEmail(InheritableBoolean enableReviewerByEmail) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, enableReviewerByEmail);
+      return this;
+    }
+
+    public Builder enableReviewerByEmail() {
+      return enableReviewerByEmail(InheritableBoolean.TRUE);
+    }
+
+    public Builder matchAuthorToCommitterDate(InheritableBoolean matchAuthorToCommitterDate) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE, matchAuthorToCommitterDate);
+      return this;
+    }
+
+    public Builder matchAuthorToCommitterDate() {
+      return matchAuthorToCommitterDate(InheritableBoolean.TRUE);
+    }
+
+    public Builder rejectEmptyCommit(InheritableBoolean rejectEmptyCommit) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.REJECT_EMPTY_COMMIT, rejectEmptyCommit);
+      return this;
+    }
+
+    public Builder rejectEmptyCommit() {
+      return rejectEmptyCommit(InheritableBoolean.TRUE);
+    }
+
+    public Builder workInProgressByDefault(InheritableBoolean workInProgressByDefault) {
+      booleanProjectConfigsBuilder()
+          .put(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT, workInProgressByDefault);
+      return this;
+    }
+
+    public Builder workInProgressByDefault() {
+      return workInProgressByDefault(InheritableBoolean.TRUE);
+    }
+
+    public Builder skipAddingAuthorAndCommitterAsReviewers(
+        InheritableBoolean skipAddingAuthorAndCommitterAsReviewers) {
+      booleanProjectConfigsBuilder()
+          .put(
+              BooleanProjectConfig.SKIP_ADDING_AUTHOR_AND_COMMITTER_AS_REVIEWERS,
+              skipAddingAuthorAndCommitterAsReviewers);
+      return this;
+    }
+
+    public Builder skipAddingAuthorAndCommitterAsReviewers() {
+      return skipAddingAuthorAndCommitterAsReviewers(InheritableBoolean.TRUE);
+    }
+
     /**
      * Removes all access sections. Useful when testing against a specific set of access sections or
      * permissions.
@@ -476,6 +622,8 @@
 
   abstract boolean removeAllAccessSections();
 
+  abstract ImmutableMap<BooleanProjectConfig, InheritableBoolean> booleanProjectConfigs();
+
   boolean hasCapabilityUpdates() {
     return !addedCapabilities().isEmpty()
         || removedPermissions().stream().anyMatch(k -> k.section().equals(GLOBAL_CAPABILITIES));
diff --git a/java/com/google/gerrit/auth/ldap/Helper.java b/java/com/google/gerrit/auth/ldap/Helper.java
index b4e6bf2..53e8ac5 100644
--- a/java/com/google/gerrit/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/auth/ldap/Helper.java
@@ -109,14 +109,14 @@
     String readTimeout = LdapRealm.optional(config, "readTimeout");
     if (readTimeout != null) {
       readTimeoutMillis =
-          Long.toString(ConfigUtil.getTimeUnit(readTimeout, 0, TimeUnit.MILLISECONDS));
+          Long.toString(ConfigUtil.getTimeUnit(readTimeout, 60000, TimeUnit.MILLISECONDS));
     } else {
       readTimeoutMillis = null;
     }
     String connectTimeout = LdapRealm.optional(config, "connectTimeout");
     if (connectTimeout != null) {
       connectTimeoutMillis =
-          Long.toString(ConfigUtil.getTimeUnit(connectTimeout, 0, TimeUnit.MILLISECONDS));
+          Long.toString(ConfigUtil.getTimeUnit(connectTimeout, 10000, TimeUnit.MILLISECONDS));
     } else {
       connectTimeoutMillis = null;
     }
diff --git a/java/com/google/gerrit/auth/oauth/OAuthRealm.java b/java/com/google/gerrit/auth/oauth/OAuthRealm.java
index c329cc0..ca7054c 100644
--- a/java/com/google/gerrit/auth/oauth/OAuthRealm.java
+++ b/java/com/google/gerrit/auth/oauth/OAuthRealm.java
@@ -108,6 +108,10 @@
             || !allowsEdit(AccountFieldName.FULL_NAME))) {
       who.setDisplayName(userInfo.getDisplayName());
     }
+    if (who.getExternalIdKey() == null) {
+      String[] externalIdKeyParts = userInfo.getExternalId().split(":", 2);
+      who.setExternalIdKey(externalIdKeyParts[0], externalIdKeyParts[1]);
+    }
     return who;
   }
 
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 4b7694a..0d0d158 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -18,7 +18,7 @@
 import static com.google.gerrit.entities.RefNames.REFS_STARRED_CHANGES;
 import static com.google.gerrit.entities.RefNames.REFS_USERS;
 
-import com.google.auto.value.AutoValue;
+import com.google.auto.value.AutoBuilder;
 import com.google.common.base.MoreObjects;
 import com.google.common.primitives.Ints;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -46,21 +46,41 @@
  *       is found.
  *   <li>{@link DiffPreferencesInfo}: user's preferences for rendering side-to-side and unified diff
  * </ul>
+ *
+ * @param id The unique identifier of the account.
+ * @param registeredOn The date and time the user registered with the review server.
+ * @param fullName The full name of the user ("Given-name Surname" style).
+ * @param displayName An optional display name of the user to be shown in the UI.
+ * @param preferredEmail The email address the user prefers to be contacted through.
+ * @param inactive Is this user inactive? This is used to avoid showing some users (eg. former
+ *     employees) in auto-suggest.
+ * @param status The user-settable status of this account (e.g. busy, OOO, available)
+ * @param metaId The ID of the user branch from which the account was read.
+ * @param uniqueTag A unique tag which identifies the current version of the account. It can be any
+ *     non-empty string. For open-source gerrit it is the same as metaId. The value can be null only
+ *     during account updating/creation.
  */
-@AutoValue
-public abstract class Account {
+public record Account(
+    Id id,
+    Instant registeredOn,
+    @Nullable String fullName,
+    @Nullable String displayName,
+    @Nullable String preferredEmail,
+    boolean inactive,
+    @Nullable String status,
+    @Nullable String metaId,
+    @Nullable String uniqueTag) {
 
   /** Placeholder for indicating an account-id that does not correspond to any local account */
   public static final Id UNKNOWN_ACCOUNT_ID = id(0);
 
   public static Id id(int id) {
-    return new AutoValue_Account_Id(id);
+    return new Id(id);
   }
 
   /** Key local to Gerrit to identify a user. */
-  @AutoValue
   @ConvertibleToProto
-  public abstract static class Id implements Comparable<Id> {
+  public record Id(int id) implements Comparable<Id> {
     /** Parse an Account.Id out of a string representation. */
     public static Optional<Id> tryParse(String str) {
       return Optional.ofNullable(Ints.tryParse(str)).map(Account::id);
@@ -115,65 +135,21 @@
       return id != null ? Account.id(id) : null;
     }
 
-    abstract int id();
-
     public int get() {
       return id();
     }
 
     @Override
-    public final int compareTo(Id o) {
+    public int compareTo(Id o) {
       return Integer.compare(id(), o.id());
     }
 
     @Override
-    public final String toString() {
+    public String toString() {
       return Integer.toString(get());
     }
   }
 
-  public abstract Id id();
-
-  /** Date and time the user registered with the review server. */
-  public abstract Instant registeredOn();
-
-  /** Full name of the user ("Given-name Surname" style). */
-  @Nullable
-  public abstract String fullName();
-
-  /** Optional display name of the user to be shown in the UI. */
-  @Nullable
-  public abstract String displayName();
-
-  /** Email address the user prefers to be contacted through. */
-  @Nullable
-  public abstract String preferredEmail();
-
-  /**
-   * Is this user inactive? This is used to avoid showing some users (eg. former employees) in
-   * auto-suggest.
-   */
-  public abstract boolean inactive();
-
-  /** The user-settable status of this account (e.g. busy, OOO, available) */
-  @Nullable
-  public abstract String status();
-
-  /** ID of the user branch from which the account was read. */
-  @Nullable
-  public abstract String metaId();
-
-  /**
-   * A unique tag which identifies the current version of the account.
-   *
-   * <p>It can be any non-empty string. For open-source gerrit it is the same as metaId; internally
-   * in google a different value is assigned.
-   *
-   * <p>The value can be null only during account updating/creation.
-   */
-  @Nullable
-  public abstract String uniqueTag();
-
   /**
    * Create a new account.
    *
@@ -181,7 +157,7 @@
    * @param registeredOn when the account was registered.
    */
   public static Account.Builder builder(Account.Id newId, Instant registeredOn) {
-    return new AutoValue_Account.Builder()
+    return new AutoBuilder_Account_Builder()
         .setInactive(false)
         .setId(newId)
         .setRegisteredOn(registeredOn);
@@ -245,9 +221,11 @@
     return !inactive();
   }
 
-  public abstract Builder toBuilder();
+  public Builder toBuilder() {
+    return new AutoBuilder_Account_Builder(this);
+  }
 
-  @AutoValue.Builder
+  @AutoBuilder
   public abstract static class Builder {
     public abstract Id id();
 
@@ -300,11 +278,11 @@
   }
 
   @Override
-  public final String toString() {
+  public String toString() {
     return getName();
   }
 
-  public final String debugString() {
+  public String debugString() {
     return MoreObjects.toStringHelper(this)
         .add("id", id())
         .add("registeredOn", registeredOn())
diff --git a/java/com/google/gerrit/entities/BooleanProjectConfig.java b/java/com/google/gerrit/entities/BooleanProjectConfig.java
index 09f63d4..aa57194 100644
--- a/java/com/google/gerrit/entities/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/entities/BooleanProjectConfig.java
@@ -38,7 +38,7 @@
   CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET("receive", "createNewChangeForAllNotInTarget"),
   ENABLE_SIGNED_PUSH("receive", "enableSignedPush"),
   REQUIRE_SIGNED_PUSH("receive", "requireSignedPush"),
-  REJECT_IMPLICIT_MERGES("receive", "rejectImplicitMerges"),
+  REJECT_IMPLICIT_MERGES("receive", "rejectImplicitMerges", true),
   PRIVATE_BY_DEFAULT("change", "privateByDefault"),
   ENABLE_REVIEWER_BY_EMAIL("reviewer", "enableByEmail"),
   MATCH_AUTHOR_TO_COMMITTER_DATE("submit", "matchAuthorToCommitterDate"),
@@ -50,10 +50,16 @@
   // Git config
   private final String section;
   private final String name;
+  private final boolean defaultValue;
 
   BooleanProjectConfig(String section, String name) {
+    this(section, name, false);
+  }
+
+  BooleanProjectConfig(String section, String name, boolean defaultValue) {
     this.section = section;
     this.name = name;
+    this.defaultValue = defaultValue;
   }
 
   public String getSection() {
@@ -68,4 +74,14 @@
   public String getName() {
     return name;
   }
+
+  public boolean getDefaultValue() {
+    return defaultValue;
+  }
+
+  public String format() {
+    return getSubSection() != null
+        ? String.format("%s.%s.%s", getSection(), getSubSection(), getName())
+        : String.format("%s.%s", getSection(), getName());
+  }
 }
diff --git a/java/com/google/gerrit/entities/CoreDownloadSchemes.java b/java/com/google/gerrit/entities/CoreDownloadSchemes.java
index 85e55a0..2e9161c 100644
--- a/java/com/google/gerrit/entities/CoreDownloadSchemes.java
+++ b/java/com/google/gerrit/entities/CoreDownloadSchemes.java
@@ -21,6 +21,7 @@
   public static final String HTTP = "http";
   public static final String SSH = "ssh";
   public static final String REPO = "repo";
+  public static final String DEPOT_TOOLS = "depot_tools";
 
   private CoreDownloadSchemes() {}
 }
diff --git a/java/com/google/gerrit/entities/GeneralProjectName.java b/java/com/google/gerrit/entities/GeneralProjectName.java
new file mode 100644
index 0000000..92bd178
--- /dev/null
+++ b/java/com/google/gerrit/entities/GeneralProjectName.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import com.google.gerrit.common.ConvertibleToProto;
+import com.google.gerrit.entities.Project.NameKey;
+
+@ConvertibleToProto
+public record GeneralProjectName(String name) implements Project.NameKey {
+
+  private static final long serialVersionUID = 1L;
+
+  public GeneralProjectName(NameKey nameKey) {
+    this(nameKey.name());
+  }
+
+  @Override
+  public int hashCode() {
+    return projectNameHashCode();
+  }
+
+  @Override
+  public boolean equals(Object b) {
+    return projectNameEquals(b);
+  }
+
+  @Override
+  public String toString() {
+    return projectNameToString();
+  }
+}
diff --git a/java/com/google/gerrit/entities/LabelFunction.java b/java/com/google/gerrit/entities/LabelFunction.java
index d49ab0f..4da3d2b 100644
--- a/java/com/google/gerrit/entities/LabelFunction.java
+++ b/java/com/google/gerrit/entities/LabelFunction.java
@@ -131,4 +131,36 @@
 
     return submitRecordLabel;
   }
+
+  public record LabelStatus(SubmitRecord.Label.Status status, @Nullable Account.Id voter) {}
+
+  /** Same as check above, but doesn't use SubmitRecord types. */
+  public LabelStatus checkNoRule(LabelType labelType, Iterable<PatchSetApproval> approvals) {
+    SubmitRecord.Label.Status status = SubmitRecord.Label.Status.MAY;
+    if (isRequired) {
+      status = SubmitRecord.Label.Status.NEED;
+    }
+    Account.Id appliedBy = null;
+
+    for (PatchSetApproval a : approvals) {
+      if (a.value() == 0) {
+        continue;
+      }
+
+      if (isBlock && labelType.isMaxNegative(a)) {
+        return new LabelStatus(SubmitRecord.Label.Status.REJECT, /* voter= */ a.accountId());
+      }
+
+      if (labelType.isMaxPositive(a) || !requiresMaxValue) {
+        appliedBy = a.accountId();
+
+        status = SubmitRecord.Label.Status.MAY;
+        if (isRequired) {
+          status = SubmitRecord.Label.Status.OK;
+        }
+      }
+    }
+
+    return new LabelStatus(status, appliedBy);
+  }
 }
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index f69f58c..64b0997 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -22,10 +22,12 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 
+@ExtensionPoint
 @AutoValue
 public abstract class LabelType {
   public static final boolean DEF_ALLOW_POST_SUBMIT = true;
@@ -206,6 +208,8 @@
 
     public abstract Builder setDescription(Optional<String> description);
 
+    public abstract Builder setDescription(String description);
+
     /**
      * @deprecated All label functions except {@code PATCH_SET_LOCK} are deprecated in favour of
      *     using submit requirements. When submit requirements are used the label function needs to
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index bef6580..1dc7149 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -21,6 +21,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Patch.FileMode;
 import java.util.List;
 
 /**
@@ -204,6 +205,10 @@
     public int getMode() {
       return mode;
     }
+
+    public int getModeAsOctal() {
+      return Integer.parseInt(Integer.toOctalString(getMode()));
+    }
   }
 
   private Patch() {}
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index b3f685d..8ea4561 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -25,6 +25,7 @@
 import com.google.errorprone.annotations.InlineMe;
 import com.google.gerrit.common.ConvertibleToProto;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import java.time.Instant;
 import java.util.List;
 import java.util.Optional;
@@ -294,6 +295,30 @@
   @ConvertibleToProto
   public abstract static class Conflicts {
     /**
+     * The SHA1 of the commit that was used as the base commit for the Git merge that created the
+     * revision.
+     *
+     * <p>A base is not set if:
+     *
+     * <ul>
+     *   <li>the merged commits do not have a common ancestor (in this case {@link #noBaseReason()}
+     *       is {@link NoMergeBaseReason#NO_COMMON_ANCESTOR}).
+     *   <li>the merged commits have multiple merge bases (happens for criss-cross-merges) and the
+     *       base was computed (in this case {@link #noBaseReason()} is {@link
+     *       NoMergeBaseReason#COMPUTED_BASE}).
+     *   <li>a one sided merge strategy (e.g. {@code ours} or {@code theirs}) has been used and
+     *       computing a base was not required for the merge (in this case {@link #noBaseReason()}
+     *       is {@link NoMergeBaseReason#ONE_SIDED_MERGE_STRATEGY}).
+     *   <li>the revision was not created by performing a Git merge operation (in this case {@link
+     *       #noBaseReason()} is {@link NoMergeBaseReason#NO_MERGE_PERFORMED}).
+     *   <li>the revision has been created before Gerrit started to store the base for conflicts (in
+     *       this case {@link #noBaseReason()} is {@link
+     *       NoMergeBaseReason#HISTORIC_DATA_WITHOUT_BASE}).
+     * </ul>
+     */
+    public abstract Optional<ObjectId> base();
+
+    /**
      * The SHA1 of the commit that was used as {@code ours} for the Git merge that created the
      * revision.
      *
@@ -314,6 +339,35 @@
     public abstract Optional<ObjectId> theirs();
 
     /**
+     * The merge strategy was used for the Git merge that created the revision.
+     *
+     * <p>Possible values: {@code resolve}, {@code recursive}, {@code simple-two-way-in-core},
+     * {@code ours} and {@code theirs}.
+     */
+    public abstract Optional<String> mergeStrategy();
+
+    /**
+     * Reason why {@link #base()} is not set.
+     *
+     * <p>Only set if {@link #base()} is not set.
+     *
+     * <p>Possible values are:
+     *
+     * <ul>
+     *   <li>{@code NO_COMMON_ANCESTOR}: The merged commits do not have a common ancestor.
+     *   <li>{@code COMPUTED_BASE}: The merged commits have multiple merge bases (happens for
+     *       criss-cross-merges) and the base was computed.
+     *   <li>{@code ONE_SIDED_MERGE_STRATEGY}: A one sided merge strategy (e.g. {@code ours} or
+     *       {@code theirs}) has been used and computing a base was not required for the merge.
+     *   <li>{@code NO_MERGE_PERFORMED}: The revision was not created by performing a Git merge
+     *       operation.
+     *   <li>{@code HISTORIC_DATA_WITHOUT_BASE}: The revision has been created before Gerrit started
+     *       to store the base for conflicts.
+     * </ul>
+     */
+    public abstract Optional<NoMergeBaseReason> noBaseReason();
+
+    /**
      * Whether any of the files in the revision has a conflict due to merging {@link #ours} and
      * {@link #theirs}.
      *
@@ -327,8 +381,14 @@
     public abstract boolean containsConflicts();
 
     public static Conflicts create(
-        Optional<ObjectId> ours, Optional<ObjectId> theirs, boolean containsConflicts) {
-      return new AutoValue_PatchSet_Conflicts(ours, theirs, containsConflicts);
+        Optional<ObjectId> base,
+        Optional<ObjectId> ours,
+        Optional<ObjectId> theirs,
+        Optional<String> mergeStrategy,
+        Optional<NoMergeBaseReason> noBaseReason,
+        boolean containsConflicts) {
+      return new AutoValue_PatchSet_Conflicts(
+          base, ours, theirs, mergeStrategy, noBaseReason, containsConflicts);
     }
   }
 }
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 1f2f151..4d926b8 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -31,7 +31,9 @@
 public abstract class Permission implements Comparable<Permission> {
   public static final String ABANDON = "abandon";
   public static final String ADD_PATCH_SET = "addPatchSet";
+  // https://gerrit-review.googlesource.com/Documentation/access-control.html#category_create
   public static final String CREATE = "create";
+  // https://gerrit-review.googlesource.com/Documentation/access-control.html#category_create_signed
   public static final String CREATE_SIGNED_TAG = "createSignedTag";
   public static final String CREATE_TAG = "createTag";
   public static final String DELETE = "delete";
@@ -47,8 +49,10 @@
   public static final String LABEL_AS = "labelAs-";
   public static final String REMOVE_LABEL = "removeLabel-";
   public static final String OWNER = "owner";
+  // https://gerrit-review.googlesource.com/Documentation/access-control.html#category_push
   public static final String PUSH = "push";
   public static final String PUSH_MERGE = "pushMerge";
+  // https://gerrit-review.googlesource.com/Documentation/access-control.html#category_read
   public static final String READ = "read";
   public static final String REBASE = "rebase";
   public static final String REMOVE_REVIEWER = "removeReviewer";
diff --git a/java/com/google/gerrit/entities/PredicateResult.java b/java/com/google/gerrit/entities/PredicateResult.java
new file mode 100644
index 0000000..ba8fe72
--- /dev/null
+++ b/java/com/google/gerrit/entities/PredicateResult.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+/**
+ * Entity detailing the result of evaluating a predicate.
+ *
+ * <p>Example - branch:refs/heads/foo and has:unresolved
+ *
+ * <p>The above predicate is an "And" predicate having two child predicates:
+ *
+ * <ul>
+ *   <li>branch:refs/heads/foo
+ *   <li>has:unresolved
+ * </ul>
+ *
+ * <p>Each child predicate as well as the parent contains the result of its evaluation.
+ */
+@AutoValue
+public abstract class PredicateResult {
+  abstract ImmutableList<PredicateResult> childPredicateResults();
+
+  /** We only set this field for leaf predicates. */
+  public abstract String predicateString();
+
+  /** true if the predicate is passing for a given change. */
+  public abstract boolean status();
+
+  /**
+   * An explanation of the predicate result.
+   *
+   * <p>This is used to provide more information about complex atoms, which may otherwise be opaque
+   * and hard to debug.
+   *
+   * <p>This will be empty for most predicate results and all non-leaf predicates.
+   */
+  public abstract String explanation();
+
+  /** Returns a list of leaf predicate results whose {@link PredicateResult#status()} is true. */
+  public ImmutableList<String> getPassingAtoms() {
+    return getAtoms(/* status= */ true).stream()
+        .map(PredicateResult::predicateString)
+        .collect(ImmutableList.toImmutableList());
+  }
+
+  /** Returns a list of leaf predicate results whose {@link PredicateResult#status()} is false. */
+  public ImmutableList<String> getFailingAtoms() {
+    return getAtoms(/* status= */ false).stream()
+        .map(PredicateResult::predicateString)
+        .collect(ImmutableList.toImmutableList());
+  }
+
+  public ImmutableMap<String, String> getAtomExplanations() {
+    return getAtoms().stream()
+        .collect(
+            ImmutableMap.toImmutableMap(
+                PredicateResult::predicateString, PredicateResult::explanation, (a, b) -> a));
+  }
+
+  /** Returns the list of leaf {@link PredicateResult}. */
+  private ImmutableList<PredicateResult> getAtoms() {
+    ImmutableList.Builder<PredicateResult> atomsList = ImmutableList.builder();
+    getAtomsRecursively(atomsList);
+    return atomsList.build();
+  }
+
+  /**
+   * Returns the list of leaf {@link PredicateResult} whose {@link #status()} is equal to the {@code
+   * status} parameter.
+   */
+  private ImmutableList<PredicateResult> getAtoms(boolean status) {
+    ImmutableList.Builder<PredicateResult> atomsList = ImmutableList.builder();
+    getAtomsRecursively(atomsList, status);
+    return atomsList.build();
+  }
+
+  private void getAtomsRecursively(ImmutableList.Builder<PredicateResult> list) {
+    if (!predicateString().isEmpty()) {
+      list.add(this);
+      return;
+    }
+    childPredicateResults().forEach(c -> c.getAtomsRecursively(list));
+  }
+
+  private void getAtomsRecursively(ImmutableList.Builder<PredicateResult> list, boolean status) {
+    if (!predicateString().isEmpty() && status() == status) {
+      list.add(this);
+      return;
+    }
+    childPredicateResults().forEach(c -> c.getAtomsRecursively(list, status));
+  }
+
+  public static Builder builder() {
+    return new AutoValue_PredicateResult.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    protected abstract ImmutableList.Builder<PredicateResult> childPredicateResultsBuilder();
+
+    public abstract Builder predicateString(String value);
+
+    public abstract Builder status(boolean value);
+
+    public abstract Builder explanation(String value);
+
+    @CanIgnoreReturnValue
+    public Builder addChildPredicateResult(PredicateResult result) {
+      childPredicateResultsBuilder().add(result);
+      return this;
+    }
+
+    public abstract PredicateResult build();
+  }
+}
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 7b02597..45c3bc0 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.entities;
 
-import static java.util.Objects.requireNonNull;
-
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableMap;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -41,63 +39,64 @@
   public static final SubmitType DEFAULT_ALL_PROJECTS_SUBMIT_TYPE = SubmitType.MERGE_IF_NECESSARY;
 
   public static NameKey nameKey(String name) {
-    return new NameKey(name);
+    return new GeneralProjectName(name);
   }
 
   /**
    * Project name key.
    *
-   * <p>This class has subclasses such as {@code AllProjectsName}, which make Guice injection more
-   * convenient. Subclasses must compare equal if they have the same name, regardless of the
-   * specific class. This implies that subclasses may not add additional fields.
+   * <p>This interface has a few implementations. Callers should normally only interact with the
+   * interface directly, and create an instance using {@link #nameKey}.
    *
-   * <p>Because of this unusual subclassing behavior, this class is not an {@code @AutoValue},
-   * unlike other key types in this package. However, this is strictly an implementation detail; its
-   * interface and semantics are otherwise analogous to the {@code @AutoValue} types.
+   * <p>The main implementation is {@code GeneralProjectName}, which supports any project name.
    *
-   * <p>This class is immutable and thread safe.
+   * <p>Other implementations are specific for reserved repo names, such as {@code AllProjectsName}.
+   * Having them as separate types makes the Guice injection more convenient. Implementors must
+   * compare equal if they have the same name, regardless of the specific class. This implies that
+   * implementors may not add additional fields besides `name`, and that they must override {@code
+   * equals} and {@code hashCode} to use the {@link #projectNameEquals} and {@link
+   * #projectNameHashCode} methods provided by this interface. All implementors must be immutable
+   * and ThreadSafe, please use {@code record} for new implementors.
+   *
+   * <p>Why was it implemented this way? We needed the implementations to be distinguished record
+   * types. As records do not support inheritance, the cleanest way to share the common behavior
+   * between them was this interface. Interfaces cannot override {@link Object}methods, and
+   * therefore the implementors must wrap these on their own.
    */
   @Immutable
   @ConvertibleToProto
-  public static class NameKey implements Serializable, Comparable<NameKey> {
-    private static final long serialVersionUID = 1L;
+  public interface NameKey extends Serializable, Comparable<NameKey> {
+    long serialVersionUID = 1L;
 
     /** Parse a Project.NameKey out of a string representation. */
-    public static NameKey parse(String str) {
+    static NameKey parse(String str) {
       return nameKey(ProjectUtil.sanitizeProjectName(KeyUtil.decode(str)));
     }
 
-    private final String name;
+    String name();
 
-    protected NameKey(String name) {
-      this.name = requireNonNull(name);
+    default String get() {
+      return name();
     }
 
-    public String get() {
-      return name;
+    default int projectNameHashCode() {
+      return name().hashCode();
     }
 
-    @Override
-    public final int hashCode() {
-      return name.hashCode();
-    }
-
-    @Override
-    public final boolean equals(Object b) {
+    default boolean projectNameEquals(Object b) {
       if (b instanceof NameKey) {
-        return name.equals(((NameKey) b).get());
+        return name().equals(((NameKey) b).get());
       }
       return false;
     }
 
-    @Override
-    public final int compareTo(NameKey o) {
-      return name.compareTo(o.get());
+    default String projectNameToString() {
+      return KeyUtil.encode(name());
     }
 
     @Override
-    public final String toString() {
-      return KeyUtil.encode(name);
+    default int compareTo(NameKey o) {
+      return name().compareTo(o.get());
     }
   }
 
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index e79c530..c4d8da9 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -164,11 +164,6 @@
     return shard(id.get(), r).append('/').toString();
   }
 
-  public static String robotCommentsRef(Change.Id id) {
-    StringBuilder r = newStringBuilder().append(REFS_CHANGES);
-    return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString();
-  }
-
   public static boolean isNoteDbMetaRef(String ref) {
     if (ref.startsWith(REFS_CHANGES)
         && (ref.endsWith(META_SUFFIX) || ref.endsWith(ROBOT_COMMENTS_SUFFIX))) {
diff --git a/java/com/google/gerrit/entities/RobotComment.java b/java/com/google/gerrit/entities/RobotComment.java
deleted file mode 100644
index a4288ca..0000000
--- a/java/com/google/gerrit/entities/RobotComment.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.entities;
-
-import java.time.Instant;
-import java.util.Map;
-import java.util.Objects;
-
-@Deprecated
-public final class RobotComment extends Comment {
-  public String robotId;
-  public String robotRunId;
-  public String url;
-  public Map<String, String> properties;
-
-  public RobotComment(
-      Key key,
-      Account.Id author,
-      Instant writtenOn,
-      short side,
-      String message,
-      String serverId,
-      String robotId,
-      String robotRunId) {
-    super(key, author, writtenOn, side, message, serverId);
-    this.robotId = robotId;
-    this.robotRunId = robotRunId;
-  }
-
-  @Override
-  public int getApproximateSize() {
-    int approximateSize =
-        super.getCommentFieldApproximateSize() + nullableLength(robotId, robotRunId, url);
-    approximateSize +=
-        properties != null
-            ? properties.entrySet().stream()
-                .mapToInt(entry -> nullableLength(entry.getKey(), entry.getValue()))
-                .sum()
-            : 0;
-    approximateSize +=
-        fixSuggestions != null
-            ? fixSuggestions.stream().mapToInt(FixSuggestion::getApproximateSize).sum()
-            : 0;
-    return approximateSize;
-  }
-
-  @Override
-  public String toString() {
-    return toStringHelper()
-        .add("robotId", robotId)
-        .add("robotRunId", robotRunId)
-        .add("url", url)
-        .add("properties", Objects.toString(properties, ""))
-        .toString();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof RobotComment)) {
-      return false;
-    }
-    RobotComment c = (RobotComment) o;
-    return super.equals(o)
-        && Objects.equals(robotId, c.robotId)
-        && Objects.equals(robotRunId, c.robotRunId)
-        && Objects.equals(url, c.url)
-        && Objects.equals(properties, c.properties);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(super.hashCode(), robotId, robotRunId, url, properties);
-  }
-}
diff --git a/java/com/google/gerrit/entities/SubmitRequirement.java b/java/com/google/gerrit/entities/SubmitRequirement.java
index 523b993..cc2ea55 100644
--- a/java/com/google/gerrit/entities/SubmitRequirement.java
+++ b/java/com/google/gerrit/entities/SubmitRequirement.java
@@ -77,10 +77,12 @@
    */
   public abstract boolean allowOverrideInChildProjects();
 
-  public static SubmitRequirement.Builder builder() {
+  public static Builder builder() {
     return new AutoValue_SubmitRequirement.Builder();
   }
 
+  public abstract Builder toBuilder();
+
   public static TypeAdapter<SubmitRequirement> typeAdapter(Gson gson) {
     return new AutoValue_SubmitRequirement.GsonTypeAdapter(gson);
   }
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
index 0bdae29..8bf50f8 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -17,7 +17,6 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gson.Gson;
 import com.google.gson.TypeAdapter;
 import java.util.Optional;
@@ -162,116 +161,4 @@
     /** Submit requirement expression was not evaluated. */
     NOT_EVALUATED
   }
-
-  /**
-   * Entity detailing the result of evaluating a predicate.
-   *
-   * <p>Example - branch:refs/heads/foo and has:unresolved
-   *
-   * <p>The above predicate is an "And" predicate having two child predicates:
-   *
-   * <ul>
-   *   <li>branch:refs/heads/foo
-   *   <li>has:unresolved
-   * </ul>
-   *
-   * <p>Each child predicate as well as the parent contains the result of its evaluation.
-   */
-  @AutoValue
-  public abstract static class PredicateResult {
-    abstract ImmutableList<PredicateResult> childPredicateResults();
-
-    /** We only set this field for leaf predicates. */
-    public abstract String predicateString();
-
-    /** true if the predicate is passing for a given change. */
-    abstract boolean status();
-
-    /**
-     * An explanation of the predicate result.
-     *
-     * <p>This is used to provide more information about complex atoms, which may otherwise be
-     * opaque and hard to debug.
-     *
-     * <p>This will be empty for most predicate results and all non-leaf predicates.
-     */
-    public abstract String explanation();
-
-    /** Returns a list of leaf predicate results whose {@link PredicateResult#status()} is true. */
-    ImmutableList<String> getPassingAtoms() {
-      return getAtoms(/* status= */ true).stream()
-          .map(PredicateResult::predicateString)
-          .collect(ImmutableList.toImmutableList());
-    }
-
-    /** Returns a list of leaf predicate results whose {@link PredicateResult#status()} is false. */
-    ImmutableList<String> getFailingAtoms() {
-      return getAtoms(/* status= */ false).stream()
-          .map(PredicateResult::predicateString)
-          .collect(ImmutableList.toImmutableList());
-    }
-
-    ImmutableMap<String, String> getAtomExplanations() {
-      return getAtoms().stream()
-          .collect(
-              ImmutableMap.toImmutableMap(
-                  PredicateResult::predicateString, PredicateResult::explanation, (a, b) -> a));
-    }
-
-    /** Returns the list of leaf {@link PredicateResult}. */
-    private ImmutableList<PredicateResult> getAtoms() {
-      ImmutableList.Builder<PredicateResult> atomsList = ImmutableList.builder();
-      getAtomsRecursively(atomsList);
-      return atomsList.build();
-    }
-
-    /**
-     * Returns the list of leaf {@link PredicateResult} whose {@link #status()} is equal to the
-     * {@code status} parameter.
-     */
-    private ImmutableList<PredicateResult> getAtoms(boolean status) {
-      ImmutableList.Builder<PredicateResult> atomsList = ImmutableList.builder();
-      getAtomsRecursively(atomsList, status);
-      return atomsList.build();
-    }
-
-    private void getAtomsRecursively(ImmutableList.Builder<PredicateResult> list) {
-      if (!predicateString().isEmpty()) {
-        list.add(this);
-        return;
-      }
-      childPredicateResults().forEach(c -> c.getAtomsRecursively(list));
-    }
-
-    private void getAtomsRecursively(ImmutableList.Builder<PredicateResult> list, boolean status) {
-      if (!predicateString().isEmpty() && status() == status) {
-        list.add(this);
-        return;
-      }
-      childPredicateResults().forEach(c -> c.getAtomsRecursively(list, status));
-    }
-
-    public static Builder builder() {
-      return new AutoValue_SubmitRequirementExpressionResult_PredicateResult.Builder();
-    }
-
-    @AutoValue.Builder
-    public abstract static class Builder {
-      protected abstract ImmutableList.Builder<PredicateResult> childPredicateResultsBuilder();
-
-      public abstract Builder predicateString(String value);
-
-      public abstract Builder status(boolean value);
-
-      public abstract Builder explanation(String value);
-
-      @CanIgnoreReturnValue
-      public Builder addChildPredicateResult(PredicateResult result) {
-        childPredicateResultsBuilder().add(result);
-        return this;
-      }
-
-      public abstract PredicateResult build();
-    }
-  }
 }
diff --git a/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java b/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
index c073a5f..15c18ba 100644
--- a/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/AccountInputProtoConverter.java
@@ -18,8 +18,10 @@
 
 import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
+import java.util.stream.Collectors;
 
 /**
  * Proto converter between {@link AccountInput} and {@link
@@ -30,6 +32,9 @@
     implements ProtoConverter<Entities.AccountInput, AccountInput> {
   INSTANCE;
 
+  private final ProtoConverter<Entities.AuthTokenInput, AuthTokenInput> tokenInputConverter =
+      TokenInputProtoConverter.INSTANCE;
+
   @Override
   public Entities.AccountInput toProto(AccountInput accountInput) {
     Entities.AccountInput.Builder builder = Entities.AccountInput.newBuilder();
@@ -54,6 +59,12 @@
     if (accountInput.groups != null) {
       builder.addAllGroups(accountInput.groups);
     }
+    if (accountInput.tokens != null) {
+      builder.addAllTokens(
+          accountInput.tokens.stream()
+              .map(tokenInputConverter::toProto)
+              .collect(Collectors.toList()));
+    }
 
     return builder.build();
   }
@@ -82,6 +93,12 @@
     if (proto.getGroupsCount() > 0) {
       accountInput.groups = proto.getGroupsList();
     }
+    if (proto.getTokensCount() > 0) {
+      accountInput.tokens =
+          proto.getTokensList().stream()
+              .map(tokenInputConverter::fromProto)
+              .collect(Collectors.toList());
+    }
     return accountInput;
   }
 
diff --git a/java/com/google/gerrit/entities/converter/BUILD b/java/com/google/gerrit/entities/converter/BUILD
new file mode 100644
index 0000000..bb01b30
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/BUILD
@@ -0,0 +1,46 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "converters",
+    srcs = glob(
+        ["**/*.java"],
+        exclude = [
+            "ProtoConverter.java",
+            "SafeProtoConverter.java",
+        ],
+    ),
+    deps = [
+        ":proto_converter",
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/extensions:api",
+        "//lib:gson",
+        "//lib:guava",
+        "//lib:jgit",
+        "//lib:protobuf",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/auto:auto-value-gson",
+        "//lib/errorprone:annotations",
+        "//lib/flogger:api",
+        "//proto:cache_java_proto",
+        "//proto:entities_java_proto",
+    ],
+)
+
+java_library(
+    name = "proto_converter",
+    srcs = [
+        "ProtoConverter.java",
+        "SafeProtoConverter.java",
+    ],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//lib:protobuf",
+        "//lib/errorprone:annotations",
+    ],
+)
diff --git a/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java b/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java
index c7dd9e2..298fe5b 100644
--- a/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java
@@ -16,6 +16,7 @@
 
 import com.google.errorprone.annotations.Immutable;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
 import java.util.Optional;
@@ -32,20 +33,35 @@
   @Override
   public Entities.Conflicts toProto(PatchSet.Conflicts conflicts) {
     Entities.Conflicts.Builder builder = Entities.Conflicts.newBuilder();
+    conflicts.base().ifPresent(base -> builder.setBase(objectIdConverter.toProto(base)));
     conflicts.ours().ifPresent(ours -> builder.setOurs(objectIdConverter.toProto(ours)));
     conflicts.theirs().ifPresent(theirs -> builder.setTheirs(objectIdConverter.toProto(theirs)));
+    conflicts.mergeStrategy().ifPresent(mergeStrategy -> builder.setMergeStrategy(mergeStrategy));
+    conflicts
+        .noBaseReason()
+        .ifPresent(
+            noBaseReason ->
+                builder.setNoBaseReason(
+                    Entities.NoMergeBaseReason.forNumber(noBaseReason.getValue())));
     return builder.setContainsConflicts(conflicts.containsConflicts()).build();
   }
 
   @Override
   public PatchSet.Conflicts fromProto(Entities.Conflicts proto) {
     return PatchSet.Conflicts.create(
+        proto.hasBase()
+            ? Optional.of(objectIdConverter.fromProto(proto.getBase()))
+            : Optional.empty(),
         proto.hasOurs()
             ? Optional.of(objectIdConverter.fromProto(proto.getOurs()))
             : Optional.empty(),
         proto.hasTheirs()
             ? Optional.of(objectIdConverter.fromProto(proto.getTheirs()))
             : Optional.empty(),
+        proto.hasMergeStrategy() ? Optional.of(proto.getMergeStrategy()) : Optional.empty(),
+        proto.hasNoBaseReason()
+            ? Optional.of(NoMergeBaseReason.valueOf(proto.getNoBaseReason().name()))
+            : Optional.empty(),
         proto.hasContainsConflicts() ? proto.getContainsConflicts() : false);
   }
 
diff --git a/java/com/google/gerrit/entities/converter/GeneralProjectNameConverter.java b/java/com/google/gerrit/entities/converter/GeneralProjectNameConverter.java
new file mode 100644
index 0000000..0dd49f6
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/GeneralProjectNameConverter.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.entities.converter;
+
+import com.google.gerrit.entities.GeneralProjectName;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.Project_NameKey;
+import com.google.protobuf.Parser;
+
+public enum GeneralProjectNameConverter
+    implements SafeProtoConverter<Entities.Project_NameKey, GeneralProjectName> {
+  INSTANCE;
+
+  @Override
+  public Project_NameKey toProto(GeneralProjectName nameKey) {
+    return Entities.Project_NameKey.newBuilder().setName(nameKey.get()).build();
+  }
+
+  @Override
+  public GeneralProjectName fromProto(Project_NameKey proto) {
+    return new GeneralProjectName(proto.getName());
+  }
+
+  @Override
+  public Class<Project_NameKey> getProtoClass() {
+    return Project_NameKey.class;
+  }
+
+  @Override
+  public Class<GeneralProjectName> getEntityClass() {
+    return GeneralProjectName.class;
+  }
+
+  @Override
+  public Parser<Project_NameKey> getParser() {
+    return Project_NameKey.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
index 320b8fc..47d624a 100644
--- a/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
@@ -15,39 +15,28 @@
 package com.google.gerrit.entities.converter;
 
 import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.GeneralProjectName;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.proto.Entities.Project_NameKey;
 import com.google.protobuf.Parser;
 
 @Immutable
 public enum ProjectNameKeyProtoConverter
-    implements SafeProtoConverter<Entities.Project_NameKey, Project.NameKey> {
+    implements ProtoConverter<Entities.Project_NameKey, Project.NameKey> {
   INSTANCE;
 
   @Override
   public Entities.Project_NameKey toProto(Project.NameKey nameKey) {
-    return Entities.Project_NameKey.newBuilder().setName(nameKey.get()).build();
+    return GeneralProjectNameConverter.INSTANCE.toProto(new GeneralProjectName(nameKey));
   }
 
   @Override
-  public Project.NameKey fromProto(Entities.Project_NameKey proto) {
-    return Project.nameKey(proto.getName());
+  public GeneralProjectName fromProto(Entities.Project_NameKey proto) {
+    return GeneralProjectNameConverter.INSTANCE.fromProto(proto);
   }
 
   @Override
   public Parser<Entities.Project_NameKey> getParser() {
     return Entities.Project_NameKey.parser();
   }
-
-  @Override
-  public Class<Project_NameKey> getProtoClass() {
-    return Project_NameKey.class;
-  }
-
-  @Override
-  public Class<NameKey> getEntityClass() {
-    return NameKey.class;
-  }
 }
diff --git a/java/com/google/gerrit/entities/converter/TokenInputProtoConverter.java b/java/com/google/gerrit/entities/converter/TokenInputProtoConverter.java
new file mode 100644
index 0000000..713c322
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/TokenInputProtoConverter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gerrit.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+
+/**
+ * Proto converter between {@link AuthTokenInput} and {@link
+ * com.google.gerrit.proto.Entities.AuthTokenInput}.
+ */
+@Immutable
+public enum TokenInputProtoConverter
+    implements ProtoConverter<Entities.AuthTokenInput, AuthTokenInput> {
+  INSTANCE;
+
+  @Override
+  public Entities.AuthTokenInput toProto(AuthTokenInput tokenInput) {
+    Entities.AuthTokenInput.Builder builder = Entities.AuthTokenInput.newBuilder();
+    if (tokenInput.id != null) {
+      builder.setId(tokenInput.id);
+    }
+    if (tokenInput.token != null) {
+      builder.setToken(tokenInput.token);
+    }
+
+    return builder.build();
+  }
+
+  @Override
+  public AuthTokenInput fromProto(Entities.AuthTokenInput proto) {
+    AuthTokenInput tokenInput = new AuthTokenInput();
+    if (proto.hasId()) {
+      tokenInput.id = proto.getId();
+    }
+    if (proto.hasToken()) {
+      tokenInput.token = proto.getToken();
+    }
+
+    return tokenInput;
+  }
+
+  @Override
+  public Parser<Entities.AuthTokenInput> getParser() {
+    return Entities.AuthTokenInput.parser();
+  }
+}
diff --git a/java/com/google/gerrit/exceptions/GerritNoMergeBaseException.java b/java/com/google/gerrit/exceptions/GerritNoMergeBaseException.java
new file mode 100644
index 0000000..c12ba10
--- /dev/null
+++ b/java/com/google/gerrit/exceptions/GerritNoMergeBaseException.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.exceptions;
+
+/**
+ * Exception that represents a NoMergeBaseException from JGit that should be mapped to a user error
+ * (e.g. 4xx), because it is triggered by the repository state or user input (e.g. the specified
+ * merge strategy).
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ *   <li>There are multiple merge bases and the merge strategy doesn't support multiple merge bases.
+ *   <li>There are conflicts when merging the merge bases and the merge strategy doesn't support
+ *       merging bases when there are conflicts.
+ *   <li>The are too many merge bases.
+ * </ul>
+ *
+ * <p>If this exception happens the intended merge is not support. The caller may retry using a
+ * different merge strategy.
+ */
+public class GerritNoMergeBaseException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public GerritNoMergeBaseException(
+      org.eclipse.jgit.errors.NoMergeBaseException jgitNoMergeBaseException) {
+    super(
+        String.format("Cannot create merge commit: %s", jgitNoMergeBaseException.getMessage()),
+        jgitNoMergeBaseException.getCause());
+  }
+}
diff --git a/java/com/google/gerrit/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index 762df80..1258a13 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -33,6 +33,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
+        "//lib:servlet-api",
         "//lib/auto:auto-value-annotations",
         "//lib/errorprone:annotations",
         "//lib/guice",
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index a68307a..2d260ea 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.extensions.api.accounts;
 
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -118,11 +120,17 @@
 
   void setName(String name) throws RestApiException;
 
+  @CanIgnoreReturnValue
+  AuthTokenInfo createToken(AuthTokenInput input) throws RestApiException;
+
+  List<AuthTokenInfo> getTokens() throws RestApiException;
+
   /**
    * Generate a new HTTP password.
    *
    * @return the generated password.
    */
+  @Deprecated
   String generateHttpPassword() throws RestApiException;
 
   /**
@@ -134,6 +142,7 @@
    * @return the new password, {@code null} if the password was removed.
    */
   @CanIgnoreReturnValue
+  @Deprecated
   String setHttpPassword(String httpPassword) throws RestApiException;
 
   void delete() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountInput.java b/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
index 2bcce30..675c8c9 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import java.util.List;
 
@@ -23,6 +24,7 @@
   public String displayName;
   public String email;
   public String sshKey;
-  public String httpPassword;
+  @Deprecated public String httpPassword;
+  public List<AuthTokenInput> tokens;
   public List<String> groups;
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index bfb9d7b..7af343a 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -28,11 +28,14 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitMessageInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
+import com.google.gerrit.extensions.common.EvaluateChangeQueryExpressionResultInfo;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.extensions.common.IsFlowsEnabledInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RebaseChainInfo;
 import com.google.gerrit.extensions.common.RevertSubmissionInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInput;
 import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -94,6 +97,20 @@
    */
   ReviewerApi reviewer(String id) throws RestApiException;
 
+  /** Creates a new flow on the change. */
+  @CanIgnoreReturnValue
+  FlowInfo createFlow(FlowInput flowInput) throws RestApiException;
+
+  /** Look up a flow of this change by its UUID. */
+  FlowApi flow(String flowUuid) throws RestApiException;
+
+  IsFlowsEnabledInfo isFlowsEnabled() throws RestApiException;
+
+  /** Get the flows of this change/ */
+  List<FlowInfo> flows() throws RestApiException;
+
+  EvaluateChangeQueryExpressionRequest evaluateChangeQueryExpression();
+
   default void abandon() throws RestApiException {
     abandon(new AbandonInput());
   }
@@ -413,14 +430,6 @@
   CommentsRequest commentsRequest() throws RestApiException;
 
   /**
-   * Get all robot comments on a change.
-   *
-   * @return robot comments in a map keyed by path; robot comments have the {@code revision} field
-   *     set to indicate their patch set.
-   */
-  Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException;
-
-  /**
    * Get all draft comments for the current user on a change.
    *
    * @return drafts in a map keyed by path; comments have the {@code revision} field set to indicate
@@ -606,4 +615,33 @@
       return reviewerState;
     }
   }
+
+  abstract class EvaluateChangeQueryExpressionRequest {
+    private String expression;
+    private boolean useIndex;
+
+    public abstract EvaluateChangeQueryExpressionResultInfo get() throws RestApiException;
+
+    public EvaluateChangeQueryExpressionRequest withExpression(String expression) {
+      this.expression = expression;
+      return this;
+    }
+
+    public EvaluateChangeQueryExpressionRequest useIndex() {
+      return useIndex(true);
+    }
+
+    public EvaluateChangeQueryExpressionRequest useIndex(boolean useIndex) {
+      this.useIndex = useIndex;
+      return this;
+    }
+
+    public String getExpression() {
+      return expression;
+    }
+
+    public boolean getUseIndex() {
+      return useIndex;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeIdentifier.java b/java/com/google/gerrit/extensions/api/changes/ChangeIdentifier.java
new file mode 100644
index 0000000..bd8a80c
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeIdentifier.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Identifier for a change to be used in the {@link Changes} API to get the {@link ChangeApi} for a
+ * change.
+ *
+ * <p>Supports any change identifier that is supported by the REST API, including {@code
+ * <project>~<changeNumber>}, {@code <changeId>} (I-hash), {@code <project>~<branch>~<changeId>}
+ * (aka as triplet) and {@code <changeNumber>} (numeric change ID).
+ */
+@AutoValue
+public abstract class ChangeIdentifier {
+  public abstract String id();
+
+  @Override
+  public final String toString() {
+    return id();
+  }
+
+  /**
+   * Creates a change identifier in the format {@code <project>~<changeNumber>}.
+   *
+   * <p>Always uniquely identifies one change.
+   *
+   * <p>This is the preferred identifier for identifying changes.
+   *
+   * @param projectName the name of the project that contains the change
+   * @param numericChangeId the numeric change ID (aka as change number)
+   */
+  public static ChangeIdentifier byProjectAndNumericChangeId(
+      String projectName, int numericChangeId) {
+    return new AutoValue_ChangeIdentifier(projectName + "~" + numericChangeId);
+  }
+
+  /**
+   * Creates a change identifier in the format {@code <project>~<branch>~<changeId>}.
+   *
+   * <p>Always uniquely identifies one change.
+   *
+   * @param projectName the name of the project that contains the change
+   * @param branchName the name of the branch that contains the change, may be the short branch name
+   *     without the {@code refs/heads/} prefix.
+   * @param changeId the I-hash of the change
+   */
+  public static ChangeIdentifier byTriplet(String projectName, String branchName, String changeId) {
+    checkState(changeId.startsWith("I"), "expected changeId to be the I-hash");
+    return new AutoValue_ChangeIdentifier(projectName + "~" + branchName + "~" + changeId);
+  }
+
+  /**
+   * Creates a change identifier in the format {@code <changeId>}.
+   *
+   * <p>May not always identify a single change (e.g. it matches multiple changes if a change has
+   * been cherry-picked).
+   *
+   * @param changeId the I-hash of the change
+   */
+  public static ChangeIdentifier byChangeId(String changeId) {
+    checkState(changeId.startsWith("I"), "expected changeId to be the I-hash");
+    return new AutoValue_ChangeIdentifier(changeId);
+  }
+
+  /**
+   * Creates a change identifier in the format {@code <changeNumber>}.
+   *
+   * <p>May not always identify a single change (e.g. when changes have been imported from another
+   * Gerrit instance).
+   *
+   * @param numericChangeId the numeric change ID (aka as change number)
+   */
+  public static ChangeIdentifier byNumericChangeId(int numericChangeId) {
+    return new AutoValue_ChangeIdentifier(Integer.valueOf(numericChangeId).toString());
+  }
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/Changes.java b/java/com/google/gerrit/extensions/api/changes/Changes.java
index bd838fd..f3b7e0c 100644
--- a/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -76,6 +76,18 @@
    */
   ChangeApi id(String project, int id) throws RestApiException;
 
+  /**
+   * Look up a change by the given change identifier.
+   *
+   * @see #id(String)
+   * @param changeIdentifier identifier that identifies a change
+   * @return API for accessing the change.
+   * @throws RestApiException if an error occurred.
+   */
+  default ChangeApi id(ChangeIdentifier changeIdentifier) throws RestApiException {
+    return id(changeIdentifier.id());
+  }
+
   @CanIgnoreReturnValue
   ChangeApi create(ChangeInput in) throws RestApiException;
 
diff --git a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
index 6349595..81131c8 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileContentInput.java
@@ -20,6 +20,25 @@
 /** Content to be added to a file (new or existing) via change edit. */
 public class FileContentInput {
   @DefaultInput public RawInput content;
-  public String binary_content;
+
+  /**
+   * The file content as a base-64 encoded data URI.
+   *
+   * <p>If no content is provided, an empty is created or if an existing file is updated the file
+   * content is removed so that the file becomes empty.
+   *
+   * <p>The content must be a SHA1 if the file mode is {@code 160000} (gitlink).
+   */
+  public String binaryContent;
+
+  /**
+   * The file mode in octal format.
+   *
+   * <p>Supported values are {@code 100644} (regular file), {@code 100755} (executable file), {@code
+   * 120000} (symlink) and {@code 160000} (gitlink).
+   *
+   * <p>If unset, new files are created with file mode {@code 100644} (regular file) and for
+   * existing files the existing file mode is kept.
+   */
   public Integer fileMode;
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java b/java/com/google/gerrit/extensions/api/changes/FlowApi.java
similarity index 68%
rename from java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
rename to java/com/google/gerrit/extensions/api/changes/FlowApi.java
index 8ff4e95..ca3612b 100644
--- a/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/FlowApi.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2016 The Android Open Source Project
+// Copyright (C) 2025 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,9 +14,14 @@
 
 package com.google.gerrit.extensions.api.changes;
 
-import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.common.FlowInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
-public interface RobotCommentApi {
-  RobotCommentInfo get() throws RestApiException;
+/** API to call REST endpoints on flows. */
+public interface FlowApi {
+  /** Gets the flow. */
+  FlowInfo get() throws RestApiException;
+
+  /** Deletes a flow. */
+  void delete() throws RestApiException;
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/RelatedChangeAndCommitInfo.java b/java/com/google/gerrit/extensions/api/changes/RelatedChangeAndCommitInfo.java
index 740caa8..cecd73d 100644
--- a/java/com/google/gerrit/extensions/api/changes/RelatedChangeAndCommitInfo.java
+++ b/java/com/google/gerrit/extensions/api/changes/RelatedChangeAndCommitInfo.java
@@ -26,6 +26,7 @@
   public Integer _currentRevisionNumber;
   public String status;
   public Boolean submittable;
+  public Boolean workInProgress;
 
   public RelatedChangeAndCommitInfo() {}
 
@@ -40,6 +41,7 @@
         .add("_currentRevisionNumber", _currentRevisionNumber)
         .add("status", status)
         .add("submittable", submittable)
+        .add("workInProgress", workInProgress)
         .toString();
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 2584448..4f495a7 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -38,7 +38,6 @@
 
   public Map<String, Short> labels;
   public Map<String, List<CommentInput>> comments;
-  @Deprecated public Map<String, List<RobotCommentInput>> robotComments;
 
   /**
    * How to process draft comments already in the database that were not also described in this
@@ -114,14 +113,6 @@
     public Boolean unresolved;
   }
 
-  @Deprecated
-  public static class RobotCommentInput extends Comment {
-    public String robotId;
-    public String robotRunId;
-    public String url;
-    public Map<String, String> properties;
-  }
-
   @CanIgnoreReturnValue
   public ReviewInput message(String msg) {
     message = msg != null && !msg.isEmpty() ? msg : null;
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index a1a5444..a2f5ed1 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -100,16 +99,12 @@
 
   Map<String, List<CommentInfo>> comments() throws RestApiException;
 
-  Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException;
-
   Map<String, List<CommentInfo>> drafts() throws RestApiException;
 
   List<CommentInfo> commentsAsList() throws RestApiException;
 
   List<CommentInfo> draftsAsList() throws RestApiException;
 
-  List<RobotCommentInfo> robotCommentsAsList() throws RestApiException;
-
   Map<String, List<CommentInfo>> portedComments() throws RestApiException;
 
   Map<String, List<CommentInfo>> portedDrafts() throws RestApiException;
@@ -148,10 +143,6 @@
 
   CommentApi comment(String id) throws RestApiException;
 
-  RobotCommentApi robotComment(String id) throws RestApiException;
-
-  String etag() throws RestApiException;
-
   /** Returns patch of revision. */
   BinaryResult patch() throws RestApiException;
 
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index 1b53610..79e76ea 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.config;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -21,7 +22,9 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.CacheInfo;
 import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.TopMenu;
 import java.util.List;
@@ -56,6 +59,10 @@
 
   ListExperimentsRequest listExperiments() throws RestApiException;
 
+  ListGlobalLabelsRequest listGlobalLabels() throws RestApiException;
+
+  ListGlobalSubmitRequirementsRequest listGlobalSubmitRequirements() throws RestApiException;
+
   CachesApi caches(String name) throws RestApiException;
 
   Map<String, CacheInfo> listCaches() throws RestApiException;
@@ -74,4 +81,12 @@
       return enabledOnly;
     }
   }
+
+  abstract class ListGlobalLabelsRequest {
+    public abstract ImmutableList<LabelDefinitionInfo> get() throws RestApiException;
+  }
+
+  abstract class ListGlobalSubmitRequirementsRequest {
+    public abstract ImmutableList<SubmitRequirementInfo> get() throws RestApiException;
+  }
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/BranchInput.java b/java/com/google/gerrit/extensions/api/projects/BranchInput.java
index 1ccbd4f..b11b588 100644
--- a/java/com/google/gerrit/extensions/api/projects/BranchInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/BranchInput.java
@@ -17,9 +17,44 @@
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import java.util.Map;
 
+/**
+ * Input for creating a new branch.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#branch-input
+ *
+ * <p>This class holds the data needed to create a new branch, such as the revision to base the
+ * branch on and whether to create an empty commit.
+ *
+ * <p>The following properties can be set:
+ *
+ * <ul>
+ *   <li>{@code revision}: The base revision of the new branch. If not set and create_empty_commit
+ *       is true the branch is created with an empty initial commit. If not set and
+ *       create_empty_commit is false or unset HEAD will be used as base revision.
+ *   <li>{@code createEmptyCommit}: Whether the branch should be created with an empty initial
+ *       commit. It is not possible to create a branch on a project with no commits. Cannot be used
+ *       in combination with setting a revision. Can be used to review the initial content of a
+ *       branch (create the branch with an empty initial commit, make a second commit with the
+ *       initial content, e.g. by merging in another branch, and push the commit for review)..
+ *   <li>{@code ref}: The name of the branch. The prefix refs/heads/ can be omitted. If set, must
+ *       match the branch ID in the URL.
+ *   <li>{@code sourceRef}: The full name of the source ref where {@code revision} can be found.
+ *       This ref should be visible to the caller. Used when {@code revision} is not a ref name in
+ *       order to check reachability from a specific ref. If not set, then all visible refs under
+ *       refs/heads/ and refs/tags/ are searched (see {@code CreateRefControl#checkCreateCommit} for
+ *       details).
+ *   <li>{@code validationOptions}: Map with key-value pairs that are forwarded as options to the
+ *       ref operation validation listeners (e.g. can be used to skip certain validations). Which
+ *       validation options are supported depends on the installed ref operation validation
+ *       listeners. Gerrit core doesn’t support any validation options, but ref operation validation
+ *       listeners that are implemented in plugins may. Please refer to the documentation of the
+ *       installed plugins to learn whether they support validation options. Unknown validation
+ *       options are silently ignored.
+ * </ul>
+ */
 public class BranchInput {
   @DefaultInput public String revision;
   public boolean createEmptyCommit;
   public String ref;
+  public String sourceRef;
   public Map<String, String> validationOptions;
 }
diff --git a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java b/java/com/google/gerrit/extensions/api/projects/DeleteChangesInput.java
similarity index 73%
copy from java/com/google/gerrit/pgm/init/index/lucene/package-info.java
copy to java/com/google/gerrit/extensions/api/projects/DeleteChangesInput.java
index e72368b..2c90926 100644
--- a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java
+++ b/java/com/google/gerrit/extensions/api/projects/DeleteChangesInput.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2025 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-@CheckReturnValue
-package com.google.gerrit.pgm.init.index.lucene;
+package com.google.gerrit.extensions.api.projects;
 
-import com.google.errorprone.annotations.CheckReturnValue;
+import java.util.List;
+
+public class DeleteChangesInput {
+  public List<String> changes;
+}
diff --git a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java b/java/com/google/gerrit/extensions/api/projects/DeleteChangesResult.java
similarity index 75%
copy from java/com/google/gerrit/pgm/init/index/lucene/package-info.java
copy to java/com/google/gerrit/extensions/api/projects/DeleteChangesResult.java
index e72368b..c70d828 100644
--- a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java
+++ b/java/com/google/gerrit/extensions/api/projects/DeleteChangesResult.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2025 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-@CheckReturnValue
-package com.google.gerrit.pgm.init.index.lucene;
+package com.google.gerrit.extensions.api.projects;
 
-import com.google.errorprone.annotations.CheckReturnValue;
+public enum DeleteChangesResult {
+  NOT_UNIQUE,
+  SUCCESS,
+  FAILURE
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 787603f..f96755a 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -48,6 +48,7 @@
   @CanIgnoreReturnValue
   ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException;
 
+  @CanIgnoreReturnValue
   ChangeInfo accessChange(ProjectAccessInput p) throws RestApiException;
 
   AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException;
@@ -71,6 +72,9 @@
 
   void deleteBranches(DeleteBranchesInput in) throws RestApiException;
 
+  Map<DeleteChangesResult, Collection<String>> deleteChanges(DeleteChangesInput in)
+      throws RestApiException;
+
   void deleteTags(DeleteTagsInput in) throws RestApiException;
 
   abstract class ListRefsRequest<T extends RefInfo> {
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
index e61d316..cabb745 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -38,4 +38,21 @@
   public InheritableBoolean requireSignedPush;
   public String maxObjectSizeLimit;
   public Map<String, Map<String, ConfigValue>> pluginConfigValues;
+
+  /**
+   * If set, only the project initialization is being (re-)done and the repository creation is
+   * skipped.
+   *
+   * <p>The project initialization consists out of setting {@code HEAD}, creating the {@code
+   * project.config} file in {@code refs/meta/config} and creating initial branches with empty
+   * commits.
+   *
+   * <p>This is useful to retry the project initialization after a create project request has failed
+   * and as a result the repository was created but the project initialization was not done.
+   *
+   * <p>Note, that this does not override any existing project configuration.
+   *
+   * <p>If a conflicting configuration already exists the request is rejected with `409 Conflict`.
+   */
+  public boolean initOnly;
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/TagInput.java b/java/com/google/gerrit/extensions/api/projects/TagInput.java
index 115bb88..b4f56d9 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagInput.java
@@ -15,9 +15,14 @@
 package com.google.gerrit.extensions.api.projects;
 
 import com.google.gerrit.extensions.restapi.DefaultInput;
+import java.sql.Timestamp;
 
 public class TagInput {
   @DefaultInput public String ref;
   public String revision;
   public String message;
+
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  public Timestamp date;
 }
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/extensions/auth/AuthTokenInfo.java
similarity index 67%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/extensions/auth/AuthTokenInfo.java
index 3ec74ba..5743da7 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/extensions/auth/AuthTokenInfo.java
@@ -12,9 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.extensions.auth;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+import com.google.gerrit.common.Nullable;
+import java.sql.Timestamp;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+public class AuthTokenInfo {
+  public String id;
+  /* Should only be set when the token is being created. */
+  @Nullable public String token;
+  @Nullable public Timestamp expiration;
+}
diff --git a/java/com/google/gerrit/extensions/auth/AuthTokenInput.java b/java/com/google/gerrit/extensions/auth/AuthTokenInput.java
new file mode 100644
index 0000000..c7e5f6e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/auth/AuthTokenInput.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.auth;
+
+import com.google.gerrit.common.ConvertibleToProto;
+import com.google.gerrit.common.Nullable;
+import java.util.Objects;
+
+@ConvertibleToProto
+public class AuthTokenInput {
+  @Nullable public String id;
+  @Nullable public String token;
+  @Nullable public String lifetime;
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(id, token, lifetime);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof AuthTokenInput)) {
+      return false;
+    }
+    AuthTokenInput other = (AuthTokenInput) obj;
+    return Objects.equals(id, other.id)
+        && Objects.equals(token, other.token)
+        && Objects.equals(lifetime, other.lifetime);
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/AccountInfo.java b/java/com/google/gerrit/extensions/common/AccountInfo.java
index 229ea5d..74d495c 100644
--- a/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -76,6 +76,9 @@
   /** Whether the account is inactive. */
   public Boolean inactive;
 
+  /** Whether the account is deleted. */
+  public Boolean deleted;
+
   /** Tags, such as whether this account is a service user. */
   public List<String> tags;
 
diff --git a/java/com/google/gerrit/extensions/common/AuthInfo.java b/java/com/google/gerrit/extensions/common/AuthInfo.java
index 3aa40fc..38b8b4e 100644
--- a/java/com/google/gerrit/extensions/common/AuthInfo.java
+++ b/java/com/google/gerrit/extensions/common/AuthInfo.java
@@ -119,4 +119,11 @@
    * <p>Only set if authentication type is {@code LDAP}, {@code LDAP_BIND} or {@code OAUTH}.
    */
   public GitBasicAuthPolicy gitBasicAuthPolicy;
+
+  /**
+   * The maximum lifetime allowed for authentication tokens in minutes.
+   *
+   * <p>The value of the {@code auth.maxAuthTokenLifetime} parameter in {@code gerrit.config}.
+   */
+  public long maxTokenLifetime;
 }
diff --git a/java/com/google/gerrit/extensions/common/AvatarInfo.java b/java/com/google/gerrit/extensions/common/AvatarInfo.java
index b620ac2..55ae08f 100644
--- a/java/com/google/gerrit/extensions/common/AvatarInfo.java
+++ b/java/com/google/gerrit/extensions/common/AvatarInfo.java
@@ -38,22 +38,17 @@
   /** The height of the avatar image in pixels. */
   public Integer height;
 
-  /** The width of the avatar image in pixels. */
-  public Integer width;
-
   @Override
   public boolean equals(Object o) {
     if (o instanceof AvatarInfo) {
       AvatarInfo avatarInfo = (AvatarInfo) o;
-      return Objects.equals(url, avatarInfo.url)
-          && Objects.equals(height, avatarInfo.height)
-          && Objects.equals(width, avatarInfo.width);
+      return Objects.equals(url, avatarInfo.url) && Objects.equals(height, avatarInfo.height);
     }
     return false;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(url, height, width);
+    return Objects.hash(url, height);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
index 80bf130..c967946 100644
--- a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -21,6 +21,6 @@
   public int updateDelay;
   public Boolean submitWholeTopic;
   public String mergeabilityComputationBehavior;
-  public Boolean enableRobotComments;
   public Boolean conflictsPredicateEnabled;
+  public Boolean allowMarkdownBase64ImagesInComments;
 }
diff --git a/java/com/google/gerrit/extensions/common/ConflictsInfo.java b/java/com/google/gerrit/extensions/common/ConflictsInfo.java
index ba9f1be..8e623d3 100644
--- a/java/com/google/gerrit/extensions/common/ConflictsInfo.java
+++ b/java/com/google/gerrit/extensions/common/ConflictsInfo.java
@@ -17,6 +17,29 @@
 /** Information about conflicts in a revision. */
 public class ConflictsInfo {
   /**
+   * The SHA1 of the commit that was used as the base commit for the Git merge that created the
+   * revision.
+   *
+   * <p>A base is not set if:
+   *
+   * <ul>
+   *   <li>the merged commits do not have a common ancestor (in this case {@link #noBaseReason} is
+   *       {@link NoMergeBaseReason#NO_COMMON_ANCESTOR}).
+   *   <li>the merged commits have multiple merge bases (happens for criss-cross-merges) and the
+   *       base was computed (in this case {@link #noBaseReason} is {@link
+   *       NoMergeBaseReason#COMPUTED_BASE}).
+   *   <li>a one sided merge strategy (e.g. {@code ours} or {@code theirs}) has been used and
+   *       computing a base was not required for the merge (in this case {@link #noBaseReason} is
+   *       {@link NoMergeBaseReason#ONE_SIDED_MERGE_STRATEGY}).
+   *   <li>the revision was not created by performing a Git merge operation (in this case {@link
+   *       #noBaseReason} is {@link NoMergeBaseReason#NO_MERGE_PERFORMED}).
+   *   <li>the revision has been created before Gerrit started to store the base for conflicts (in
+   *       this case {@link #noBaseReason} is {@link NoMergeBaseReason#HISTORIC_DATA_WITHOUT_BASE}).
+   * </ul>
+   */
+  public String base;
+
+  /**
    * The SHA1 of the commit that was used as {@code ours} for the Git merge that created the
    * revision.
    *
@@ -37,6 +60,35 @@
   public String theirs;
 
   /**
+   * The merge strategy was used for the Git merge that created the revision.
+   *
+   * <p>Possible values: {@code resolve}, {@code recursive}, {@code simple-two-way-in-core}, {@code
+   * ours} and {@code theirs}.
+   */
+  public String mergeStrategy;
+
+  /**
+   * Reason why {@link #base} is not set.
+   *
+   * <p>Only set if {@link #base} is not set.
+   *
+   * <p>Possible values are:
+   *
+   * <ul>
+   *   <li>{@code NO_COMMON_ANCESTOR}: The merged commits do not have a common ancestor.
+   *   <li>{@code COMPUTED_BASE}: The merged commits have multiple merge bases (happens for
+   *       criss-cross-merges) and the base was computed.
+   *   <li>{@code ONE_SIDED_MERGE_STRATEGY}: A one sided merge strategy (e.g. {@code ours} or {@code
+   *       theirs}) has been used and computing a base was not required for the merge.
+   *   <li>{@code NO_MERGE_PERFORMED}: The revision was not created by performing a Git merge
+   *       operation.
+   *   <li>{@code HISTORIC_DATA_WITHOUT_BASE}: The revision has been created before Gerrit started
+   *       to store the base for conflicts.
+   * </ul>
+   */
+  public NoMergeBaseReason noBaseReason;
+
+  /**
    * Whether any of the files in the revision has a conflict due to merging {@link #ours} and {@link
    * #theirs}.
    *
diff --git a/java/com/google/gerrit/extensions/common/EvaluateChangeQueryExpressionResultInfo.java b/java/com/google/gerrit/extensions/common/EvaluateChangeQueryExpressionResultInfo.java
new file mode 100644
index 0000000..84f7793
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/EvaluateChangeQueryExpressionResultInfo.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.common.Nullable;
+import java.util.List;
+import java.util.Map;
+
+/** Result of evaluating a change query expression. */
+public class EvaluateChangeQueryExpressionResultInfo {
+  /** Whether the change matches the change query expression. */
+  public boolean status;
+
+  /** List of passing leaf atoms (atoms that match the change). */
+  public List<String> passingAtoms;
+
+  /** List of failing leaf atoms (atoms that do not match the change). */
+  public List<String> failingAtoms;
+
+  /**
+   * Explanations for why atoms pass or fail.
+   *
+   * <p>Explanations are only available for a few atoms, for most atoms no explanation is provided.
+   *
+   * <p>Not set if none of the atoms has an explanation.
+   */
+  @Nullable public Map<String, String> atomExplanations;
+}
diff --git a/java/com/google/gerrit/extensions/common/FileInfo.java b/java/com/google/gerrit/extensions/common/FileInfo.java
index 9526fbb..29a6d4f 100644
--- a/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -20,6 +20,8 @@
   public Character status;
   public Integer oldMode;
   public Integer newMode;
+  public String oldSha;
+  public String newSha;
   public Boolean binary;
   public String oldPath;
   public Integer linesInserted;
@@ -32,6 +34,10 @@
     if (o instanceof FileInfo) {
       FileInfo fileInfo = (FileInfo) o;
       return Objects.equals(status, fileInfo.status)
+          && Objects.equals(oldMode, fileInfo.oldMode)
+          && Objects.equals(newMode, fileInfo.newMode)
+          && Objects.equals(oldSha, fileInfo.oldSha)
+          && Objects.equals(newSha, fileInfo.newSha)
           && Objects.equals(binary, fileInfo.binary)
           && Objects.equals(oldPath, fileInfo.oldPath)
           && Objects.equals(linesInserted, fileInfo.linesInserted)
@@ -52,6 +58,14 @@
     return "FileInfo{"
         + "status="
         + status
+        + ", oldMode="
+        + oldMode
+        + ", newMode="
+        + oldMode
+        + ", oldSha="
+        + oldSha
+        + ", newSha="
+        + oldSha
         + ", binary="
         + binary
         + ", oldPath="
diff --git a/java/com/google/gerrit/extensions/common/FlowActionInfo.java b/java/com/google/gerrit/extensions/common/FlowActionInfo.java
new file mode 100644
index 0000000..00a17ca
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/FlowActionInfo.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+
+/**
+ * Representation of a flow action in the REST API.
+ *
+ * <p>This class determines the JSON format of flow actions in the REST API.
+ *
+ * <p>An action to be triggered when the condition of a flow expression becomes satisfied.
+ */
+public class FlowActionInfo {
+  /**
+   * The name of the action.
+   *
+   * <p>Which actions are supported depends on the flow service implementation.
+   */
+  public String name;
+
+  /**
+   * Parameters for the action.
+   *
+   * <p>Which parameters are supported depends on the flow service implementation.
+   */
+  public List<String> parameters;
+}
diff --git a/java/com/google/gerrit/extensions/common/FlowExpressionInfo.java b/java/com/google/gerrit/extensions/common/FlowExpressionInfo.java
new file mode 100644
index 0000000..31a6c19
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/FlowExpressionInfo.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.common.Nullable;
+
+/**
+ * Representation of a flow expression in the REST API.
+ *
+ * <p>This class determines the JSON format of flow expressions in the REST API.
+ *
+ * <p>A stage flow expression defines an action that should be triggered when a condition becomes
+ * satisfied.
+ */
+public class FlowExpressionInfo {
+  /**
+   * The condition which must be satisfied for the action to be triggered.
+   *
+   * <p>Can contain multiple conditions separated by comma.
+   *
+   * <p>The syntax of the condition depends on the flow service implementation.
+   */
+  public String condition;
+
+  /**
+   * The action that should be triggered when the condition is satisfied.
+   *
+   * <p>If null, no action is performed.
+   */
+  @Nullable public FlowActionInfo action;
+}
diff --git a/java/com/google/gerrit/extensions/common/FlowInfo.java b/java/com/google/gerrit/extensions/common/FlowInfo.java
new file mode 100644
index 0000000..10c22cc
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/FlowInfo.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * Representation of a flow in the REST API.
+ *
+ * <p>This class determines the JSON format of flows in the REST API.
+ *
+ * <p>A flow is an automation rule on a change that triggers actions on the change when the flow
+ * conditions become satisfied.
+ */
+public class FlowInfo {
+  /** The universally unique identifier that identifies the flow. */
+  public String uuid;
+
+  /** The owner of the flow as an {@link AccountInfo} entity. */
+  public AccountInfo owner;
+
+  // TODO(issue-40014498): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+
+  /** The timestamp of when the flow was created. */
+  public Timestamp created;
+
+  @SuppressWarnings("JdkObsolete")
+  public void setCreated(Instant when) {
+    created = Timestamp.from(when);
+  }
+
+  /** The stages of this flow (sorted by execution order). */
+  public List<FlowStageInfo> stages;
+
+  /**
+   * The timestamp of when the flow was last evaluated.
+   *
+   * <p>Not set if the flow has not been evaluated yet.
+   */
+  public Timestamp lastEvaluated;
+
+  @SuppressWarnings("JdkObsolete")
+  public void setLastEvaluated(Instant when) {
+    lastEvaluated = Timestamp.from(when);
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/FlowInput.java b/java/com/google/gerrit/extensions/common/FlowInput.java
new file mode 100644
index 0000000..35d4ec2
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/FlowInput.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+
+/**
+ * REST API input for creating a flow.
+ *
+ * <p>This class determines the JSON format of flow inputs in the REST API.
+ *
+ * <p>Clients send a flow input in the request body when calling {@code POST
+ * /change/<change-id>/flows}.
+ */
+public class FlowInput {
+  /** The expressions for the stages of the flow (sorted by execution order). */
+  public List<FlowExpressionInfo> stageExpressions;
+}
diff --git a/java/com/google/gerrit/extensions/common/FlowStageInfo.java b/java/com/google/gerrit/extensions/common/FlowStageInfo.java
new file mode 100644
index 0000000..04f2882
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/FlowStageInfo.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+/**
+ * Representation of a stage in a flow in the REST API.
+ *
+ * <p>This class determines the JSON format of stages in flows in the REST API.
+ *
+ * <p>A stage in a flow consists out of a flow expression that defines an action that should be
+ * triggered when a condition becomes satisfied and a status.
+ */
+public class FlowStageInfo {
+  /** The expression defining the condition and the action of this stage. */
+  public FlowExpressionInfo expression;
+
+  /** The state for this stage. */
+  public FlowStageState state;
+
+  /**
+   * Optional message for the stage.
+   *
+   * <p>May be unset.
+   */
+  public String message;
+}
diff --git a/java/com/google/gerrit/extensions/common/FlowStageState.java b/java/com/google/gerrit/extensions/common/FlowStageState.java
new file mode 100644
index 0000000..69b2ac3
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/FlowStageState.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+/** State of a stage in a flow in the REST API. */
+public enum FlowStageState {
+  /** The condition of the stage is not satisfied yet or the action has not been executed yet. */
+  PENDING,
+
+  /** The condition of the stage is satisfied and the action has been executed. */
+  DONE,
+
+  /** The stage has a non-recoverable error, e.g. performing the action has failed. */
+  FAILED,
+
+  /**
+   * The stage has been terminated without having been executed, e.g. because a previous stage
+   * failed or because it wasn't done within a timeout.
+   */
+  TERMINATED;
+}
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/extensions/common/GroupsInfo.java
similarity index 61%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/extensions/common/GroupsInfo.java
index 3ec74ba..26b4b8a 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/extensions/common/GroupsInfo.java
@@ -12,9 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.extensions.common;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
-
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+/**
+ * Representation of group-related server configuration in the REST API.
+ *
+ * <p>This class determines the JSON format of group-related server configuration in the REST API.
+ */
+public class GroupsInfo {
+  /** The value of the {@code groups.enableDeleteGroup} parameter in {@code gerrit.config}. */
+  public Boolean enableDeleteGroup;
+}
diff --git a/java/com/google/gerrit/extensions/common/HttpPasswordInput.java b/java/com/google/gerrit/extensions/common/HttpPasswordInput.java
index 246c7cf..d45b23f 100644
--- a/java/com/google/gerrit/extensions/common/HttpPasswordInput.java
+++ b/java/com/google/gerrit/extensions/common/HttpPasswordInput.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
+@Deprecated
 public class HttpPasswordInput {
   public String httpPassword;
   public boolean generate;
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/extensions/common/IsFlowsEnabledInfo.java
similarity index 64%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/extensions/common/IsFlowsEnabledInfo.java
index 3ec74ba..21e4734 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/extensions/common/IsFlowsEnabledInfo.java
@@ -12,9 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.extensions.common;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+/**
+ * Representation of a boolean flag in the REST API.
+ *
+ * <p>This class determines the JSON format of boolean flags in the REST API.
+ */
+public class IsFlowsEnabledInfo {
+  public boolean enabled;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+  public IsFlowsEnabledInfo(boolean enabled) {
+    this.enabled = enabled;
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/NoMergeBaseReason.java b/java/com/google/gerrit/extensions/common/NoMergeBaseReason.java
new file mode 100644
index 0000000..840fd4f
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/NoMergeBaseReason.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.common.base.CaseFormat;
+
+/** Reasons why a merge base is not available. */
+public enum NoMergeBaseReason {
+  /**
+   * The revision has been created before Gerrit started to compute and store the base for
+   * conflicts.
+   */
+  HISTORIC_DATA_WITHOUT_BASE(0),
+
+  /** The merged commits do not have a common ancestor. */
+  NO_COMMON_ANCESTOR(1),
+
+  /**
+   * The merged commits have multiple merge bases (happens for criss-cross-merges) and the base was
+   * computed.
+   */
+  COMPUTED_BASE(2),
+
+  /**
+   * A one sided merge strategy (e.g. {@code ours} or {@code theirs}) has been used and computing a
+   * base was not required for the merge.
+   */
+  ONE_SIDED_MERGE_STRATEGY(3),
+
+  /** The revision was not created by performing a Git merge operation. */
+  NO_MERGE_PERFORMED(4);
+
+  private final int value;
+
+  NoMergeBaseReason(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  public String getDescription() {
+    return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, name()).replace('-', ' ');
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/RobotCommentInfo.java b/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
index 780cab7..bd9d088 100644
--- a/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
+++ b/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
@@ -14,12 +14,5 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.util.Map;
-
 @Deprecated
-public class RobotCommentInfo extends CommentInfo {
-  public String robotId;
-  public String robotRunId;
-  public String url;
-  public Map<String, String> properties;
-}
+public class RobotCommentInfo extends CommentInfo {}
diff --git a/java/com/google/gerrit/extensions/common/ServerInfo.java b/java/com/google/gerrit/extensions/common/ServerInfo.java
index 0a3b6ee..35f5949 100644
--- a/java/com/google/gerrit/extensions/common/ServerInfo.java
+++ b/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -23,6 +23,7 @@
   public ChangeConfigInfo change;
   public DownloadInfo download;
   public GerritInfo gerrit;
+  public GroupsInfo groups;
   public Boolean noteDbEnabled;
   public PluginConfigInfo plugin;
   public SshdInfo sshd;
@@ -31,6 +32,7 @@
   public ReceiveInfo receive;
   public String defaultTheme;
   public List<String> submitRequirementDashboardColumns;
+  public Boolean dashboardShowAllLabels;
 
   /** Server metadata populated by plugins. */
   public List<MetadataInfo> metadata;
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
index 9347e7e..9f1b4fc 100644
--- a/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
@@ -22,6 +22,13 @@
   public String description;
 
   /**
+   * The name of the project that defines this submit requirement.
+   *
+   * <p>{@code null} if the it's a global submit requirement.
+   */
+  public String projectName;
+
+  /**
    * Expression string to be evaluated on a change. Decides if this submit requirement is applicable
    * on the given change.
    */
diff --git a/java/com/google/gerrit/extensions/common/testing/AccountInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/AccountInfoSubject.java
index 8fa6617..bdf5088 100644
--- a/java/com/google/gerrit/extensions/common/testing/AccountInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/AccountInfoSubject.java
@@ -16,9 +16,11 @@
 
 import static com.google.common.truth.Truth.assertAbout;
 
+import com.google.common.truth.ComparableSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.IntegerSubject;
 import com.google.common.truth.Subject;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
 
 /** A Truth subject for {@link AccountInfo} instances. */
@@ -39,8 +41,12 @@
     this.accountInfo = accountInfo;
   }
 
-  public IntegerSubject id() {
-    return check("id").that(accountInfo()._accountId);
+  public IntegerSubject hasIdThat() {
+    return check("id()").that(accountInfo()._accountId);
+  }
+
+  public ComparableSubject<Account.Id> hasAccountIdThat() {
+    return check("accountId()").that(Account.id(accountInfo()._accountId));
   }
 
   private AccountInfo accountInfo() {
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index c126928..d607540 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -8,6 +8,9 @@
     deps = [
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/flow",
+        "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/truth",
         "//lib:guava",
         "//lib:jgit",
diff --git a/java/com/google/gerrit/extensions/common/testing/FlowActionInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FlowActionInfoSubject.java
new file mode 100644
index 0000000..9b3fee5
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FlowActionInfoSubject.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.extensions.common.FlowActionInfo;
+
+/** A Truth subject for {@link FlowActionInfo} instances. */
+public class FlowActionInfoSubject extends Subject {
+  private final FlowActionInfo flowActionInfo;
+
+  public static FlowActionInfoSubject assertThat(FlowActionInfo flowActionInfo) {
+    return assertAbout(flowActions()).that(flowActionInfo);
+  }
+
+  public static Factory<FlowActionInfoSubject, FlowActionInfo> flowActions() {
+    return FlowActionInfoSubject::new;
+  }
+
+  private FlowActionInfoSubject(FailureMetadata metadata, FlowActionInfo flowActionInfo) {
+    super(metadata, flowActionInfo);
+    this.flowActionInfo = flowActionInfo;
+  }
+
+  public StringSubject hasNameThat() {
+    return check("name()").that(flowActionInfo().name);
+  }
+
+  public IterableSubject hasParametersThat() {
+    return check("parameters()").that(flowActionInfo().parameters);
+  }
+
+  private FlowActionInfo flowActionInfo() {
+    isNotNull();
+    return flowActionInfo;
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/FlowExpressionInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FlowExpressionInfoSubject.java
new file mode 100644
index 0000000..abcb194
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FlowExpressionInfoSubject.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.extensions.common.testing.FlowActionInfoSubject.flowActions;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.extensions.common.FlowExpressionInfo;
+
+/** A Truth subject for {@link FlowExpressionInfo} instances. */
+public class FlowExpressionInfoSubject extends Subject {
+  private final FlowExpressionInfo flowExpressionInfo;
+
+  public static FlowExpressionInfoSubject assertThat(FlowExpressionInfo flowExpressionInfo) {
+    return assertAbout(flowExpressions()).that(flowExpressionInfo);
+  }
+
+  public static Factory<FlowExpressionInfoSubject, FlowExpressionInfo> flowExpressions() {
+    return FlowExpressionInfoSubject::new;
+  }
+
+  private FlowExpressionInfoSubject(
+      FailureMetadata metadata, FlowExpressionInfo flowExpressionInfo) {
+    super(metadata, flowExpressionInfo);
+    this.flowExpressionInfo = flowExpressionInfo;
+  }
+
+  public StringSubject hasConditionThat() {
+    return check("condition()").that(flowExpressionInfo().condition);
+  }
+
+  public void hasNoAction() {
+    assertWithMessage("FlowExpressionInfo.action is not null, but no action is expected")
+        .that(flowExpressionInfo().action)
+        .isNull();
+  }
+
+  public FlowActionInfoSubject hasActionThat() {
+    assertWithMessage("FlowExpressionInfo.action is null")
+        .that(flowExpressionInfo().action)
+        .isNotNull();
+    return check("action()").about(flowActions()).that(flowExpressionInfo().action);
+  }
+
+  private FlowExpressionInfo flowExpressionInfo() {
+    isNotNull();
+    return flowExpressionInfo;
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/FlowInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FlowInfoSubject.java
new file mode 100644
index 0000000..2fc9bb4
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FlowInfoSubject.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.AccountInfoSubject.accounts;
+import static com.google.gerrit.truth.ListSubject.elements;
+import static com.google.gerrit.truth.OptionalSubject.optionals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.ComparableSubject;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.extensions.common.FlowExpressionInfo;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.extensions.common.FlowStageInfo;
+import com.google.gerrit.extensions.common.FlowStageState;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowStage;
+import com.google.gerrit.server.restapi.flow.FlowJson;
+import com.google.gerrit.truth.ListSubject;
+import com.google.gerrit.truth.OptionalSubject;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Optional;
+
+/** A Truth subject for {@link FlowInfo} instances. */
+public class FlowInfoSubject extends Subject {
+  private final FailureMetadata metadata;
+  private final FlowInfo flowInfo;
+
+  public static FlowInfoSubject assertThat(FlowInfo flowInfo) {
+    return assertAbout(flows()).that(flowInfo);
+  }
+
+  public static Factory<FlowInfoSubject, FlowInfo> flows() {
+    return FlowInfoSubject::new;
+  }
+
+  private FlowInfoSubject(FailureMetadata metadata, FlowInfo flowInfo) {
+    super(metadata, flowInfo);
+    this.metadata = metadata;
+    this.flowInfo = flowInfo;
+  }
+
+  public StringSubject hasUuidThat() {
+    return check("uuid()").that(flowInfo().uuid);
+  }
+
+  public AccountInfoSubject hasOwnerThat() {
+    return check("owner()").about(accounts()).that(flowInfo().owner);
+  }
+
+  public ComparableSubject<Instant> hasCreatedThat() {
+    return check("created()").that(flowInfo().created.toInstant());
+  }
+
+  public ListSubject<FlowStageInfoSubject, FlowStageInfo> hasStagesThat() {
+    return check("stages()")
+        .about(elements())
+        .thatCustom(flowInfo().stages, FlowStageInfoSubject.flowStages());
+  }
+
+  public OptionalSubject<ComparableSubject<Instant>, ?> hasLastEvaluated() {
+    return check("lastEvaluated()")
+        .about(optionals())
+        .thatCustom(
+            Optional.ofNullable(flowInfo().lastEvaluated).map(Timestamp::toInstant),
+            (builder, value) -> new ComparableSubject<>(metadata, value) {});
+  }
+
+  /**
+   * Asserts that the properties of this {@link FlowInfo} match with the properties of the given
+   * {@link FlowInput} instance.
+   */
+  public void matches(FlowInput flowInput) {
+    hasUuidThat().isNotEmpty();
+    hasLastEvaluated().isEmpty();
+
+    hasStagesThat().hasSize(flowInput.stageExpressions.size());
+
+    for (int i = 0; i < flowInput.stageExpressions.size(); i++) {
+      FlowExpressionInfo flowExpressionInfo = flowInput.stageExpressions.get(i);
+      FlowStageInfoSubject stageSubject = hasStagesThat().element(i);
+      stageSubject.hasStateThat().isEqualTo(FlowStageState.PENDING);
+      stageSubject.hasExpressionThat().hasConditionThat().isEqualTo(flowExpressionInfo.condition);
+      if (flowInput.stageExpressions.get(i).action != null) {
+        stageSubject
+            .hasExpressionThat()
+            .hasActionThat()
+            .hasNameThat()
+            .isEqualTo(flowExpressionInfo.action.name);
+        stageSubject
+            .hasExpressionThat()
+            .hasActionThat()
+            .hasParametersThat()
+            .isEqualTo(
+                flowExpressionInfo.action.parameters != null
+                    ? flowExpressionInfo.action.parameters
+                    : ImmutableList.of());
+      } else {
+        stageSubject.hasExpressionThat().hasNoAction();
+      }
+    }
+  }
+
+  /**
+   * Asserts that the properties of this {@link FlowInfo} match with the properties of the given
+   * {@link Flow} instance.
+   */
+  public void matches(Flow flow) {
+    hasUuidThat().isEqualTo(flow.key().uuid());
+    hasOwnerThat().hasAccountIdThat().isEqualTo(flow.ownerId());
+    hasCreatedThat().isEqualTo(flow.createdOn());
+    hasLastEvaluated().isEqualTo(flow.lastEvaluatedOn());
+
+    hasStagesThat().hasSize(flow.stages().size());
+
+    for (int i = 0; i < flow.stages().size(); i++) {
+      FlowStage flowStage = flow.stages().get(i);
+
+      FlowStageInfoSubject stageSubject = hasStagesThat().element(i);
+      stageSubject.hasStateThat().isEqualTo(FlowJson.mapState(flowStage.status().state()));
+      stageSubject
+          .hasExpressionThat()
+          .hasConditionThat()
+          .isEqualTo(flowStage.expression().condition());
+
+      if (flowStage.expression().action().isPresent()) {
+        stageSubject
+            .hasExpressionThat()
+            .hasActionThat()
+            .hasNameThat()
+            .isEqualTo(flowStage.expression().action().get().name());
+        stageSubject
+            .hasExpressionThat()
+            .hasActionThat()
+            .hasParametersThat()
+            .isEqualTo(flowStage.expression().action().get().parameters());
+      } else {
+        stageSubject.hasExpressionThat().hasNoAction();
+      }
+    }
+  }
+
+  private FlowInfo flowInfo() {
+    isNotNull();
+    return flowInfo;
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/FlowStageInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FlowStageInfoSubject.java
new file mode 100644
index 0000000..fb7f507
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FlowStageInfoSubject.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.extensions.common.testing.FlowExpressionInfoSubject.flowExpressions;
+
+import com.google.common.truth.ComparableSubject;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.gerrit.extensions.common.FlowStageInfo;
+import com.google.gerrit.extensions.common.FlowStageState;
+
+/** A Truth subject for {@link FlowStageInfo} instances. */
+public class FlowStageInfoSubject extends Subject {
+  private final FlowStageInfo flowStageInfo;
+
+  public static FlowStageInfoSubject assertThat(FlowStageInfo flowStageInfo) {
+    return assertAbout(flowStages()).that(flowStageInfo);
+  }
+
+  public static Factory<FlowStageInfoSubject, FlowStageInfo> flowStages() {
+    return FlowStageInfoSubject::new;
+  }
+
+  private FlowStageInfoSubject(FailureMetadata metadata, FlowStageInfo flowStageInfo) {
+    super(metadata, flowStageInfo);
+    this.flowStageInfo = flowStageInfo;
+  }
+
+  public FlowExpressionInfoSubject hasExpressionThat() {
+    return check("expression()").about(flowExpressions()).that(flowStageInfo().expression);
+  }
+
+  public ComparableSubject<FlowStageState> hasStateThat() {
+    return check("state()").that(flowStageInfo().state);
+  }
+
+  private FlowStageInfo flowStageInfo() {
+    isNotNull();
+    return flowStageInfo;
+  }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
deleted file mode 100644
index 5176145..0000000
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.common.testing;
-
-import static com.google.common.truth.Truth.assertAbout;
-import static com.google.gerrit.truth.ListSubject.elements;
-
-import com.google.common.truth.FailureMetadata;
-import com.google.common.truth.MapSubject;
-import com.google.common.truth.StringSubject;
-import com.google.common.truth.Subject;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
-import com.google.gerrit.truth.ListSubject;
-import java.util.List;
-
-public class RobotCommentInfoSubject extends Subject {
-
-  public static ListSubject<RobotCommentInfoSubject, RobotCommentInfo> assertThatList(
-      List<RobotCommentInfo> robotCommentInfos) {
-    return ListSubject.assertThat(robotCommentInfos, robotComments());
-  }
-
-  public static RobotCommentInfoSubject assertThat(RobotCommentInfo robotCommentInfo) {
-    return assertAbout(robotComments()).that(robotCommentInfo);
-  }
-
-  private static Factory<RobotCommentInfoSubject, RobotCommentInfo> robotComments() {
-    return RobotCommentInfoSubject::new;
-  }
-
-  private final RobotCommentInfo robotCommentInfo;
-
-  private RobotCommentInfoSubject(
-      FailureMetadata failureMetadata, RobotCommentInfo robotCommentInfo) {
-    super(failureMetadata, robotCommentInfo);
-    this.robotCommentInfo = robotCommentInfo;
-  }
-
-  public ListSubject<FixSuggestionInfoSubject, FixSuggestionInfo> fixSuggestions() {
-    return check("fixSuggestions")
-        .about(elements())
-        .thatCustom(robotCommentInfo.fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
-  }
-
-  public StringSubject path() {
-    isNotNull();
-    return check("path").that(robotCommentInfo.path);
-  }
-
-  public StringSubject robotId() {
-    isNotNull();
-    return check("robotId").that(robotCommentInfo.robotId);
-  }
-
-  public StringSubject robotRunId() {
-    isNotNull();
-    return check("robotRunId").that(robotCommentInfo.robotRunId);
-  }
-
-  public StringSubject url() {
-    isNotNull();
-    return check("url").that(robotCommentInfo.url);
-  }
-
-  public MapSubject properties() {
-    isNotNull();
-    return check("property").that(robotCommentInfo.properties);
-  }
-
-  public FixSuggestionInfoSubject onlyFixSuggestion() {
-    return fixSuggestions().onlyElement();
-  }
-}
diff --git a/java/com/google/gerrit/extensions/restapi/RestReadView.java b/java/com/google/gerrit/extensions/restapi/RestReadView.java
index 8991f0b..320ece0 100644
--- a/java/com/google/gerrit/extensions/restapi/RestReadView.java
+++ b/java/com/google/gerrit/extensions/restapi/RestReadView.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import javax.servlet.http.HttpServletRequest;
+
 /**
  * RestView to read a resource without modification.
  *
@@ -47,4 +49,33 @@
    */
   Response<?> apply(R resource)
       throws AuthException, BadRequestException, ResourceConflictException, Exception;
+
+  /**
+   * Process the view operation by reading from the resource.
+   *
+   * <p>The value of the returned response is automatically converted to JSON unless it is a {@link
+   * BinaryResult}.
+   *
+   * <p>The returned response defines the status code that is returned to the client. For
+   * RestReadViews this is usually {@code 200 OK}, but other 2XX or 3XX status codes are also
+   * possible (e.g. {@link Response.Redirect} can be returned for {@code 302 Found}).
+   *
+   * <p>Throwing a subclass of {@link RestApiException} results in a 4XX response to the client. For
+   * any other exception the client will get a {@code 500 Internal Server Error} response.
+   *
+   * @param req original request that has been processed by all the applicable Filters
+   * @param resource resource to read
+   * @return response to return to the client
+   * @throws AuthException the caller is not permitted to access this view.
+   * @throws BadRequestException the request was incorrectly specified and cannot be handled by this
+   *     view.
+   * @throws ResourceConflictException the resource state does not permit this view to make the
+   *     changes at this time.
+   * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
+   *     500 Internal Server Error will be returned to the client.
+   */
+  default Response<?> apply(HttpServletRequest req, R resource)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+    return apply(resource);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/validators/CommentForValidation.java b/java/com/google/gerrit/extensions/validators/CommentForValidation.java
index a8ffc26..ae4f58f 100644
--- a/java/com/google/gerrit/extensions/validators/CommentForValidation.java
+++ b/java/com/google/gerrit/extensions/validators/CommentForValidation.java
@@ -27,9 +27,7 @@
   /** The creator of the comment. */
   public enum CommentSource {
     /** A regular user comment. */
-    HUMAN,
-    /** A robot comment. */
-    ROBOT
+    HUMAN
   }
 
   /** The type of comment. */
@@ -52,15 +50,12 @@
   public abstract CommentType getType();
 
   /**
-   * Returns the comment text. Note that especially for robot comments the total size may be
-   * significantly larger and should be determined by using {@link #getApproximateSize()}.
+   * Returns the comment text. Note that the total size may be significantly larger and should be
+   * determined by using {@link #getApproximateSize()}.
    */
   public abstract String getText();
 
-  /**
-   * Returns this instance's approximate size in bytes for the purpose of applying size limits. For
-   * robot comments this may be significantly larger than the size of the comment text.
-   */
+  /** Returns this instance's approximate size in bytes for the purpose of applying size limits. */
   public abstract int getApproximateSize();
 
   public CommentValidationFailure failValidation(String message) {
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 4ccd05a..919da12 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PropertyMap;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AuthResult;
@@ -206,7 +207,7 @@
   public void setUserAccountId(Account.Id id) {
     key = new WebSessionManager.Key("id:" + id);
     val = new WebSessionManager.Val(id, 0, false, null, 0, null, null);
-    user = identified.runAs(id, user, PropertyMap.EMPTY);
+    user = identified.runAs(id, user, PropertyMap.EMPTY, ImpersonationPermissionMode.THIS_USER);
   }
 
   @Override
@@ -268,7 +269,6 @@
     outCookie.setSecure(isSecure(request));
     outCookie.setPath(path);
     outCookie.setMaxAge(ageSeconds);
-    outCookie.setSecure(authConfig.getCookieSecure());
     outCookie.setHttpOnly(authConfig.getCookieHttpOnly());
     response.addCookie(outCookie);
   }
diff --git a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 7cb2357..e520fa2 100644
--- a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -33,8 +33,8 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.AuthTokenVerifier;
 import com.google.gerrit.server.account.AuthenticationFailedException;
-import com.google.gerrit.server.account.externalids.PasswordVerifier;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
@@ -77,7 +77,7 @@
   private final AccountManager accountManager;
   private final AuthConfig authConfig;
   private final AuthRequest.Factory authRequestFactory;
-  private final PasswordVerifier passwordVerifier;
+  private final AuthTokenVerifier tokenVerifier;
 
   @Inject
   ProjectBasicAuthFilter(
@@ -86,13 +86,13 @@
       AccountManager accountManager,
       AuthConfig authConfig,
       AuthRequest.Factory authRequestFactory,
-      PasswordVerifier passwordVerifier) {
+      AuthTokenVerifier tokenVerifier) {
     this.session = session;
     this.accountCache = accountCache;
     this.accountManager = accountManager;
     this.authConfig = authConfig;
     this.authRequestFactory = authRequestFactory;
-    this.passwordVerifier = passwordVerifier;
+    this.tokenVerifier = tokenVerifier;
   }
 
   @Override
@@ -162,7 +162,7 @@
       }
 
       AccountState who = accountState.get();
-      if (passwordVerifier.checkPassword(who.externalIds(), username, password)) {
+      if (tokenVerifier.checkToken(who.account().id(), password)) {
         logger.atFine().log(
             "HTTP:%s %s username/password authentication succeeded",
             req.getMethod(), req.getRequestURI());
@@ -191,8 +191,7 @@
       // of the need to still be able to authenticate service users.
       Optional<AccountState> who =
           accountCache.getByUsername(username).filter(a -> a.account().isActive());
-      if (who.isPresent()
-          && passwordVerifier.checkPassword(who.get().externalIds(), username, password)) {
+      if (who.isPresent() && tokenVerifier.checkToken(who.get().account().id(), password)) {
         return succeedAuthentication(who.get(), null);
       }
       logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(username, req));
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index 5a99cab..9d58d09 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -69,6 +69,7 @@
   private static final String REALM_NAME = "Gerrit Code Review";
   private static final String AUTHORIZATION = "Authorization";
   private static final String BASIC = "Basic ";
+  private static final String BEARER = "Bearer ";
   private static final String GIT_COOKIE_PREFIX = "git-";
 
   private final DynamicItem<WebSession> session;
@@ -123,30 +124,52 @@
 
   private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
     AuthInfo authInfo;
+    AuthRequest authRequest;
 
-    // first check if there is a BASIC authentication header
     String hdr = req.getHeader(AUTHORIZATION);
-    if (hdr != null && hdr.startsWith(BASIC)) {
-      authInfo = extractAuthInfo(hdr, encoding(req));
-      if (authInfo == null) {
-        rsp.sendError(SC_UNAUTHORIZED);
-        return false;
-      }
+    // first check if there is a BEARER authentication header
+    if (hdr != null && hdr.startsWith(BEARER)) {
+      authInfo = extractAuthInfoFromBearer(hdr);
+      authRequest = authRequestFactory.createForBearerToken(authInfo.tokenOrSecret);
+      // or if there is a BASIC authentication header
     } else {
-      // if there is no BASIC authentication header, check if there is
-      // a cookie starting with the prefix "git-"
-      Cookie cookie = findGitCookie(req);
-      if (cookie != null) {
-        authInfo = extractAuthInfo(cookie);
+      if (hdr != null && hdr.startsWith(BASIC)) {
+        authInfo = extractAuthInfo(hdr, encoding(req));
         if (authInfo == null) {
           rsp.sendError(SC_UNAUTHORIZED);
           return false;
         }
       } else {
-        // if there is no authentication information at all, it might be
-        // an anonymous connection, or there might be a session cookie
-        return true;
+        // if there are no BASIC or BEARER authentication headers, check if there is
+        // a cookie starting with the prefix "git-"
+        Cookie cookie = findGitCookie(req);
+        if (cookie != null) {
+          authInfo = extractAuthInfo(cookie);
+          if (authInfo == null) {
+            rsp.sendError(SC_UNAUTHORIZED);
+            return false;
+          }
+        } else {
+          // if there is no authentication information at all, it might be
+          // an anonymous connection, or there might be a session cookie
+          return true;
+        }
       }
+      authRequest = authRequestFactory.createForExternalUser(authInfo.username);
+      Optional<AccountState> who =
+          accountCache.getByUsername(authInfo.username).filter(a -> a.account().isActive());
+      if (!who.isPresent()) {
+        logger.atWarning().log(
+            "%s: account inactive or not provisioned in Gerrit",
+            authenticationFailedMsg(authInfo.username, req));
+        rsp.sendError(SC_UNAUTHORIZED);
+        return false;
+      }
+
+      Account account = who.get().account();
+      authRequest.setEmailAddress(account.preferredEmail());
+      authRequest.setDisplayName(account.fullName());
+      authRequest.setPassword(authInfo.tokenOrSecret);
     }
 
     // if there is authentication information but no secret => 401
@@ -155,21 +178,6 @@
       return false;
     }
 
-    Optional<AccountState> who =
-        accountCache.getByUsername(authInfo.username).filter(a -> a.account().isActive());
-    if (!who.isPresent()) {
-      logger.atWarning().log(
-          "%s: account inactive or not provisioned in Gerrit",
-          authenticationFailedMsg(authInfo.username, req));
-      rsp.sendError(SC_UNAUTHORIZED);
-      return false;
-    }
-
-    Account account = who.get().account();
-    AuthRequest authRequest = authRequestFactory.createForExternalUser(authInfo.username);
-    authRequest.setEmailAddress(account.preferredEmail());
-    authRequest.setDisplayName(account.fullName());
-    authRequest.setPassword(authInfo.tokenOrSecret);
     authRequest.setAuthPlugin(authInfo.pluginName);
     authRequest.setAuthProvider(authInfo.exportName);
 
@@ -244,6 +252,12 @@
   }
 
   @Nullable
+  private AuthInfo extractAuthInfoFromBearer(String hdr) {
+    String token = hdr.substring(BEARER.length());
+    return new AuthInfo(token, defaultAuthPlugin, defaultAuthProvider);
+  }
+
+  @Nullable
   private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
     String username =
         URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
@@ -289,7 +303,7 @@
   }
 
   private class AuthInfo {
-    private final String username;
+    @Nullable private final String username;
     private final String tokenOrSecret;
     private final String pluginName;
     private final String exportName;
@@ -300,6 +314,10 @@
       this.pluginName = pluginName;
       this.exportName = exportName;
     }
+
+    private AuthInfo(String token, String pluginName, String exportName) {
+      this(null, token, pluginName, exportName);
+    }
   }
 
   private static class Response extends HttpServletResponseWrapper {
diff --git a/java/com/google/gerrit/httpd/RunAsFilter.java b/java/com/google/gerrit/httpd/RunAsFilter.java
index b93f7ed..b521bd7 100644
--- a/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,7 +21,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -88,15 +87,13 @@
 
       CurrentUser self = session.get().getUser();
       try {
-        if (!self.isIdentifiedUser()) {
-          // Always disallow for anonymous users, even if permitted by the ACL,
-          // because that would be crazy.
-          throw new AuthException("denied");
+        // Always disallow for anonymous users, even if permitted by the ACL,
+        // because that would be crazy.
+        if (!self.isIdentifiedUser()
+            || !permissionBackend.user(self).test(GlobalPermission.RUN_AS)) {
+          replyError(req, res, SC_FORBIDDEN, "not permitted to use " + RUN_AS, null);
+          return;
         }
-        permissionBackend.user(self).check(GlobalPermission.RUN_AS);
-      } catch (AuthException e) {
-        replyError(req, res, SC_FORBIDDEN, "not permitted to use " + RUN_AS, null);
-        return;
       } catch (PermissionBackendException e) {
         logger.atWarning().withCause(e).log("cannot check runAs");
         replyError(req, res, SC_INTERNAL_SERVER_ERROR, RUN_AS + " unavailable", null);
diff --git a/java/com/google/gerrit/httpd/XsrfCookieFilter.java b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
index 079efa4..9bad714 100644
--- a/java/com/google/gerrit/httpd/XsrfCookieFilter.java
+++ b/java/com/google/gerrit/httpd/XsrfCookieFilter.java
@@ -18,7 +18,8 @@
 
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.AuthConfig;
+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;
@@ -38,23 +39,22 @@
 public class XsrfCookieFilter implements Filter {
   private final Provider<CurrentUser> user;
   private final DynamicItem<WebSession> session;
-  private final AuthConfig authConfig;
 
   @Inject
-  XsrfCookieFilter(
-      Provider<CurrentUser> user, DynamicItem<WebSession> session, AuthConfig authConfig) {
+  XsrfCookieFilter(Provider<CurrentUser> user, DynamicItem<WebSession> session) {
     this.user = user;
     this.session = session;
-    this.authConfig = authConfig;
   }
 
   @Override
   public void doFilter(ServletRequest req, ServletResponse rsp, FilterChain chain)
       throws IOException, ServletException {
-    HttpServletRequest httpRequest = (HttpServletRequest) req;
-    if (!GitSmartHttpTools.isGitClient(httpRequest)) {
-      WebSession s = user.get().isIdentifiedUser() ? session.get() : null;
-      setXsrfTokenCookie(httpRequest, (HttpServletResponse) rsp, s);
+    try (TraceTimer ignored = TraceContext.newTimer("XsrfCookieFilter#preprocess")) {
+      HttpServletRequest httpRequest = (HttpServletRequest) req;
+      if (!GitSmartHttpTools.isGitClient(httpRequest)) {
+        WebSession s = user.get().isIdentifiedUser() ? session.get() : null;
+        setXsrfTokenCookie(httpRequest, (HttpServletResponse) rsp, s);
+      }
     }
     chain.doFilter(req, rsp);
   }
@@ -64,7 +64,7 @@
     String v = session != null ? session.getXGerritAuth() : null;
     Cookie c = new Cookie(XsrfConstants.XSRF_COOKIE_NAME, nullToEmpty(v));
     c.setPath("/");
-    c.setSecure(authConfig.getCookieSecure() && isSecure(req));
+    c.setSecure(isSecure(req));
     c.setMaxAge(
         v != null
             ? -1 // Set the cookie for this browser session.
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index d3de8e0..18d26c0 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -40,7 +40,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.AnonymousUser;
@@ -429,10 +428,13 @@
       }
 
       projectState.get().checkStatePermitsRead();
-      permissionBackend.user(userProvider.get()).project(nameKey).check(ProjectPermission.READ);
-    } catch (AuthException e) {
-      sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
-      return;
+      if (!permissionBackend
+          .user(userProvider.get())
+          .project(nameKey)
+          .test(ProjectPermission.READ)) {
+        sendErrorOrRedirect(req, rsp, HttpServletResponse.SC_NOT_FOUND);
+        return;
+      }
     } catch (IOException | PermissionBackendException err) {
       logger.atSevere().withCause(err).log("cannot load %s", name);
       rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 64b69d1..bef75b0 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -20,6 +20,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.auth.AuthModule;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.GerritAuthModule;
@@ -53,6 +54,8 @@
 import com.google.gerrit.server.StartupChecks.StartupChecksModule;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountDeactivator.AccountDeactivatorModule;
+import com.google.gerrit.server.account.AuthTokenModule;
+import com.google.gerrit.server.account.CachingAuthTokenModule;
 import com.google.gerrit.server.account.InternalAccountDirectory.InternalAccountDirectoryModule;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCaseSensitivityMigrator;
 import com.google.gerrit.server.api.GerritApiModule;
@@ -157,6 +160,7 @@
   private Injector dbInjector;
   private Injector cfgInjector;
   private Config config;
+  private AuthConfig authConfig;
   private Injector sysInjector;
   private Injector webInjector;
   private Injector sshInjector;
@@ -222,6 +226,7 @@
       dbInjector = createDbInjector();
       initIndexType();
       config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+      authConfig = cfgInjector.getInstance(AuthConfig.class);
       sysInjector = createSysInjector();
       if (!sshdOff()) {
         sshInjector = createSshInjector();
@@ -359,6 +364,16 @@
 
     SshSessionFactoryInitializer.init();
     modules.add(SshKeyCacheImpl.module());
+
+    boolean useAuthTokenCache =
+        authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP
+            || authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP_LDAP;
+    if (useAuthTokenCache) {
+      modules.add(new CachingAuthTokenModule());
+    } else {
+      modules.add(new AuthTokenModule());
+    }
+
     modules.add(
         new AbstractModule() {
           @Override
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 2ef499f..df88e03 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
@@ -27,12 +28,14 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.httpd.raw.IndexPreloadingUtil.RequestedPage;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
 import com.google.gson.Gson;
 import com.google.template.soy.data.SanitizedContent;
 import java.net.URI;
@@ -41,6 +44,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -68,10 +72,13 @@
       String requestedURL)
       throws URISyntaxException, RestApiException {
     ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+    boolean asyncSubmitRequirements =
+        experimentFeatures.isFeatureEnabled(ExperimentFeaturesConstants.ASYNC_SUBMIT_REQUIREMENTS);
     data.putAll(
             staticTemplateData(
                 canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
-        .putAll(dynamicTemplateData(gerritApi, requestedURL, canonicalURL));
+        .putAll(
+            dynamicTemplateData(gerritApi, requestedURL, canonicalURL, asyncSubmitRequirements));
     Set<String> enabledExperiments = new HashSet<>();
     enabledExperiments.addAll(experimentFeatures.getEnabledExperimentFeatures());
     // Add all experiments enabled through url
@@ -107,7 +114,10 @@
 
   /** Returns dynamic parameters of {@code index.html}. */
   public static ImmutableMap<String, Object> dynamicTemplateData(
-      GerritApi gerritApi, String requestedURL, String canonicalURL)
+      GerritApi gerritApi,
+      String requestedURL,
+      String canonicalURL,
+      boolean asyncSubmitRequirements)
       throws RestApiException, URISyntaxException {
     ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
     Map<String, SanitizedContent> initialData = new HashMap<>();
@@ -127,15 +137,23 @@
     Integer basePatchNum = computeBasePatchNum(requestedPath);
     switch (page) {
       case CHANGE, DIFF -> {
-        if (basePatchNum.equals(0)) {
+        LinkedHashSet<ListChangesOption> changeDetailOptions =
+            new LinkedHashSet<>(
+                basePatchNum.equals(0)
+                    ? IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS
+                    : IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS);
+        if (asyncSubmitRequirements) {
+          changeDetailOptions.remove(ListChangesOption.SUBMIT_REQUIREMENTS);
+          changeDetailOptions.remove(ListChangesOption.SUBMITTABLE);
           data.put(
-              "defaultChangeDetailHex",
-              ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS));
-        } else {
-          data.put(
-              "defaultChangeDetailHex",
-              ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS));
+              "submitRequirementsHex",
+              ListOption.toHex(
+                  ImmutableSet.of(
+                      ListChangesOption.SUBMIT_REQUIREMENTS,
+                      ListChangesOption.SUBMITTABLE,
+                      ListChangesOption.SKIP_DIFFSTAT)));
         }
+        data.put("defaultChangeDetailHex", ListOption.toHex(changeDetailOptions));
         data.put(
             "changeRequestsPath",
             IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 43d0172..bb18859 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -107,20 +107,10 @@
           ListChangesOption.SUBMIT_REQUIREMENTS);
 
   public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS_WITH_PARENTS =
-      ImmutableSet.of(
-          ListChangesOption.ALL_COMMITS,
-          ListChangesOption.ALL_REVISIONS,
-          ListChangesOption.CHANGE_ACTIONS,
-          ListChangesOption.DETAILED_ACCOUNTS,
-          ListChangesOption.DETAILED_LABELS,
-          ListChangesOption.DOWNLOAD_COMMANDS,
-          ListChangesOption.MESSAGES,
-          ListChangesOption.REVIEWER_UPDATES,
-          ListChangesOption.SUBMITTABLE,
-          ListChangesOption.WEB_LINKS,
-          ListChangesOption.SKIP_DIFFSTAT,
-          ListChangesOption.SUBMIT_REQUIREMENTS,
-          ListChangesOption.PARENTS);
+      ImmutableSet.<ListChangesOption>builder()
+          .addAll(CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS)
+          .add(ListChangesOption.PARENTS)
+          .build();
 
   @Nullable
   public static String getPath(@Nullable String requestedURL) throws URISyntaxException {
diff --git a/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 871ec78..5eb600d 100644
--- a/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -20,7 +20,6 @@
 import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
 import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
 import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@@ -200,14 +199,7 @@
       return;
     }
 
-    String e = req.getParameter("e");
-    if (e != null && !r.etag.equals(e)) {
-      CacheHeaders.setNotCacheable(rsp);
-      rsp.setStatus(SC_NOT_FOUND);
-      return;
-    } else if (!requiresPostProcess
-        && cacheOnClient
-        && r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+    if (!requiresPostProcess && cacheOnClient && r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
       rsp.setStatus(SC_NOT_MODIFIED);
       return;
     }
@@ -230,11 +222,7 @@
       CacheHeaders.setNotCacheable(rsp);
     }
     if (!CacheHeaders.hasCacheHeader(rsp)) {
-      if (e != null && r.etag.equals(e)) {
-        CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
-      } else {
-        CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
-      }
+      CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
     }
     rsp.setContentType(r.contentType);
     rsp.setContentLength(tosend.length);
diff --git a/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index de53e64..e82f341 100644
--- a/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -188,7 +188,7 @@
     pluginOptions.onBeanParseStart();
     try {
       clp.parseOptionMap(in);
-    } catch (CmdLineException | NumberFormatException e) {
+    } catch (CmdLineException | IllegalArgumentException e) {
       if (!clp.wasHelpRequestedByOption()) {
         replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
         return false;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index e65d290..45a20b6 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -352,13 +352,14 @@
       // plugins happens before the client sees the response. This is needed for being able to
       // test performance logging from an acceptance test (see
       // TraceIT#performanceLoggingForRestCall()).
-      try (RequestStateContext requestStateContext =
+      try (PerformanceLogContext performanceLogContext =
+              new PerformanceLogContext(globals.config, globals.performanceLoggers);
+          RequestStateContext requestStateContext =
               RequestStateContext.open()
+                  .setPerformanceSummaryProvider(performanceLogContext)
                   .addRequestStateProvider(
                       globals.deadlineCheckerFactory.create(
-                          requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
-          PerformanceLogContext performanceLogContext =
-              new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
+                          requestInfo, req.getHeader(X_GERRIT_DEADLINE)))) {
         traceRequestData(req);
 
         if (corsResponder.filterCorsPreflight(req, res)) {
@@ -833,7 +834,7 @@
         traceContext,
         getViewName(viewData),
         ActionType.REST_READ_REQUEST,
-        () -> view.apply(rsrc));
+        () -> view.apply(req, rsrc));
   }
 
   private Response<?> invokeRestModifyViewWithRetry(
@@ -1660,10 +1661,8 @@
 
   private void checkRequiresCapability(ViewData d)
       throws AuthException, PermissionBackendException {
-    try {
-      globals.permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-    } catch (AuthException e) {
-      // Skiping
+    if (!globals.permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
+      // Not admin. Check for permissions from annotations.
       globals
           .permissionBackend
           .currentUser()
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 8b48fc0..a38b14c 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -23,6 +23,7 @@
         "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:proto_converter",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 0be0019..498b750 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -288,7 +290,10 @@
                 getRequestedFields());
         logger.atFine().log("Query options: %s", opts);
         // Apply index-specific rewrite first
-        Predicate<T> pred = rewriter.rewrite(q, opts);
+        Predicate<T> pred;
+        try (TraceTimer ignored = TraceContext.newTimer("rewriteQuery")) {
+          pred = rewriter.rewrite(q, opts);
+        }
         if (enforceVisibility) {
           pred = enforceVisibility(pred);
         }
@@ -312,26 +317,30 @@
       // Run each query asynchronously, if supported.
       List<ResultSet<T>> matches = new ArrayList<>(cnt);
       for (DataSource<T> s : sources) {
-        matches.add(s.read());
+        try (TraceTimer ignored = TraceContext.newTimer("readDataSource")) {
+          matches.add(s.read());
+        }
       }
 
       out = new ArrayList<>(cnt);
       for (int i = 0; i < cnt; i++) {
-        String queryString = queryStrings != null ? queryStrings.get(i) : null;
-        ImmutableList<T> matchesList = matches.get(i).toList();
-        int matchCount = matchesList.size();
-        int limit = limits.get(i);
-        logger.atFine().log(
-            "Matches[%d]:\n%s",
-            i, matchesList.stream().map(this::formatForLogging).collect(toList()));
-        // TODO(brohlfs): Remove this extra logging by end of Q3 2023.
-        if (limit > 500 && userProvidedLimit <= 0 && matchCount > 100 && enforceVisibility) {
-          logger.atWarning().log(
-              "%s index query without provided limit. effective limit: %d, result count: %d, query:"
-                  + " %s",
-              schemaDef.getName(), getPermittedLimit(), matchCount, queryString);
+        try (TraceTimer ignored = TraceContext.newTimer("createQueryResult")) {
+          String queryString = queryStrings != null ? queryStrings.get(i) : null;
+          ImmutableList<T> matchesList = matches.get(i).toList();
+          int matchCount = matchesList.size();
+          int limit = limits.get(i);
+          logger.atFine().log(
+              "Matches[%d]:\n%s",
+              i, matchesList.stream().map(this::formatForLogging).collect(toList()));
+          // TODO(brohlfs): Remove this extra logging by end of Q3 2023.
+          if (limit > 500 && userProvidedLimit <= 0 && matchCount > 100 && enforceVisibility) {
+            logger.atWarning().log(
+                "%s index query without provided limit. effective limit: %d, result count: %d,"
+                    + " query: %s",
+                schemaDef.getName(), getPermittedLimit(), matchCount, queryString);
+          }
+          out.add(QueryResult.create(queryString, predicates.get(i), limit, matchesList));
         }
-        out.add(QueryResult.create(queryString, predicates.get(i), limit, matchesList));
       }
 
       // Only measure successful queries that actually touched the index.
diff --git a/java/com/google/gerrit/index/testing/BUILD b/java/com/google/gerrit/index/testing/BUILD
index c9f1994..b39c6ce 100644
--- a/java/com/google/gerrit/index/testing/BUILD
+++ b/java/com/google/gerrit/index/testing/BUILD
@@ -10,6 +10,7 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:converters",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/server",
diff --git a/java/com/google/gerrit/index/testing/FakeIndexModuleOnInit.java b/java/com/google/gerrit/index/testing/FakeIndexModuleOnInit.java
deleted file mode 100644
index 75d8de2..0000000
--- a/java/com/google/gerrit/index/testing/FakeIndexModuleOnInit.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.index.testing;
-
-import com.google.gerrit.index.testing.AbstractFakeIndex.FakeAccountIndex;
-import com.google.gerrit.index.testing.AbstractFakeIndex.FakeGroupIndex;
-import com.google.gerrit.server.index.account.AccountIndex;
-import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.inject.AbstractModule;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
-
-public class FakeIndexModuleOnInit extends AbstractModule {
-  @Override
-  protected void configure() {
-    install(
-        new FactoryModuleBuilder()
-            .implement(AccountIndex.class, FakeAccountIndex.class)
-            .build(AccountIndex.Factory.class));
-
-    install(
-        new FactoryModuleBuilder()
-            .implement(GroupIndex.class, FakeGroupIndex.class)
-            .build(GroupIndex.Factory.class));
-  }
-}
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index a02289e..e42a11d 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -26,6 +26,8 @@
         ":query_builder",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:converters",
+        "//java/com/google/gerrit/entities/converter:proto_converter",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
diff --git a/java/com/google/gerrit/metrics/Field.java b/java/com/google/gerrit/metrics/Field.java
index 01b89cf..5f5218b 100644
--- a/java/com/google/gerrit/metrics/Field.java
+++ b/java/com/google/gerrit/metrics/Field.java
@@ -155,8 +155,7 @@
     }
   }
 
-  private static final Pattern SUBMETRIC_NAME_PATTERN =
-      Pattern.compile("[a-zA-Z0-9_-]+([a-zA-Z0-9_-]+)*");
+  private static final Pattern SUBMETRIC_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+");
   private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("[^\\w-]");
   private static final String REPLACEMENT_PREFIX = "_0x";
 
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 0967130..c07047d 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -37,6 +37,7 @@
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/mem",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index a4b8caa..b3e4dfc 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.auth.AuthModule;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.GerritAuthModule;
@@ -62,6 +63,8 @@
 import com.google.gerrit.server.StartupChecks.StartupChecksModule;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountDeactivator.AccountDeactivatorModule;
+import com.google.gerrit.server.account.AuthTokenModule;
+import com.google.gerrit.server.account.CachingAuthTokenModule;
 import com.google.gerrit.server.account.InternalAccountDirectory.InternalAccountDirectoryModule;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCaseSensitivityMigrator;
@@ -88,6 +91,7 @@
 import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SysExecutorModule;
 import com.google.gerrit.server.events.EventBroker.EventBrokerModule;
 import com.google.gerrit.server.events.StreamEventsApiListener.StreamEventsApiListenerModule;
@@ -100,6 +104,7 @@
 import com.google.gerrit.server.index.VersionManager;
 import com.google.gerrit.server.index.options.AutoFlush;
 import com.google.gerrit.server.index.scheduler.PeriodicIndexScheduler;
+import com.google.gerrit.server.logging.LoggingContext;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier.SignedTokenEmailTokenVerifierModule;
 import com.google.gerrit.server.mail.receive.MailReceiver.MailReceiverModule;
@@ -218,6 +223,8 @@
   private Injector dbInjector;
   private Injector cfgInjector;
   private Config config;
+  private LogConfig logConfig;
+  private AuthConfig authConfig;
   private Injector sysInjector;
   private Injector sshInjector;
   private Injector webInjector;
@@ -285,7 +292,15 @@
     }
     mustHaveValidSite();
     Thread.setDefaultUncaughtExceptionHandler(
-        (t, e) -> logger.atSevere().withCause(e).log("Thread %s threw exception", t.getName()));
+        (t, e) -> {
+          logger.atSevere().withCause(e).log("Thread %s threw exception", t.getName());
+
+          // Clear the logging context to prevent that it leaks into other threads.
+          if (!LoggingContext.getInstance().isEmpty()) {
+            logger.atInfo().log("Clearing LoggingContext after uncaught exception");
+            LoggingContext.getInstance().clear();
+          }
+        });
 
     if (runId != null) {
       runFile = getSitePath().resolve("logs").resolve("gerrit.run");
@@ -396,12 +411,14 @@
     }
     cfgInjector = createCfgInjector();
     config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    authConfig = cfgInjector.getInstance(AuthConfig.class);
     indexType = IndexModule.getIndexType(cfgInjector);
     sysInjector = createSysInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class).setDbCfgInjector(dbInjector, cfgInjector);
     manager.add(dbInjector, cfgInjector, sysInjector);
 
-    manager.add(ErrorLogFile.start(getSitePath(), config, consoleLog));
+    logConfig = sysInjector.getInstance(LogConfig.class);
+    manager.add(ErrorLogFile.start(getSitePath(), logConfig, consoleLog));
 
     sshd &= !sshdOff();
     if (sshd) {
@@ -550,6 +567,15 @@
     } else {
       modules.add(NoSshKeyCache.module());
     }
+    boolean useAuthTokenCache =
+        authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP
+            || authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP_LDAP;
+    if (useAuthTokenCache) {
+      modules.add(new CachingAuthTokenModule());
+    } else {
+      modules.add(new AuthTokenModule());
+    }
+
     modules.add(
         new AbstractModule() {
           @Override
diff --git a/java/com/google/gerrit/pgm/MigratePasswordsToTokens.java b/java/com/google/gerrit/pgm/MigratePasswordsToTokens.java
new file mode 100644
index 0000000..8c6e352
--- /dev/null
+++ b/java/com/google/gerrit/pgm/MigratePasswordsToTokens.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.init.VersionedAuthTokensOnInit;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InstallAllPlugins;
+import com.google.gerrit.pgm.init.api.InstallPlugins;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.pgm.util.BatchProgramModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.server.DefaultRefLogIdentityProvider;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AuthTokenCache;
+import com.google.gerrit.server.account.AuthTokenModule;
+import com.google.gerrit.server.account.PasswordMigrator;
+import com.google.gerrit.server.account.VersionedAuthTokens;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbReadStorageModule;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbWriteStorageModule;
+import com.google.gerrit.server.account.storage.notedb.AccountNoteDbReadStorageModule;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.index.options.AutoFlush;
+import com.google.gerrit.server.index.project.ProjectIndexerImpl;
+import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
+import com.google.gerrit.server.notedb.NoteDbStarredChangesModule;
+import com.google.gerrit.server.project.DefaultLockManager;
+import com.google.gerrit.server.project.LockManager;
+import com.google.gerrit.server.restapi.account.CreateToken;
+import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.TypeLiteral;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.kohsuke.args4j.Option;
+
+/** Converts HTTP passwords for all accounts to tokens */
+public class MigratePasswordsToTokens extends SiteProgram {
+  private final LifecycleManager manager = new LifecycleManager();
+
+  private Optional<Instant> lifetime = Optional.empty();
+
+  @Inject private AuthConfig authConfig;
+  @Inject private PasswordMigrator.Factory passwordMigratorFactory;
+
+  @Option(name = "--lifetime", usage = "The lifetime of migrated tokens.")
+  public void setDefaultLifetime(String value) throws BadRequestException {
+    lifetime = CreateToken.getExpirationInstant(value, Optional.empty());
+  }
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+    ConsoleUI ui = ConsoleUI.getInstance();
+
+    Injector dbInjector = createDbInjector();
+    manager.add(dbInjector, dbInjector.createChildInjector(NoteDbSchemaVersionCheck.module()));
+    manager.start();
+    dbInjector
+        .createChildInjector(
+            new FactoryModule() {
+              @Override
+              protected void configure() {
+                bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
+                bind(ConsoleUI.class).toInstance(ui);
+                bind(Boolean.class)
+                    .annotatedWith(InstallAllPlugins.class)
+                    .toInstance(Boolean.FALSE);
+                bind(new TypeLiteral<List<String>>() {})
+                    .annotatedWith(InstallPlugins.class)
+                    .toInstance(new ArrayList<>());
+                bind(LockManager.class).toInstance(new DefaultLockManager());
+
+                factory(PasswordMigrator.Factory.class);
+                factory(MetaDataUpdate.InternalFactory.class);
+                factory(VersionedAuthTokens.Factory.class);
+                factory(VersionedAuthTokensOnInit.Factory.class);
+                factory(Section.Factory.class);
+                factory(MultiProgressMonitor.Factory.class);
+                factory(ProjectIndexerImpl.Factory.class);
+                factory(ChangeResource.Factory.class);
+                bind(IdentifiedUser.GenericFactory.class);
+
+                install(new ExternalIdNoteDbReadStorageModule());
+                install(new ExternalIdNoteDbWriteStorageModule());
+                install(new ExternalIdCacheImpl.ExternalIdCacheModule());
+                install(new ExternalIdCacheImpl.ExternalIdCacheBindingModule());
+                install(new DefaultRefLogIdentityProvider.Module());
+                install(new BatchProgramModule(dbInjector, ImmutableSet.of()));
+                install(LuceneIndexModule.latestVersion(false, AutoFlush.ENABLED));
+                install(new AccountNoteDbReadStorageModule());
+                install(new NoteDbStarredChangesModule());
+                install(new NoteDbDraftCommentsModule());
+                install(new WorkQueueModule());
+                install(AuthTokenCache.module());
+                install(new AuthTokenModule());
+              }
+            })
+        .injectMembers(this);
+
+    Optional<Duration> maxTokenLifetime = authConfig.getMaxAuthTokenLifetime();
+    if (maxTokenLifetime.isPresent()) {
+      if (lifetime.isEmpty()
+          || lifetime.get().isAfter(Instant.now().plus(maxTokenLifetime.get()))) {
+        throw new BadRequestException(
+            "A lifetime shorter than the maximum allowed lifetime has to be provided.");
+      }
+    }
+
+    passwordMigratorFactory.create(lifetime).run();
+
+    manager.stop();
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/pgm/ReduceMaxTokenLifetime.java b/java/com/google/gerrit/pgm/ReduceMaxTokenLifetime.java
new file mode 100644
index 0000000..835f557
--- /dev/null
+++ b/java/com/google/gerrit/pgm/ReduceMaxTokenLifetime.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.init.VersionedAuthTokensOnInit;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InstallAllPlugins;
+import com.google.gerrit.pgm.init.api.InstallPlugins;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.pgm.util.BatchProgramModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AuthTokenModule;
+import com.google.gerrit.server.account.MaxAuthTokenLifetimeApplier;
+import com.google.gerrit.server.account.VersionedAuthTokens;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbReadStorageModule;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbWriteStorageModule;
+import com.google.gerrit.server.account.storage.notedb.AccountNoteDbReadStorageModule;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.index.options.AutoFlush;
+import com.google.gerrit.server.index.project.ProjectIndexerImpl;
+import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
+import com.google.gerrit.server.notedb.NoteDbStarredChangesModule;
+import com.google.gerrit.server.project.DefaultLockManager;
+import com.google.gerrit.server.project.LockManager;
+import com.google.gerrit.server.restapi.account.CreateToken;
+import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.TypeLiteral;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.kohsuke.args4j.Option;
+
+/** Reduces token lifetime if they exceed a given max lifetime */
+public class ReduceMaxTokenLifetime extends SiteProgram {
+  private final LifecycleManager manager = new LifecycleManager();
+
+  private Instant expiryInstant;
+
+  @Inject private MaxAuthTokenLifetimeApplier.Factory maxAuthTokenLifetimeApplierFactory;
+
+  @Option(name = "--lifetime", usage = "The lifetime of migrated tokens.", required = true)
+  public void setMaxLifetime(String value) throws BadRequestException {
+    expiryInstant = CreateToken.getExpirationInstant(value, Optional.empty()).get();
+  }
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+    ConsoleUI ui = ConsoleUI.getInstance();
+
+    Injector dbInjector = createDbInjector();
+    manager.add(dbInjector, dbInjector.createChildInjector(NoteDbSchemaVersionCheck.module()));
+    manager.start();
+    dbInjector
+        .createChildInjector(
+            new FactoryModule() {
+              @Override
+              protected void configure() {
+                bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
+                bind(ConsoleUI.class).toInstance(ui);
+                bind(Boolean.class)
+                    .annotatedWith(InstallAllPlugins.class)
+                    .toInstance(Boolean.FALSE);
+                bind(new TypeLiteral<List<String>>() {})
+                    .annotatedWith(InstallPlugins.class)
+                    .toInstance(new ArrayList<>());
+                bind(IdentifiedUser.GenericFactory.class);
+                bind(LockManager.class).toInstance(new DefaultLockManager());
+
+                factory(MetaDataUpdate.InternalFactory.class);
+                factory(VersionedAuthTokens.Factory.class);
+                factory(VersionedAuthTokensOnInit.Factory.class);
+                factory(Section.Factory.class);
+                factory(MultiProgressMonitor.Factory.class);
+                factory(ProjectIndexerImpl.Factory.class);
+                factory(ChangeResource.Factory.class);
+
+                install(new ExternalIdNoteDbReadStorageModule());
+                install(new ExternalIdNoteDbWriteStorageModule());
+                install(new ExternalIdCacheImpl.ExternalIdCacheModule());
+                install(new ExternalIdCacheImpl.ExternalIdCacheBindingModule());
+                install(LuceneIndexModule.latestVersion(false, AutoFlush.ENABLED));
+                install(new WorkQueueModule());
+                install(new BatchProgramModule(dbInjector, ImmutableSet.of()));
+                install(new AuthTokenModule());
+                install(new AccountNoteDbReadStorageModule());
+                install(new NoteDbStarredChangesModule());
+                install(new NoteDbDraftCommentsModule());
+              }
+            })
+        .injectMembers(this);
+
+    maxAuthTokenLifetimeApplierFactory.create(expiryInstant).run();
+    return 0;
+  }
+}
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index c5e133f..7c0f258 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -58,6 +58,7 @@
 import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.multibindings.OptionalBinder;
+import com.google.inject.name.Named;
 import java.io.StringWriter;
 import java.io.Writer;
 import java.lang.reflect.InvocationTargetException;
@@ -68,6 +69,7 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 import org.eclipse.jgit.lib.Config;
@@ -100,6 +102,12 @@
   @Option(name = "--build-bloom-filter", usage = "Build bloom filter for H2 disk caches.")
   private boolean buildBloomFilter;
 
+  @Option(
+      name = "--read-only-disk-caches",
+      aliases = {"--ro-disk-caches"},
+      usage = "Don't update H2 disk caches.")
+  private boolean readOnlyDiskCaches;
+
   private Boolean reuseExistingDocumentsOption;
 
   private Injector dbInjector;
@@ -111,6 +119,9 @@
   @Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
   @Inject private DynamicMap<Cache<?, ?>> cacheMap;
 
+  @Inject
+  private @Named("DiskCacheReadOnly") AtomicBoolean isDiskCacheReadOnly;
+
   @Option(name = "--reuse", usage = "Reindex only when existing index entry is stale")
   public void setReuseExistingDocuments(boolean value) {
     reuseExistingDocumentsOption = value;
@@ -139,6 +150,7 @@
 
     sysInjector.injectMembers(this);
     checkIndicesOption();
+    isDiskCacheReadOnly.set(readOnlyDiskCaches);
 
     try {
       boolean ok = list ? list() : reindex();
diff --git a/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index 268e25b..8ad89b2 100644
--- a/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.httpd.RequestMetricsFilter;
 import com.google.gerrit.httpd.restapi.LogRedactUtil;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -33,7 +33,6 @@
 import org.eclipse.jetty.server.RequestLog;
 import org.eclipse.jetty.server.Response;
 import org.eclipse.jetty.util.component.AbstractLifeCycle;
-import org.eclipse.jgit.lib.Config;
 
 /** Writes the {@code httpd_log} file with per-request data. */
 class HttpLog extends AbstractLifeCycle implements RequestLog {
@@ -64,17 +63,14 @@
   private final AsyncAppender async;
 
   @Inject
-  HttpLog(SystemLog systemLog, @GerritServerConfig Config config) {
-    boolean json = config.getBoolean("log", "jsonLogging", false);
-    boolean text = config.getBoolean("log", "textLogging", true) || !json;
-
+  HttpLog(SystemLog systemLog, LogConfig config) {
     async = new AsyncAppender();
 
-    if (text) {
+    if (config.isTextLogging()) {
       async.addAppender(systemLog.createAsyncAppender(LOG_NAME, new HttpLogLayout()));
     }
 
-    if (json) {
+    if (config.isJsonLogging()) {
       async.addAppender(
           systemLog.createAsyncAppender(LOG_NAME + JSON_SUFFIX, new HttpLogJsonLayout()));
     }
diff --git a/java/com/google/gerrit/pgm/init/BUILD b/java/com/google/gerrit/pgm/init/BUILD
index 73c3760..5928eea 100644
--- a/java/com/google/gerrit/pgm/init/BUILD
+++ b/java/com/google/gerrit/pgm/init/BUILD
@@ -14,7 +14,6 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/launcher",
-        "//java/com/google/gerrit/lucene",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/pgm/init/api",
         "//java/com/google/gerrit/pgm/util",
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index e0eb773..3e51b98 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.common.JarUtil;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.IndexType;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -32,16 +31,12 @@
 import com.google.gerrit.pgm.init.api.InstallAllPlugins;
 import com.google.gerrit.pgm.init.api.InstallPlugins;
 import com.google.gerrit.pgm.init.api.LibraryDownload;
-import com.google.gerrit.pgm.init.index.IndexManagerOnInit;
-import com.google.gerrit.pgm.init.index.IndexModuleOnInit;
-import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.config.GerritOptions;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.plugins.JarScanner;
 import com.google.gerrit.server.schema.NoteDbSchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
@@ -59,7 +54,6 @@
 import com.google.inject.util.Providers;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -116,22 +110,16 @@
       init.flags.deleteOnFailure = false;
 
       Injector sysInjector = createSysInjector(init);
-      IndexManagerOnInit indexManager = sysInjector.getInstance(IndexManagerOnInit.class);
+      run = createSiteRun(init);
       try {
-        indexManager.start();
-        run = createSiteRun(init);
-        try {
-          run.upgradeSchema();
-        } catch (StorageException e) {
-          String msg = "Couldn't upgrade schema. Expected if slave and read-only database";
-          System.err.println(msg);
-          logger.atSevere().withCause(e).log("%s", msg);
-        }
-
-        init.initializer.postRun(sysInjector);
-      } finally {
-        indexManager.stop();
+        run.upgradeSchema();
+      } catch (StorageException e) {
+        String msg = "Couldn't upgrade schema. Expected if slave and read-only database";
+        System.err.println(msg);
+        logger.atSevere().withCause(e).log("%s", msg);
       }
+
+      init.initializer.postRun(sysInjector);
     } catch (Exception | Error failure) {
       if (init.flags.deleteOnFailure) {
         recursiveDelete(getSitePath());
@@ -415,26 +403,6 @@
             }
           });
       Injector dbInjector = createDbInjector();
-
-      IndexType indexType = IndexModule.getIndexType(dbInjector);
-      if (indexType.isLucene()) {
-        modules.add(new LuceneIndexModuleOnInit());
-      } else if (indexType.isFake()) {
-        try {
-          Class<?> clazz = Class.forName("com.google.gerrit.index.testing.FakeIndexModuleOnInit");
-          Module indexOnInitModule = (Module) clazz.getDeclaredConstructor().newInstance();
-          modules.add(indexOnInitModule);
-        } catch (InstantiationException
-            | IllegalAccessException
-            | ClassNotFoundException
-            | NoSuchMethodException
-            | InvocationTargetException e) {
-          throw new IllegalStateException("unable to create fake index", e);
-        }
-        modules.add(new IndexModuleOnInit());
-      } else {
-        throw new IllegalStateException("unsupported index.type = " + indexType);
-      }
       sysInjector = dbInjector.createChildInjector(modules);
     }
     return sysInjector;
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 1b393db..a657ad1 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -50,6 +50,7 @@
   private final ConsoleUI ui;
   private final AccountsOnInit accounts;
   private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
+  private final VersionedAuthTokensOnInit.Factory tokenFactory;
   private final ExternalIdsOnInit externalIds;
   private final SequencesOnInit sequencesOnInit;
   private final GroupsOnInit groupsOnInit;
@@ -63,6 +64,7 @@
       ConsoleUI ui,
       AccountsOnInit accounts,
       VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
+      VersionedAuthTokensOnInit.Factory tokenFactory,
       ExternalIdsOnInit externalIds,
       SequencesOnInit sequencesOnInit,
       GroupsOnInit groupsOnInit,
@@ -71,6 +73,7 @@
     this.ui = ui;
     this.accounts = accounts;
     this.authorizedKeysFactory = authorizedKeysFactory;
+    this.tokenFactory = tokenFactory;
     this.externalIds = externalIds;
     this.sequencesOnInit = sequencesOnInit;
     this.groupsOnInit = groupsOnInit;
@@ -106,12 +109,12 @@
         Account.Id id = Account.id(sequencesOnInit.nextAccountId());
         String username = ui.readString("admin", "username");
         String name = ui.readString("Administrator", "name");
-        String httpPassword = ui.readString("secret", "HTTP password");
+        String token = ui.readString("secret", "Authentication token");
         AccountSshKey sshKey = readSshKey(id);
         String email = readEmail(sshKey);
 
         List<ExternalId> extIds = new ArrayList<>(2);
-        extIds.add(externalIdFactory.createUsername(username, id, httpPassword));
+        extIds.add(externalIdFactory.createUsername(username, id));
 
         if (email != null) {
           extIds.add(externalIdFactory.createEmail(id, email));
@@ -134,6 +137,10 @@
         GroupReference adminGroup = adminGroupReference.get();
         groupsOnInit.addGroupMember(adminGroup.getUUID(), persistedAccount);
 
+        VersionedAuthTokensOnInit authTokens = tokenFactory.create(id).load();
+        authTokens.addToken("initialToken", token, Optional.empty());
+        authTokens.save("Add token for initial admin user\n");
+
         if (sshKey != null) {
           VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
           authorizedKeys.addKey(sshKey.sshPublicKey());
diff --git a/java/com/google/gerrit/pgm/init/InitModule.java b/java/com/google/gerrit/pgm/init/InitModule.java
index f36ec3d..2b5abc8 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -50,6 +50,7 @@
         .toProvider(DisabledGitRefUpdatedRepoAccountsSequenceProvider.class);
     factory(Section.Factory.class);
     factory(VersionedAuthorizedKeysOnInit.Factory.class);
+    factory(VersionedAuthTokensOnInit.Factory.class);
 
     // Steps are executed in the order listed here.
     //
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index d0d03b5..a591121 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -109,6 +109,12 @@
     extractMailExample("AddKeyHtml.soy");
     extractMailExample("AddToAttentionSet.soy");
     extractMailExample("AddToAttentionSetHtml.soy");
+    extractMailExample("AuthTokenExpired.soy");
+    extractMailExample("AuthTokenExpiredHtml.soy");
+    extractMailExample("AuthTokenWillExpire.soy");
+    extractMailExample("AuthTokenWillExpireHtml.soy");
+    extractMailExample("AuthTokenUpdate.soy");
+    extractMailExample("AuthTokenUpdateHtml.soy");
     extractMailExample("ChangeFooter.soy");
     extractMailExample("ChangeFooterHtml.soy");
     extractMailExample("ChangeSubject.soy");
diff --git a/java/com/google/gerrit/pgm/init/VersionedAuthTokensOnInit.java b/java/com/google/gerrit/pgm/init/VersionedAuthTokensOnInit.java
new file mode 100644
index 0000000..a112987
--- /dev/null
+++ b/java/com/google/gerrit/pgm/init/VersionedAuthTokensOnInit.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Strings;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.VersionedMetaDataOnInit;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
+import com.google.gerrit.server.account.VersionedAuthTokens;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+
+public class VersionedAuthTokensOnInit extends VersionedMetaDataOnInit {
+  public interface Factory {
+    VersionedAuthTokensOnInit create(Account.Id accountId);
+  }
+
+  private Map<String, AuthToken> tokens;
+
+  @Inject
+  public VersionedAuthTokensOnInit(
+      AllUsersNameOnInitProvider allUsers,
+      SitePaths site,
+      InitFlags flags,
+      @Assisted Account.Id accountId) {
+    super(flags, site, allUsers.get(), RefNames.refsUsers(accountId));
+  }
+
+  @Override
+  public VersionedAuthTokensOnInit load() throws IOException, ConfigInvalidException {
+    super.load();
+    return this;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    tokens = VersionedAuthTokens.parse(readUTF8(VersionedAuthTokens.FILE_NAME));
+  }
+
+  @CanIgnoreReturnValue
+  public AuthToken addToken(String id, String t, Optional<Instant> expirationDate)
+      throws InvalidAuthTokenException {
+    checkState(tokens != null, "Tokens not loaded yet");
+    AuthToken token = AuthToken.createWithPlainToken(id, t, expirationDate);
+    tokens.put(id, token);
+    return token;
+  }
+
+  public void updateToken(AuthToken token) {
+    checkState(tokens != null, "Tokens not loaded yet");
+    tokens.remove(token.id());
+    tokens.put(token.id(), token);
+  }
+
+  @Nullable
+  public AuthToken getToken(String id) {
+    checkState(tokens != null, "Tokens not loaded yet");
+    return tokens.get(id);
+  }
+
+  public List<AuthToken> getTokens() {
+    checkState(tokens != null, "Tokens not loaded yet");
+    return List.copyOf(tokens.values());
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated tokens\n");
+    }
+
+    Config tokenConfig = new Config();
+    for (AuthToken token : tokens.values()) {
+      tokenConfig.setString("token", token.id(), "hash", token.hashedToken());
+      if (token.expirationDate().isPresent()) {
+        tokenConfig.setString(
+            "token", token.id(), "expiration", token.expirationDate().get().toString());
+      }
+    }
+
+    saveUTF8(VersionedAuthTokens.FILE_NAME, tokenConfig.toText());
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/pgm/init/index/IndexManagerOnInit.java b/java/com/google/gerrit/pgm/init/index/IndexManagerOnInit.java
deleted file mode 100644
index 6b1ee26..0000000
--- a/java/com/google/gerrit/pgm/init/index/IndexManagerOnInit.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init.index;
-
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.index.IndexDefinition;
-import com.google.inject.Inject;
-import com.google.inject.name.Named;
-import java.util.Collection;
-
-/**
- * This class starts/stops the indexes from the init program so that init can write updates the
- * indexes.
- */
-public class IndexManagerOnInit {
-  private final LifecycleListener indexManager;
-  private final Collection<IndexDefinition<?, ?, ?>> defs;
-
-  @Inject
-  IndexManagerOnInit(
-      @Named(IndexModuleOnInit.INDEX_MANAGER) LifecycleListener indexManager,
-      Collection<IndexDefinition<?, ?, ?>> defs) {
-    this.indexManager = indexManager;
-    this.defs = defs;
-  }
-
-  public void start() {
-    indexManager.start();
-
-    for (IndexDefinition<?, ?, ?> def : defs) {
-      def.getIndexCollection().start();
-    }
-  }
-
-  public void stop() {
-    indexManager.stop();
-
-    for (IndexDefinition<?, ?, ?> def : defs) {
-      def.getIndexCollection().stop();
-    }
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java b/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
deleted file mode 100644
index 65e7493..0000000
--- a/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init.index;
-
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.index.IndexDefinition;
-import com.google.gerrit.index.SchemaDefinitions;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.index.SingleVersionModule;
-import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
-import com.google.gerrit.server.index.account.AccountIndexCollection;
-import com.google.gerrit.server.index.account.AccountIndexDefinition;
-import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
-import com.google.gerrit.server.index.account.AllAccountsIndexer;
-import com.google.gerrit.server.index.group.AllGroupsIndexer;
-import com.google.gerrit.server.index.group.GroupIndexDefinition;
-import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Names;
-import com.google.inject.util.Providers;
-import java.util.Collection;
-import java.util.Map;
-
-public class IndexModuleOnInit extends AbstractModule {
-  static final String INDEX_MANAGER = "IndexModuleOnInit/IndexManager";
-
-  private static final ImmutableList<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
-      ImmutableList.of(AccountSchemaDefinitions.INSTANCE, GroupSchemaDefinitions.INSTANCE);
-
-  @Override
-  protected void configure() {
-    // The LuceneAccountIndex needs AccountCache only for reading from the index.
-    // On init we only want to write to the index, hence we don't need the
-    // account cache.
-    bind(AccountCache.class).toProvider(Providers.of(null));
-
-    // AccountIndexDefinition wants to have AllAccountsIndexer but it is only
-    // used by the Reindex program and the OnlineReindexer which are both not
-    // used during init, hence we don't need AllAccountsIndexer.
-    bind(AllAccountsIndexer.class).toProvider(Providers.of(null));
-
-    bind(AccountIndexCollection.class);
-
-    // The LuceneGroupIndex needs GroupCache only for reading from the index. On init we only want
-    // to
-    // write to the index, hence we don't need the group cache.
-    bind(GroupCache.class).toProvider(Providers.of(null));
-
-    // GroupIndexDefinition wants to have AllGroupsIndexer but it is only used
-    // by the Reindex program and the OnlineReindexer which are both not used
-    // during init, hence we don't need AllGroupsIndexer.
-    bind(AllGroupsIndexer.class).toProvider(Providers.of(null));
-
-    bind(new TypeLiteral<Map<String, Integer>>() {})
-        .annotatedWith(Names.named(SingleVersionModule.SINGLE_VERSIONS))
-        .toInstance(ImmutableMap.of());
-    bind(LifecycleListener.class)
-        .annotatedWith(Names.named(INDEX_MANAGER))
-        .to(SingleVersionListener.class);
-  }
-
-  @Provides
-  Collection<IndexDefinition<?, ?, ?>> getIndexDefinitions(
-      AccountIndexDefinition accounts, GroupIndexDefinition groups) {
-    ImmutableList<IndexDefinition<?, ?, ?>> result = ImmutableList.of(accounts, groups);
-    ImmutableSet<String> expected =
-        FluentIterable.from(ALL_SCHEMA_DEFS).transform(SchemaDefinitions::getName).toSet();
-    ImmutableSet<String> actual =
-        FluentIterable.from(result).transform(IndexDefinition::getName).toSet();
-    if (!expected.equals(actual)) {
-      throw new ProvisionException(
-          "need index definitions for all schemas: " + expected + " != " + actual);
-    }
-    return result;
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/index/lucene/LuceneIndexModuleOnInit.java b/java/com/google/gerrit/pgm/init/index/lucene/LuceneIndexModuleOnInit.java
deleted file mode 100644
index 078e648..0000000
--- a/java/com/google/gerrit/pgm/init/index/lucene/LuceneIndexModuleOnInit.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init.index.lucene;
-
-import com.google.gerrit.lucene.LuceneAccountIndex;
-import com.google.gerrit.lucene.LuceneGroupIndex;
-import com.google.gerrit.pgm.init.index.IndexModuleOnInit;
-import com.google.gerrit.server.index.account.AccountIndex;
-import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gerrit.server.index.options.AutoFlush;
-import com.google.inject.AbstractModule;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
-
-public class LuceneIndexModuleOnInit extends AbstractModule {
-  @Override
-  protected void configure() {
-    install(
-        new FactoryModuleBuilder()
-            .implement(AccountIndex.class, LuceneAccountIndex.class)
-            .build(AccountIndex.Factory.class));
-
-    install(
-        new FactoryModuleBuilder()
-            .implement(GroupIndex.class, LuceneGroupIndex.class)
-            .build(GroupIndex.Factory.class));
-
-    install(new IndexModuleOnInit());
-
-    bind(AutoFlush.class).toInstance(AutoFlush.DISABLED);
-  }
-}
diff --git a/java/com/google/gerrit/pgm/init/index/package-info.java b/java/com/google/gerrit/pgm/init/index/package-info.java
deleted file mode 100644
index cb32880..0000000
--- a/java/com/google/gerrit/pgm/init/index/package-info.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-@CheckReturnValue
-package com.google.gerrit.pgm.init.index;
-
-import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 7e5ae2e..d59fa2f 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.common.AccountVisibility;
@@ -223,7 +224,8 @@
     modules.add(new IgnoreSelfApprovalRuleModule());
     modules.add(new SkipCurrentRulesEvaluationOnClosedChangesModule());
 
-    // Global submit requirements
+    // Global labels & submit requirements
+    DynamicSet.setOf(binder(), LabelType.class);
     DynamicSet.setOf(binder(), SubmitRequirement.class);
 
     factory(FileEditsPredicate.Factory.class);
diff --git a/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index 3d8ac8d..96d2fa4 100644
--- a/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.util.logging.LogTimestampFormatter;
@@ -26,7 +27,6 @@
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
-import org.eclipse.jgit.lib.Config;
 
 public class ErrorLogFile {
   static final String LOG_NAME = "error_log";
@@ -49,7 +49,7 @@
     root.addAppender(dst);
   }
 
-  public static LifecycleListener start(Path sitePath, Config config, boolean consoleLog)
+  public static LifecycleListener start(Path sitePath, LogConfig config, boolean consoleLog)
       throws IOException {
     Path logdir =
         FileUtil.mkdirsOrDie(new SitePaths(sitePath).logs_dir, "Cannot create log directory");
@@ -68,7 +68,7 @@
     };
   }
 
-  private static void initLogSystem(Path logdir, Config config, boolean consoleLog) {
+  private static void initLogSystem(Path logdir, LogConfig config, boolean consoleLog) {
     Logger root = LogManager.getRootLogger();
     root.removeAllAppenders();
 
@@ -86,15 +86,13 @@
       root.addAppender(dst);
     }
 
-    boolean json = config.getBoolean("log", "jsonLogging", false);
-    boolean text = config.getBoolean("log", "textLogging", true) || !(json || consoleLog);
-    boolean rotate = config.getBoolean("log", "rotate", true);
+    boolean rotate = config.shouldRotate();
 
-    if (text) {
+    if (config.isTextLogging() || !consoleLog) {
       root.addAppender(SystemLog.createAppender(logdir, LOG_NAME, errorLogLayout, rotate));
     }
 
-    if (json) {
+    if (config.isJsonLogging()) {
       root.addAppender(
           SystemLog.createAppender(
               logdir, LOG_NAME + JSON_SUFFIX, new ErrorLogJsonLayout(), rotate));
diff --git a/java/com/google/gerrit/pgm/util/LogFileManager.java b/java/com/google/gerrit/pgm/util/LogFileManager.java
index 902f7d64..ca491bd 100644
--- a/java/com/google/gerrit/pgm/util/LogFileManager.java
+++ b/java/com/google/gerrit/pgm/util/LogFileManager.java
@@ -22,8 +22,7 @@
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
@@ -42,11 +41,9 @@
 import java.time.temporal.ChronoUnit;
 import java.util.Optional;
 import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.GZIPOutputStream;
-import org.eclipse.jgit.lib.Config;
 
 /** Compresses and eventually deletes the old logs. */
 public class LogFileManager implements Runnable {
@@ -99,21 +96,10 @@
   private final Path logs_dir;
 
   @Inject
-  LogFileManager(SitePaths site, @GerritServerConfig Config config) {
+  LogFileManager(SitePaths site, LogConfig config) {
     this.logs_dir = resolve(site.logs_dir);
-    this.compressionEnabled = config.getBoolean("log", "compress", true);
-    this.timeToKeep = getTimeToKeep(config);
-  }
-
-  private Duration getTimeToKeep(Config config) {
-    try {
-      return Duration.ofDays(
-          ConfigUtil.getTimeUnit(config, "log", null, "timeToKeep", -1, TimeUnit.DAYS));
-    } catch (IllegalArgumentException e) {
-      logger.atWarning().withCause(e).log(
-          "Illegal duration value for log deletion. Disabling log deletion.");
-      return Duration.ofDays(-1L);
-    }
+    this.compressionEnabled = config.shouldCompress();
+    this.timeToKeep = config.getTimeToKeep();
   }
 
   private static Path resolve(Path p) {
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 7225e3d..edede84 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -43,6 +43,8 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:converters",
+        "//java/com/google/gerrit/entities/converter:proto_converter",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
@@ -66,7 +68,6 @@
         "//java/com/google/gerrit/server/util/git",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/util/cli",
-        "//java/com/google/gerrit/util/concurrent",
         "//java/org/apache/commons/net",
         "//lib:args4j",
         "//lib:autolink",
@@ -129,6 +130,7 @@
         ":server",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/server/flow",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 00f73ec..1ed962d 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -47,7 +46,6 @@
 import com.google.gerrit.server.patch.DiffOperations;
 import com.google.gerrit.server.patch.DiffOptions;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
-import com.google.gerrit.server.update.ChangeContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -146,9 +144,6 @@
         // Inherit unresolved value from inReplyTo comment if not specified.
         Comment.Key key = new Comment.Key(parentUuid, path, psId.get());
         Optional<HumanComment> parent = getPublishedHumanComment(changeNotes, key);
-
-        // If the comment was not found, it is descended from a robot comment, or the UUID is
-        // invalid. Either way, we use the default.
         unresolved = parent.map(p -> p.unresolved).orElse(false);
       }
     }
@@ -169,28 +164,6 @@
     return c;
   }
 
-  public RobotComment newRobotComment(
-      ChangeContext ctx,
-      String path,
-      PatchSet.Id psId,
-      short side,
-      String message,
-      String robotId,
-      String robotRunId) {
-    RobotComment c =
-        new RobotComment(
-            new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()),
-            ctx.getUser().getAccountId(),
-            ctx.getWhen(),
-            side,
-            message,
-            serverId,
-            robotId,
-            robotRunId);
-    ctx.getUser().updateRealAccountId(c::setRealAuthor);
-    return c;
-  }
-
   public Optional<HumanComment> getPublishedHumanComment(ChangeNotes notes, Comment.Key key) {
     return publishedHumanCommentsByChange(notes).stream()
         .filter(c -> key.equals(c.key))
@@ -208,23 +181,10 @@
     return sort(Lists.newArrayList(notes.getHumanComments().values()));
   }
 
-  public List<RobotComment> robotCommentsByChange(ChangeNotes notes) {
-    notes.load();
-    return sort(Lists.newArrayList(notes.getRobotComments().values()));
-  }
-
-  public Optional<RobotComment> getRobotComment(ChangeNotes notes, String uuid) {
-    return robotCommentsByChange(notes).stream().filter(c -> c.key.uuid.equals(uuid)).findFirst();
-  }
-
   public List<HumanComment> publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     return commentsOnPatchSet(notes.load().getHumanComments().values(), psId);
   }
 
-  public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
-    return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
-  }
-
   /**
    * This method populates the "changeMessageId" field of the comments parameter based on timestamp
    * matching. The comments objects will be modified.
@@ -309,12 +269,6 @@
     }
   }
 
-  public void putRobotComments(ChangeUpdate update, Iterable<RobotComment> comments) {
-    for (RobotComment c : comments) {
-      update.putRobotComment(c);
-    }
-  }
-
   public void deleteHumanComments(ChangeUpdate update, Iterable<HumanComment> comments) {
     for (HumanComment c : comments) {
       update.deleteComment(c);
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index 42d7563..1106883 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.InlineMe;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -66,7 +67,24 @@
     return this;
   }
 
-  public boolean isImpersonating() {
+  /**
+   * Identity of the user that should be used for permission checks by default.
+   *
+   * <p>In case of impersonation this controls. If the permissions of the impersonated user or of
+   * the real user/caller should be used for permission checks.
+   */
+  public CurrentUser getUserForPermission() {
+    return this;
+  }
+
+  @Deprecated(since = "3.12", forRemoval = true)
+  @InlineMe(replacement = "this.isImpersonated()")
+  public final boolean isImpersonating() {
+    return this.isImpersonated();
+  }
+
+  /** Is this account being impersonated by {@link #getRealUser()}. */
+  public boolean isImpersonated() {
     return false;
   }
 
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
index 501843c..ba236d2 100644
--- a/java/com/google/gerrit/server/ExceptionHook.java
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -19,7 +19,9 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.project.CreateProjectArgs;
 import java.util.Optional;
 
 /**
@@ -149,6 +151,27 @@
     return Optional.empty();
   }
 
+  /**
+   * Whether {code com.google.gerrit.server.project.ProjectCreator} should try initializing the
+   * project even if creating the repository has failed.
+   *
+   * <p>The project initialization consists out of setting {@code HEAD}, creating the {@code
+   * project.config} file in {@code refs/meta/config} and creating initial branches with empty
+   * commits.
+   *
+   * <p>This is useful if the repository creation includes some optional steps after the actual
+   * repository creation. If any of the optional steps fails, this method can be used to make Gerrit
+   * still do the project initialization.
+   *
+   * @param projectName the name of the project to be created
+   * @param args the project creation args
+   * @param throwable throwable that was thrown while creating the repository
+   */
+  default boolean tryProjectInitializationOnRepoCreationFailure(
+      Project.NameKey projectName, CreateProjectArgs args, Throwable throwable) {
+    return false;
+  }
+
   @AutoValue
   public abstract class Status {
     public abstract int statusCode();
diff --git a/java/com/google/gerrit/server/ExceptionHookImpl.java b/java/com/google/gerrit/server/ExceptionHookImpl.java
index ac1ed6f..da425be 100644
--- a/java/com/google/gerrit/server/ExceptionHookImpl.java
+++ b/java/com/google/gerrit/server/ExceptionHookImpl.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.exceptions.GerritNoMergeBaseException;
 import com.google.gerrit.exceptions.MergeUpdateException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -34,6 +35,8 @@
       "Updating a ref failed with LOCK_FAILURE.\n"
           + "This may be a temporary issue due to concurrent updates.\n"
           + "Please retry later.";
+  private static final String NO_MERGE_BASE_USER_MESSAGE =
+      "Cannot create merge commit: No Merge Base";
   private static final String INVALID_PROJECT_CONFIG_USER_MESSAGE =
       "Invalid " + ProjectConfig.PROJECT_CONFIG + " file.";
   private static final String CONTACT_PROJECT_OWNER_USER_MESSAGE =
@@ -46,7 +49,7 @@
 
   @Override
   public boolean skipRetryWithTrace(String actionType, String actionName, Throwable throwable) {
-    return isInvalidProjectConfig(throwable);
+    return isInvalidProjectConfig(throwable) || isNoMergeBaseException(throwable);
   }
 
   @Override
@@ -60,6 +63,9 @@
     if (isInvalidProjectConfig(throwable)) {
       return Optional.of("invalid_project_config");
     }
+    if (isNoMergeBaseException(throwable)) {
+      return Optional.of("no_merge_base");
+    }
     return Optional.empty();
   }
 
@@ -74,6 +80,10 @@
               + "\n"
               + CONTACT_PROJECT_OWNER_USER_MESSAGE);
     }
+    if (isNoMergeBaseException(throwable)) {
+      return ImmutableList.of(
+          getNoMergeBaseExceptionMessage(throwable).orElse(NO_MERGE_BASE_USER_MESSAGE));
+    }
     if (throwable instanceof MergeUpdateException) {
       return ImmutableList.of(throwable.getMessage());
     }
@@ -85,6 +95,9 @@
     if (isLockFailure(throwable)) {
       return Optional.of(Status.create(503, "Lock failure"));
     }
+    if (isNoMergeBaseException(throwable)) {
+      return Optional.of(Status.create(409, "Conflict"));
+    }
     if (isInvalidProjectConfig(throwable)) {
       return Optional.of(Status.create(409, "Conflict"));
     }
@@ -99,6 +112,17 @@
     return isMatching(throwable, t -> t instanceof MissingObjectException);
   }
 
+  private static boolean isNoMergeBaseException(Throwable throwable) {
+    return isMatching(throwable, t -> t instanceof GerritNoMergeBaseException);
+  }
+
+  private Optional<String> getNoMergeBaseExceptionMessage(Throwable throwable) {
+    return Throwables.getCausalChain(throwable).stream()
+        .filter(GerritNoMergeBaseException.class::isInstance)
+        .map(ex -> ex.getMessage())
+        .findFirst();
+  }
+
   private static boolean isInvalidProjectConfig(Throwable throwable) {
     return isMatching(
         throwable,
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index d5cb87d..44280a0 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -56,6 +56,14 @@
 public class IdentifiedUser extends CurrentUser {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  /**
+   * Setting that controls which user in case of impersonation should be used for permission checks.
+   */
+  public enum ImpersonationPermissionMode {
+    THIS_USER,
+    REAL_USER,
+  }
+
   /** Create an IdentifiedUser, ignoring any per-request state. */
   @Singleton
   public static class GenericFactory {
@@ -100,7 +108,8 @@
           enablePeerIPInReflogRecord,
           Providers.of(null),
           state,
-          /* realUser= */ null);
+          /* realUser= */ null,
+          ImpersonationPermissionMode.THIS_USER);
     }
 
     public IdentifiedUser create(Account.Id id) {
@@ -110,23 +119,32 @@
     @VisibleForTesting
     @UsedAt(UsedAt.Project.GOOGLE)
     public IdentifiedUser forTest(Account.Id id, PropertyMap properties) {
-      return runAs(/* remotePeer= */ null, id, /* caller= */ null, properties);
+      return runAs(
+          /* remotePeer= */ null,
+          id,
+          /* caller= */ null,
+          properties,
+          ImpersonationPermissionMode.THIS_USER);
     }
 
     public IdentifiedUser create(@Nullable SocketAddress remotePeer, Account.Id id) {
-      return runAs(remotePeer, id, /* caller= */ null);
+      return runAs(remotePeer, id, /* caller= */ null, ImpersonationPermissionMode.THIS_USER);
     }
 
     public IdentifiedUser runAs(
-        @Nullable SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
-      return runAs(remotePeer, id, caller, PropertyMap.EMPTY);
+        @Nullable SocketAddress remotePeer,
+        Account.Id id,
+        @Nullable CurrentUser caller,
+        ImpersonationPermissionMode permissionMode) {
+      return runAs(remotePeer, id, caller, PropertyMap.EMPTY, permissionMode);
     }
 
     private IdentifiedUser runAs(
         @Nullable SocketAddress remotePeer,
         Account.Id id,
         @Nullable CurrentUser caller,
-        PropertyMap properties) {
+        PropertyMap properties,
+        ImpersonationPermissionMode permissionMode) {
       return new IdentifiedUser(
           authConfig,
           realm,
@@ -138,8 +156,9 @@
           enablePeerIPInReflogRecord,
           Providers.of(remotePeer),
           id,
-          caller,
-          properties);
+          caller != null ? caller.getRealUser() : null,
+          properties,
+          permissionMode);
     }
   }
 
@@ -200,10 +219,15 @@
           remotePeerProvider,
           id,
           null,
-          properties);
+          properties,
+          ImpersonationPermissionMode.THIS_USER);
     }
 
-    public IdentifiedUser runAs(Account.Id id, CurrentUser caller, PropertyMap properties) {
+    public IdentifiedUser runAs(
+        Account.Id id,
+        CurrentUser caller,
+        PropertyMap properties,
+        ImpersonationPermissionMode permissionMode) {
       return new IdentifiedUser(
           authConfig,
           realm,
@@ -215,8 +239,9 @@
           enablePeerIPInReflogRecord,
           remotePeerProvider,
           id,
-          caller,
-          properties);
+          caller != null ? caller.getRealUser() : null,
+          properties,
+          permissionMode);
     }
   }
 
@@ -243,6 +268,7 @@
   private Set<String> invalidEmails;
   private GroupMembership effectiveGroups;
   private PersonIdent refLogIdent;
+  private ImpersonationPermissionMode permissionMode;
 
   private IdentifiedUser(
       AuthConfig authConfig,
@@ -255,7 +281,8 @@
       Boolean enablePeerIPInReflogRecord,
       Provider<SocketAddress> remotePeerProvider,
       AccountState state,
-      @Nullable CurrentUser realUser) {
+      @Nullable CurrentUser realUser,
+      ImpersonationPermissionMode permissionMode) {
     this(
         authConfig,
         realm,
@@ -268,7 +295,8 @@
         remotePeerProvider,
         state.account().id(),
         realUser,
-        PropertyMap.EMPTY);
+        PropertyMap.EMPTY,
+        permissionMode);
     this.state = state;
   }
 
@@ -284,7 +312,8 @@
       Provider<SocketAddress> remotePeerProvider,
       Account.Id id,
       @Nullable CurrentUser realUser,
-      PropertyMap properties) {
+      PropertyMap properties,
+      ImpersonationPermissionMode permissionMode) {
     super(properties);
     this.canonicalUrl = canonicalUrl;
     this.accountCache = accountCache;
@@ -297,6 +326,7 @@
     this.remotePeerProvider = remotePeerProvider;
     this.accountId = id;
     this.realUser = realUser != null ? realUser : this;
+    this.permissionMode = permissionMode;
   }
 
   @Override
@@ -305,7 +335,16 @@
   }
 
   @Override
-  public boolean isImpersonating() {
+  public CurrentUser getUserForPermission() {
+    if (permissionMode.equals(ImpersonationPermissionMode.THIS_USER)) {
+      return this;
+    } else {
+      return realUser;
+    }
+  }
+
+  @Override
+  public boolean isImpersonated() {
     if (realUser == this) {
       return false;
     }
@@ -536,7 +575,8 @@
         enablePeerIPInReflogRecord,
         remotePeer,
         state,
-        realUser);
+        realUser,
+        permissionMode);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/account/AccountCache.java b/java/com/google/gerrit/server/account/AccountCache.java
index 5ae7345..addddcd 100644
--- a/java/com/google/gerrit/server/account/AccountCache.java
+++ b/java/com/google/gerrit/server/account/AccountCache.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.entities.Account;
-import java.util.Map;
+import java.util.Collection;
 import java.util.Optional;
-import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Caches important (but small) account state to avoid database hits. */
@@ -35,18 +35,20 @@
   Optional<AccountState> get(Account.Id accountId);
 
   /**
-   * Returns a {@code Map} of {@code Account.Id} to {@code AccountState} for the given account IDs.
-   * If not cached yet the accounts are loaded. If an account can't be loaded (e.g. because it is
-   * missing), the entry will be missing from the result.
+   * Returns a {@code ImmutableMap} of {@code Account.Id} to {@code AccountState} for the given
+   * account IDs. If not cached yet the accounts are loaded. If an account can't be loaded (e.g.
+   * because it is missing), the entry will be missing from the result. The order of the provided
+   * account IDs is preserved in the result.
    *
    * <p>Loads accounts in parallel if applicable.
    *
    * @param accountIds IDs of the account that should be retrieved
    * @return {@code Map} of {@code Account.Id} to {@code AccountState} instances for the given
    *     account IDs, if an account can't be loaded (e.g. because it is missing), the entry will be
-   *     missing from the result
+   *     missing from the result. The order of the provided * account IDs is preserved in the
+   *     result.
    */
-  Map<Account.Id, AccountState> get(Set<Account.Id> accountIds);
+  ImmutableMap<Account.Id, AccountState> get(Collection<Account.Id> accountIds);
 
   /**
    * Returns an {@code AccountState} instance for the given account ID. If not cached yet the
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 87adb22..48dd5e2 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -44,6 +44,7 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Optional;
@@ -146,7 +147,7 @@
   }
 
   @Override
-  public Map<Account.Id, AccountState> get(Set<Account.Id> accountIds) {
+  public ImmutableMap<Account.Id, AccountState> get(Collection<Account.Id> accountIds) {
     try (TraceTimer ignored =
         TraceContext.newTimer(
             "Loading accounts", Metadata.builder().resourceCount(accountIds.size()).build())) {
diff --git a/java/com/google/gerrit/server/account/AccountDelta.java b/java/com/google/gerrit/server/account/AccountDelta.java
index 14ced50..50f34a5 100644
--- a/java/com/google/gerrit/server/account/AccountDelta.java
+++ b/java/com/google/gerrit/server/account/AccountDelta.java
@@ -20,6 +20,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.ProjectWatchKey;
@@ -177,6 +179,11 @@
         || !this.getUpdatedExternalIds().isEmpty();
   }
 
+  @UsedAt(Project.GOOGLE)
+  public boolean isEmpty() {
+    return this.equals(AccountDelta.builder().build());
+  }
+
   /**
    * Class to build an {@link AccountDelta}.
    *
@@ -536,6 +543,7 @@
      * return value is replaced with the wrapper instance so that all chained calls go through the
      * wrapper.
      */
+    @SuppressWarnings("ExtendsAutoValue")
     private static class WrapperThatConvertsNullStringArgsToEmptyStrings extends Builder {
       private final Builder delegate;
 
diff --git a/java/com/google/gerrit/server/account/AccountDirectory.java b/java/com/google/gerrit/server/account/AccountDirectory.java
index 10aecd3..78375bb 100644
--- a/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -55,7 +55,10 @@
     DISPLAY_NAME,
 
     /** Tags such as weather the account is a service user. */
-    TAGS
+    TAGS,
+
+    /** A flag that is set for deleted accounts. */
+    DELETED
   }
 
   public abstract void fillAccountInfo(Iterable<? extends AccountInfo> in, Set<FillOptions> options)
diff --git a/java/com/google/gerrit/server/account/AccountLoader.java b/java/com/google/gerrit/server/account/AccountLoader.java
index e3f4d48..263e7b6 100644
--- a/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/java/com/google/gerrit/server/account/AccountLoader.java
@@ -59,7 +59,8 @@
               FillOptions.STATUS,
               FillOptions.STATE,
               FillOptions.AVATARS,
-              FillOptions.TAGS));
+              FillOptions.TAGS,
+              FillOptions.DELETED));
 
   public interface Factory {
     AccountLoader create(boolean detailed);
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 91368a6..188caae 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -282,7 +282,8 @@
     String oldEmail = extId.email();
     if (newEmail != null && !newEmail.equals(oldEmail)) {
       ExternalId extIdWithNewEmail =
-          externalIdFactory.create(extId.key(), extId.accountId(), newEmail, extId.password());
+          externalIdFactory.createWithPassword(
+              extId.key(), extId.accountId(), newEmail, extId.password());
       checkEmailNotUsed(extId.accountId(), extIdWithNewEmail);
       accountUpdates.add(u -> u.replaceExternalId(extId, extIdWithNewEmail));
 
@@ -460,7 +461,9 @@
             externalId.accountId().get(),
             extIdToBeCreated.key().get(),
             extIdToBeCreated.accountId().get());
-        throw new AccountException("Email '" + email + "' in use by another account");
+        throw new AccountException(
+            String.format(
+                "Email '%s' in use by another account (%s)", email, externalId.accountId()));
       }
     }
   }
@@ -498,7 +501,9 @@
       ExternalId extId = optionalExtId.get();
       if (!extId.accountId().equals(to)) {
         throw new AccountException(
-            "Identity '" + extId.key().get() + "' in use by another account");
+            String.format(
+                "Identity '%s' in use by another account (%s)",
+                extId.key().get(), extId.accountId()));
       }
       update(who, extId);
     } else {
@@ -538,7 +543,9 @@
     Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
     if (optionalExtId.filter(extId -> !extId.accountId().equals(to)).isPresent()) {
       throw new AccountException(
-          "Identity '" + optionalExtId.get().key().get() + "' in use by another account");
+          String.format(
+              "Identity '%s' in use by another account (%s)",
+              optionalExtId.get().key().get(), optionalExtId.get().accountId()));
     }
 
     accountsUpdateProvider
@@ -606,7 +613,8 @@
         continue;
       }
       // The externalID is linked to some other account so we throw an error here
-      throw new AccountException("Identity '" + extId + "' in use by another account");
+      throw new AccountException(
+          String.format("Identity '%s' in use by another account (%s)", extId, extId.accountId()));
     }
 
     accountsUpdateProvider
@@ -651,7 +659,10 @@
       Optional<ExternalId> extId = externalIds.get(extIdKey);
       if (extId.isPresent()) {
         if (!extId.get().accountId().equals(from)) {
-          throw new AccountException("Identity '" + extIdKey.get() + "' in use by another account");
+          throw new AccountException(
+              String.format(
+                  "Identity '%s' in use by another account (%s)",
+                  extIdKey.get(), extId.get().accountId()));
         }
         extIds.add(extId.get());
       } else {
diff --git a/java/com/google/gerrit/server/account/AccountModule.java b/java/com/google/gerrit/server/account/AccountModule.java
index c1305cf..af9918c 100644
--- a/java/com/google/gerrit/server/account/AccountModule.java
+++ b/java/com/google/gerrit/server/account/AccountModule.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.inject.AbstractModule;
+import com.google.gerrit.extensions.config.FactoryModule;
 
-public class AccountModule extends AbstractModule {
+public class AccountModule extends FactoryModule {
   @Override
   protected void configure() {
     bind(AuthRequest.Factory.class);
+    bind(AuthTokenVerifier.class);
+    factory(PasswordMigrator.Factory.class);
+    factory(MaxAuthTokenLifetimeApplier.Factory.class);
   }
 }
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index b472524..009266d 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -207,14 +208,18 @@
       return userFactory.create(asUnique());
     }
 
-    public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller)
+    public IdentifiedUser asUniqueUserOnBehalfOf(
+        CurrentUser caller, ImpersonationPermissionMode permissionMode)
         throws UnresolvableAccountException {
       ensureUnique();
       if (isSelf()) {
         return searchedAsUser.asIdentifiedUser();
       }
       return userFactory.runAs(
-          /* remotePeer= */ null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
+          /* remotePeer= */ null,
+          list.get(0).account().id(),
+          requireNonNull(caller),
+          permissionMode);
     }
 
     @VisibleForTesting
diff --git a/java/com/google/gerrit/server/account/AccountResource.java b/java/com/google/gerrit/server/account/AccountResource.java
index 14b363b..c7bf5dc 100644
--- a/java/com/google/gerrit/server/account/AccountResource.java
+++ b/java/com/google/gerrit/server/account/AccountResource.java
@@ -34,6 +34,8 @@
   public static final TypeLiteral<RestView<StarredChange>> STARRED_CHANGE_KIND =
       new TypeLiteral<>() {};
 
+  public static final TypeLiteral<RestView<Token>> TOKEN_KIND = new TypeLiteral<>() {};
+
   private final IdentifiedUser user;
 
   public AccountResource(IdentifiedUser user) {
@@ -130,4 +132,17 @@
       return labels;
     }
   }
+
+  public static class Token extends AccountResource {
+    private final String id;
+
+    public Token(IdentifiedUser user, String id) {
+      super(user);
+      this.id = id;
+    }
+
+    public String getId() {
+      return id;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 13c913d..e2fd6af 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -38,9 +37,21 @@
  *
  * <p>Most callers should not construct AccountStates directly but rather lookup accounts via the
  * account cache (see {@link AccountCache#get(Account.Id)}).
+ *
+ * @param account Cached account metadata.
+ * @param externalIds The external identities that identify the account holder.
+ * @param userName The username, if one has been declared for this user.
+ * @param projectWatches The project watches of the account.
+ * @param defaultPreferences Gerrit's default preferences as stored in {@code preferences.config}.
+ * @param userPreferences User preferences as stored in {@code preferences.config}.
  */
-@AutoValue
-public abstract class AccountState {
+public record AccountState(
+    Account account,
+    ImmutableSet<ExternalId> externalIds,
+    Optional<String> userName,
+    ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches,
+    Optional<CachedPreferences> defaultPreferences,
+    Optional<CachedPreferences> userPreferences) {
 
   /**
    * Creates an AccountState for a given account with no external IDs, no project watches and
@@ -63,7 +74,7 @@
       CachedAccountDetails account, CachedPreferences defaultConfig, ExternalIds externalIds)
       throws IOException {
     ImmutableSet<ExternalId> extIds = externalIds.byAccount(account.account().id());
-    return new AutoValue_AccountState(
+    return new AccountState(
         account.account(),
         extIds,
         ExternalId.getUserName(extIds),
@@ -80,7 +91,7 @@
    * @return the account state
    */
   public static AccountState forAccount(Account account, Collection<ExternalId> extIds) {
-    return new AutoValue_AccountState(
+    return new AccountState(
         account,
         ImmutableSet.copyOf(extIds),
         ExternalId.getUserName(extIds),
@@ -97,31 +108,10 @@
       ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches,
       Optional<CachedPreferences> defaultPreferences,
       Optional<CachedPreferences> userPreferences) {
-    return new AutoValue_AccountState(
+    return new AccountState(
         account, externalIds, userName, projectWatches, defaultPreferences, userPreferences);
   }
 
-  /** Get the cached account metadata. */
-  public abstract Account account();
-
-  /** The external identities that identify the account holder. */
-  public abstract ImmutableSet<ExternalId> externalIds();
-
-  /**
-   * Get the username, if one has been declared for this user.
-   *
-   * <p>The username is the {@link ExternalId} using the scheme {@link ExternalId#SCHEME_USERNAME}.
-   *
-   * @return the username, {@link Optional#empty()} if the user has no username, or if the username
-   *     is empty
-   */
-  public abstract Optional<String> userName();
-
-  /** The project watches of the account. */
-  public abstract ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches();
-
-  /** The general preferences of the account. */
-
   /** The general preferences of the account. */
   public GeneralPreferencesInfo generalPreferences() {
     return CachedPreferences.general(
@@ -141,13 +131,13 @@
   }
 
   @Override
-  public final String toString() {
+  public String toString() {
     MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
     h.addValue(account().id());
     return h.toString();
   }
 
-  public final String debugString() {
+  public String debugString() {
     // Most of the fields might have a large representation. Using a multiline format to ease the
     // reading.
     return "AccountState[\n\t"
@@ -161,10 +151,4 @@
             .add("editPreferences: " + editPreferences())
         + "\n]";
   }
-
-  /** Gerrit's default preferences as stored in {@code preferences.config}. */
-  public abstract Optional<CachedPreferences> defaultPreferences();
-
-  /** User preferences as stored in {@code preferences.config}. */
-  public abstract Optional<CachedPreferences> userPreferences();
 }
diff --git a/java/com/google/gerrit/server/account/AuthRequest.java b/java/com/google/gerrit/server/account/AuthRequest.java
index cf1e552..03c6631 100644
--- a/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/java/com/google/gerrit/server/account/AuthRequest.java
@@ -87,11 +87,21 @@
       r.setEmailAddress(email);
       return r;
     }
+
+    /**
+     * Create a request for a login with a bearer token, where the username is not known. It has to
+     * be ensured that the realm adds the external ID in a later step.
+     */
+    public AuthRequest createForBearerToken(String token) {
+      AuthRequest r = new AuthRequest(externalIdKeyFactory);
+      r.setPassword(token);
+      return r;
+    }
   }
 
   private final ExternalIdKeyFactory externalIdKeyFactory;
 
-  private ExternalId.Key externalId;
+  @Nullable private ExternalId.Key externalId;
   private String password;
   private String displayName;
   private String emailAddress;
@@ -102,7 +112,12 @@
   private boolean authProvidesAccountActiveStatus;
   private boolean active;
 
-  private AuthRequest(ExternalId.Key externalId, ExternalIdKeyFactory externalIdKeyFactory) {
+  private AuthRequest(ExternalIdKeyFactory externalIdKeyFactory) {
+    this(null, externalIdKeyFactory);
+  }
+
+  private AuthRequest(
+      @Nullable ExternalId.Key externalId, ExternalIdKeyFactory externalIdKeyFactory) {
     this.externalId = externalId;
     this.externalIdKeyFactory = externalIdKeyFactory;
   }
@@ -111,6 +126,10 @@
     return externalId;
   }
 
+  public void setExternalIdKey(String scheme, String id) {
+    externalId = externalIdKeyFactory.create(scheme, id);
+  }
+
   @Nullable
   public String getLocalUser() {
     if (externalId.isScheme(SCHEME_GERRIT)) {
diff --git a/java/com/google/gerrit/server/account/AuthToken.java b/java/com/google/gerrit/server/account/AuthToken.java
new file mode 100644
index 0000000..390042b
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthToken.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.auto.value.AutoValue;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+@AutoValue
+public abstract class AuthToken {
+  private static final Pattern TOKEN_ID_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9-_]*$");
+
+  public static AuthToken createWithPlainToken(@Nullable String id, String plainToken)
+      throws InvalidAuthTokenException {
+    return createWithPlainToken(id, plainToken, Optional.empty());
+  }
+
+  public static AuthToken createWithPlainToken(
+      @Nullable String id, String plainToken, Optional<Instant> expirationDate)
+      throws InvalidAuthTokenException {
+    return create(id, HashedPassword.fromPassword(plainToken).encode(), expirationDate);
+  }
+
+  public static AuthToken create(@Nullable String id, String hashedToken)
+      throws InvalidAuthTokenException {
+    return create(id, hashedToken, Optional.empty());
+  }
+
+  public static AuthToken create(
+      @Nullable String id, String hashedToken, Optional<Instant> expirationDate)
+      throws InvalidAuthTokenException {
+    if (Strings.isNullOrEmpty(id)) {
+      id = "token_" + System.currentTimeMillis();
+    } else {
+      validateId(id);
+    }
+    return new AutoValue_AuthToken(id, hashedToken, expirationDate);
+  }
+
+  public abstract String id();
+
+  public abstract String hashedToken();
+
+  public abstract Optional<Instant> expirationDate();
+
+  public boolean isExpired() {
+    return expirationDate().isPresent() && Instant.now().isAfter(expirationDate().get());
+  }
+
+  private static void validateId(String id) throws InvalidAuthTokenException {
+    if (!TOKEN_ID_PATTERN.matcher(id).matches()) {
+      throw new InvalidAuthTokenException(
+          "Token ID must contain only letters, numbers, hyphens and underscores.");
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenAccessor.java b/java/com/google/gerrit/server/account/AuthTokenAccessor.java
new file mode 100644
index 0000000..a50bac7
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenAccessor.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.entities.Account;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public interface AuthTokenAccessor {
+  public List<AuthToken> getTokens(Account.Id accountId) throws IOException, ConfigInvalidException;
+
+  public Optional<AuthToken> getToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException;
+
+  public AuthToken addPlainToken(
+      Account.Id accountId, String id, String token, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException;
+
+  public AuthToken addToken(
+      Account.Id accountId, String id, String hashedToken, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException;
+
+  public void deleteToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException;
+
+  public List<AuthToken> getValidTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException;
+
+  public void deleteAllTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException;
+
+  public void addTokens(Account.Id accountId, Collection<AuthToken> tokens)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException;
+
+  public void updateToken(Account.Id accountId, AuthToken token)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException;
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenCache.java b/java/com/google/gerrit/server/account/AuthTokenCache.java
new file mode 100644
index 0000000..9452a73
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenCache.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+public class AuthTokenCache {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final String CACHE_NAME = "tokens";
+
+  private final LoadingCache<Account.Id, List<AuthToken>> cache;
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, Account.Id.class, new TypeLiteral<List<AuthToken>>() {})
+            .loader(Loader.class);
+        bind(AuthTokenCache.class);
+      }
+    };
+  }
+
+  @Inject
+  AuthTokenCache(@Named(CACHE_NAME) LoadingCache<Account.Id, List<AuthToken>> cache) {
+    this.cache = cache;
+  }
+
+  public List<AuthToken> get(Account.Id accountId) {
+    try {
+      return cache.get(accountId);
+    } catch (ExecutionException e) {
+      logger.atWarning().withCause(e).log(
+          "Cannot load authentication tokens for %d", accountId.get());
+      throw new StorageException(e);
+    }
+  }
+
+  public void evict(Account.Id accountId) {
+    if (accountId != null) {
+      logger.atFine().log("Evict authentication token for username %d", accountId.get());
+      cache.invalidate(accountId);
+    }
+  }
+
+  static class Loader extends CacheLoader<Account.Id, List<AuthToken>> {
+    private final DirectAuthTokenAccessor directAccessor;
+
+    @Inject
+    Loader(DirectAuthTokenAccessor directAccessor) {
+      this.directAccessor = directAccessor;
+    }
+
+    @Override
+    public ImmutableList<AuthToken> load(Account.Id accountId) throws Exception {
+      return directAccessor.getTokens(accountId);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenConflictException.java b/java/com/google/gerrit/server/account/AuthTokenConflictException.java
new file mode 100644
index 0000000..77af119
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenConflictException.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.entities.Account;
+
+public class AuthTokenConflictException extends InvalidAuthTokenException {
+  private static final long serialVersionUID = 1L;
+
+  public AuthTokenConflictException(String id, Account.Id accountId) {
+    super(message(id, accountId));
+  }
+
+  public AuthTokenConflictException(String id, Account.Id accountId, Throwable cause) {
+    super(message(id, accountId), cause);
+  }
+
+  private static String message(String id, Account.Id accountId) {
+    return String.format("A token with id %s already exists for account %d.", id, accountId.get());
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java b/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java
new file mode 100644
index 0000000..99b2da4
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenExpiryNotifier.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.server.mail.EmailFactories.AUTH_TOKEN_WILL_EXPIRE;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.account.storage.notedb.AccountsNoteDbImpl;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.mail.EmailFactories;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@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 final AccountsNoteDbImpl accounts;
+  private final AuthTokenAccessor tokenAccessor;
+  private final EmailFactories emailFactories;
+
+  public static Module module() {
+    return new LifecycleModule() {
+      @Override
+      protected void configure() {
+        bind(AuthTokenExpiryNotifier.class);
+        listener().to(AuthTokenExpiryNotifier.Lifecycle.class);
+      }
+    };
+  }
+
+  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());
+      }
+    }
+
+    @Override
+    public void stop() {
+      // handled by WorkQueue.stop() already
+    }
+  }
+
+  @Inject
+  public AuthTokenExpiryNotifier(
+      AccountsNoteDbImpl accounts, AuthTokenAccessor tokenAccessor, EmailFactories emailFactories) {
+    this.accounts = accounts;
+    this.tokenAccessor = tokenAccessor;
+    this.emailFactories = emailFactories;
+  }
+
+  @Override
+  public void run() {
+    Instant now = 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());
+            emailFactories
+                .createOutgoingEmail(
+                    AUTH_TOKEN_WILL_EXPIRE,
+                    emailFactories.createAuthTokenWillExpireEmail(account.account(), token))
+                .send();
+          }
+        }
+      }
+    } catch (IOException | ConfigInvalidException e) {
+      throw new RuntimeException("Failed to read accounts from NoteDB", e);
+    } catch (EmailException e) {
+      logger.atSevere().withCause(e).log("Failed to send token expiry notification email");
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenModule.java b/java/com/google/gerrit/server/account/AuthTokenModule.java
new file mode 100644
index 0000000..938034e
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenModule.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+public class AuthTokenModule extends FactoryModule {
+
+  @Override
+  protected void configure() {
+    factory(HttpPasswordFallbackAuthTokenAccessor.Factory.class);
+    factory(MaxAuthTokenLifetimeApplier.Factory.class);
+  }
+
+  @Provides
+  @Singleton
+  public AuthTokenAccessor createAuthTokenAccessor(
+      AuthConfig authConfig,
+      HttpPasswordFallbackAuthTokenAccessor.Factory fallbackFactory,
+      DirectAuthTokenAccessor directAccessor) {
+    if (authConfig.isHttpPasswordFallbackEnabled()) {
+      return fallbackFactory.create(directAccessor);
+    }
+    return directAccessor;
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AuthTokenVerifier.java b/java/com/google/gerrit/server/account/AuthTokenVerifier.java
new file mode 100644
index 0000000..564b70c
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AuthTokenVerifier.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.base.Strings;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.inject.Inject;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Checks if a given username and token match a user's credentials. */
+public class AuthTokenVerifier {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final int MAX_PASSWORD_LENGTH_ACCORDING_TO_BCRYPT_LIMITS = 72;
+
+  private final AuthTokenAccessor tokenAccessor;
+
+  @Inject
+  public AuthTokenVerifier(AuthTokenAccessor tokenAccessor) {
+    this.tokenAccessor = tokenAccessor;
+  }
+
+  /**
+   * Checks if a given username and token match a user's credentials.
+   *
+   * @param accountId the account ID to check.
+   * @param providedToken the token to check.
+   * @return whether there is a token hash stored for the account that matches the provided token.
+   */
+  public boolean checkToken(Account.Id accountId, @Nullable String providedToken) {
+    if (Strings.isNullOrEmpty(providedToken)
+        || providedToken.length() >= MAX_PASSWORD_LENGTH_ACCORDING_TO_BCRYPT_LIMITS) {
+      return false;
+    }
+
+    try {
+      for (AuthToken t : tokenAccessor.getValidTokens(accountId)) {
+        if (HashedPassword.decode(t.hashedToken()).checkPassword(providedToken)) {
+          return true;
+        }
+      }
+    } catch (IOException | ConfigInvalidException e) {
+      logger.atSevere().withCause(e).log(
+          "Could not read tokens for account %s: %s ", accountId, e.getMessage());
+    } catch (HashedPassword.DecoderException e) {
+      logger.atSevere().withCause(e).log(
+          "Could not decode token for account %s: %s ", accountId, e.getMessage());
+    }
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/server/account/CachingAuthTokenAccessor.java b/java/com/google/gerrit/server/account/CachingAuthTokenAccessor.java
new file mode 100644
index 0000000..a0c095d
--- /dev/null
+++ b/java/com/google/gerrit/server/account/CachingAuthTokenAccessor.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Read/write authentication tokens by user ID using a cache for faster access. */
+public class CachingAuthTokenAccessor implements AuthTokenAccessor {
+
+  public interface Factory {
+    CachingAuthTokenAccessor create(AuthTokenAccessor accessor);
+  }
+
+  private final AuthTokenCache authTokenCache;
+  private final AuthTokenAccessor accessor;
+
+  @AssistedInject
+  CachingAuthTokenAccessor(
+      AuthTokenCache authTokenCache, @Assisted AuthTokenAccessor directAuthTokenAccessor) {
+    this.authTokenCache = authTokenCache;
+    this.accessor = directAuthTokenAccessor;
+  }
+
+  @Override
+  public List<AuthToken> getValidTokens(Account.Id accountId) {
+    return getTokens(accountId).stream().filter(t -> !t.isExpired()).toList();
+  }
+
+  @Override
+  public List<AuthToken> getTokens(Account.Id accountId) {
+    return authTokenCache.get(accountId);
+  }
+
+  @Override
+  public Optional<AuthToken> getToken(Account.Id accountId, String id) {
+    return getTokens(accountId).stream().filter(token -> token.id().equals(id)).findFirst();
+  }
+
+  @Override
+  public synchronized void addTokens(Account.Id accountId, Collection<AuthToken> tokens)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.addTokens(accountId, tokens);
+    authTokenCache.evict(accountId);
+  }
+
+  @Override
+  @CanIgnoreReturnValue
+  public synchronized AuthToken addToken(
+      Account.Id accountId, String id, String hashedToken, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    AuthToken token = accessor.addToken(accountId, id, hashedToken, expiration);
+    authTokenCache.evict(accountId);
+    return token;
+  }
+
+  @Override
+  public AuthToken addPlainToken(
+      Account.Id accountId, String id, String token, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    AuthToken authToken = accessor.addPlainToken(accountId, id, token, expiration);
+    authTokenCache.evict(accountId);
+    return authToken;
+  }
+
+  @Override
+  public void deleteToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.deleteToken(accountId, id);
+    authTokenCache.evict(accountId);
+  }
+
+  @Override
+  public void deleteAllTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.deleteAllTokens(accountId);
+    authTokenCache.evict(accountId);
+  }
+
+  @Override
+  public void updateToken(Account.Id accountId, AuthToken token)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.updateToken(accountId, token);
+    authTokenCache.evict(accountId);
+  }
+}
diff --git a/java/com/google/gerrit/server/account/CachingAuthTokenModule.java b/java/com/google/gerrit/server/account/CachingAuthTokenModule.java
new file mode 100644
index 0000000..553b78b
--- /dev/null
+++ b/java/com/google/gerrit/server/account/CachingAuthTokenModule.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+public class CachingAuthTokenModule extends FactoryModule {
+
+  @Override
+  protected void configure() {
+    install(AuthTokenCache.module());
+    install(AuthTokenExpiryNotifier.module());
+    factory(CachingAuthTokenAccessor.Factory.class);
+    factory(HttpPasswordFallbackAuthTokenAccessor.Factory.class);
+  }
+
+  @Provides
+  @Singleton
+  public AuthTokenAccessor createAuthTokenAccessor(
+      AuthConfig authConfig,
+      HttpPasswordFallbackAuthTokenAccessor.Factory fallbackFactory,
+      CachingAuthTokenAccessor.Factory cachingFactory,
+      DirectAuthTokenAccessor directAccessor) {
+    if (authConfig.isHttpPasswordFallbackEnabled()) {
+      return fallbackFactory.create(cachingFactory.create(directAccessor));
+    }
+    return cachingFactory.create(directAccessor);
+  }
+}
diff --git a/java/com/google/gerrit/server/account/DirectAuthTokenAccessor.java b/java/com/google/gerrit/server/account/DirectAuthTokenAccessor.java
new file mode 100644
index 0000000..e69addd
--- /dev/null
+++ b/java/com/google/gerrit/server/account/DirectAuthTokenAccessor.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Read/write authentication tokens by user ID. */
+@Singleton
+public class DirectAuthTokenAccessor implements AuthTokenAccessor {
+  public static final String LEGACY_ID = "legacy";
+
+  private final AllUsersName allUsersName;
+  private final VersionedAuthTokens.Factory authTokenFactory;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  DirectAuthTokenAccessor(
+      AllUsersName allUsersName,
+      VersionedAuthTokens.Factory authTokenFactory,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      IdentifiedUser.GenericFactory userFactory) {
+    this.allUsersName = allUsersName;
+    this.authTokenFactory = authTokenFactory;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.userFactory = userFactory;
+  }
+
+  @Override
+  public ImmutableList<AuthToken> getValidTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException {
+    return ImmutableList.copyOf(getTokens(accountId).stream().filter(t -> !t.isExpired()).toList());
+  }
+
+  @Override
+  public ImmutableList<AuthToken> getTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException {
+    return readFromNoteDb(accountId).getTokens();
+  }
+
+  @Override
+  public Optional<AuthToken> getToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException {
+    return Optional.ofNullable(readFromNoteDb(accountId).getToken(id));
+  }
+
+  @Override
+  @CanIgnoreReturnValue
+  public synchronized AuthToken addPlainToken(
+      Account.Id accountId, String id, String token, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    String hashedToken = HashedPassword.fromPassword(token).encode();
+    return addToken(accountId, id, hashedToken, expiration);
+  }
+
+  @Override
+  public synchronized void addTokens(Account.Id accountId, Collection<AuthToken> tokens)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    VersionedAuthTokens authTokens = readFromNoteDb(accountId);
+    for (AuthToken token : tokens) {
+      authTokens.addToken(token);
+    }
+    commit(accountId, authTokens);
+  }
+
+  @Override
+  @CanIgnoreReturnValue
+  public synchronized AuthToken addToken(
+      Account.Id accountId, String id, String hashedToken, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    VersionedAuthTokens authTokens = readFromNoteDb(accountId);
+    AuthToken token = authTokens.addToken(id, hashedToken, expiration);
+    commit(accountId, authTokens);
+    return token;
+  }
+
+  @Override
+  public synchronized void deleteToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException {
+    VersionedAuthTokens authTokens = readFromNoteDb(accountId);
+    if (authTokens.deleteToken(id)) {
+      commit(accountId, authTokens);
+    }
+  }
+
+  @Override
+  public void deleteAllTokens(Account.Id accountId) throws IOException, ConfigInvalidException {
+    VersionedAuthTokens authTokens = readFromNoteDb(accountId);
+    if (authTokens.getTokens().isEmpty()) {
+      return;
+    }
+    for (AuthToken token : getTokens(accountId)) {
+      @SuppressWarnings("unused")
+      var unused = authTokens.deleteToken(token.id());
+    }
+    commit(accountId, authTokens);
+  }
+
+  @Override
+  public void updateToken(Account.Id accountId, AuthToken token)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    VersionedAuthTokens authTokens = readFromNoteDb(accountId);
+    @SuppressWarnings("unused")
+    var unused = authTokens.deleteToken(token.id());
+    authTokens.addToken(token);
+    commit(accountId, authTokens);
+  }
+
+  private VersionedAuthTokens readFromNoteDb(Account.Id accountId)
+      throws IOException, ConfigInvalidException {
+    return authTokenFactory.create(accountId).load();
+  }
+
+  private void commit(Account.Id accountId, VersionedAuthTokens authTokens) throws IOException {
+    try (MetaDataUpdate md =
+        metaDataUpdateFactory.get().create(allUsersName, userFactory.create(accountId))) {
+      authTokens.commit(md, true);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/HttpPasswordFallbackAuthTokenAccessor.java b/java/com/google/gerrit/server/account/HttpPasswordFallbackAuthTokenAccessor.java
new file mode 100644
index 0000000..f837086
--- /dev/null
+++ b/java/com/google/gerrit/server/account/HttpPasswordFallbackAuthTokenAccessor.java
@@ -0,0 +1,191 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+
+public class HttpPasswordFallbackAuthTokenAccessor implements AuthTokenAccessor {
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static final String LEGACY_ID = "legacy";
+
+  public interface Factory {
+    HttpPasswordFallbackAuthTokenAccessor create(AuthTokenAccessor accessor);
+  }
+
+  private final AccountCache accountCache;
+  private final AuthTokenAccessor accessor;
+  private final ExternalIds externalIds;
+  private final ExternalIdFactory externalIdFactory;
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+  private final ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
+  private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
+
+  @AssistedInject
+  HttpPasswordFallbackAuthTokenAccessor(
+      AccountCache accountCache,
+      @Assisted AuthTokenAccessor accessor,
+      ExternalIds externalIds,
+      ExternalIdFactory externalIdFactory,
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers,
+      ExternalIdNotes.FactoryNoReindex externalIdNotesFactory,
+      Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory) {
+    this.accessor = accessor;
+    this.accountCache = accountCache;
+    this.externalIds = externalIds;
+    this.externalIdFactory = externalIdFactory;
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+    this.externalIdNotesFactory = externalIdNotesFactory;
+    this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
+  }
+
+  @Override
+  public List<AuthToken> getTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException {
+    List<AuthToken> tokens = accessor.getTokens(accountId);
+    if (tokens.isEmpty()) {
+      tokens = fallBackToLegacyHttpPassword(accountId);
+    }
+    return tokens;
+  }
+
+  @Override
+  public List<AuthToken> getValidTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException {
+    return ImmutableList.copyOf(getTokens(accountId).stream().filter(t -> !t.isExpired()).toList());
+  }
+
+  @Override
+  public Optional<AuthToken> getToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException {
+    return getTokens(accountId).stream().filter(token -> token.id().equals(id)).findFirst();
+  }
+
+  @Override
+  public AuthToken addToken(
+      Account.Id accountId, String id, String hashedToken, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    AuthToken token = accessor.addToken(accountId, id, hashedToken, expiration);
+    purgeHttpPassword(accountId);
+    return token;
+  }
+
+  @Override
+  public void addTokens(Account.Id accountId, Collection<AuthToken> tokens)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.addTokens(accountId, tokens);
+    purgeHttpPassword(accountId);
+  }
+
+  @Override
+  public AuthToken addPlainToken(
+      Account.Id accountId, String id, String token, Optional<Instant> expiration)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    AuthToken authToken = accessor.addPlainToken(accountId, id, token, expiration);
+    purgeHttpPassword(accountId);
+    return authToken;
+  }
+
+  @Override
+  public void deleteToken(Account.Id accountId, String id)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.deleteToken(accountId, id);
+  }
+
+  @Override
+  public void deleteAllTokens(Account.Id accountId)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.deleteAllTokens(accountId);
+  }
+
+  @Override
+  public void updateToken(Account.Id accountId, AuthToken token)
+      throws IOException, ConfigInvalidException, InvalidAuthTokenException {
+    accessor.updateToken(accountId, token);
+  }
+
+  ImmutableList<AuthToken> fallBackToLegacyHttpPassword(Account.Id accountId) {
+    AccountState accountState = accountCache.getEvenIfMissing(accountId);
+    Optional<ExternalId> optUser =
+        accountState.externalIds().stream()
+            .filter(e -> e.key().scheme().equals(SCHEME_USERNAME))
+            .findFirst();
+    if (optUser.isEmpty()) {
+      return ImmutableList.of();
+    }
+    ExternalId user = optUser.get();
+    String password = user.password();
+    if (password != null) {
+      try {
+        return ImmutableList.of(AuthToken.create(LEGACY_ID, password));
+      } catch (InvalidAuthTokenException e1) {
+        // Can be ignored because the token ID is hardcoded.
+      }
+    }
+    return ImmutableList.of();
+  }
+
+  void purgeHttpPassword(Account.Id accountId) {
+    try {
+      ImmutableSet<ExternalId> accountExtIds = externalIds.byAccount(accountId);
+      Optional<ExternalId> usernameExtId =
+          accountExtIds.stream()
+              .filter(e -> e.key().scheme().equals(SCHEME_USERNAME) && e.password() != null)
+              .findFirst();
+      if (usernameExtId.isPresent()) {
+        ExternalId oldExtId = usernameExtId.get();
+        ExternalId updatedExtId =
+            externalIdFactory.createWithEmail(
+                oldExtId.key(), oldExtId.accountId(), oldExtId.email());
+        try (Repository repo = repoManager.openRepository(allUsers)) {
+          ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
+          extIdNotes.replace(oldExtId, updatedExtId);
+          try (MetaDataUpdate metaDataUpdate = metaDataUpdateServerFactory.get().create(allUsers)) {
+            metaDataUpdate.setMessage("Delete old HTTP password on token creation");
+            extIdNotes.commit(metaDataUpdate);
+          }
+        }
+      }
+    } catch (IOException | ConfigInvalidException | DuplicateExternalIdKeyException e) {
+      logger.atSevere().withCause(e).log("Unable to purge HTTP password.");
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index c6ffce5..6d5c66b 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -20,6 +20,7 @@
 import static java.util.stream.Stream.concat;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -44,7 +45,6 @@
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Stream;
@@ -108,7 +108,7 @@
     Set<FillOptions> fillOptionsWithoutSecondaryEmails =
         Sets.difference(options, EnumSet.of(FillOptions.SECONDARY_EMAILS));
     Set<Account.Id> ids = stream(in).map(a -> Account.id(a._accountId)).collect(toSet());
-    Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
+    ImmutableMap<Account.Id, AccountState> accountStates = accountCache.get(ids);
     for (AccountInfo info : in) {
       Account.Id id = Account.id(info._accountId);
       AccountState state = accountStates.get(id);
@@ -124,6 +124,7 @@
 
       } else {
         info._accountId = options.contains(FillOptions.ID) ? id.get() : null;
+        info.deleted = options.contains(FillOptions.DELETED) ? true : null;
       }
     }
   }
@@ -131,7 +132,7 @@
   @Override
   public void fillAccountAttributeInfo(Iterable<? extends AccountAttribute> in) {
     Set<Account.Id> ids = stream(in).map(a -> Account.id(a.accountId)).collect(toSet());
-    Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
+    ImmutableMap<Account.Id, AccountState> accountStates = accountCache.get(ids);
     for (AccountAttribute accountAttribute : in) {
       Account.Id id = Account.id(accountAttribute.accountId);
       AccountState accountState = accountStates.get(id);
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/server/account/InvalidAuthTokenException.java
similarity index 64%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/server/account/InvalidAuthTokenException.java
index 3ec74ba..8666db6 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/server/account/InvalidAuthTokenException.java
@@ -12,9 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.server.account;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+public class InvalidAuthTokenException extends Exception {
+  private static final long serialVersionUID = 1L;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+  public InvalidAuthTokenException(String message) {
+    super(message);
+  }
+
+  public InvalidAuthTokenException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/java/com/google/gerrit/server/account/MaxAuthTokenLifetimeApplier.java b/java/com/google/gerrit/server/account/MaxAuthTokenLifetimeApplier.java
new file mode 100644
index 0000000..4c813d1
--- /dev/null
+++ b/java/com/google/gerrit/server/account/MaxAuthTokenLifetimeApplier.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.annotations.VisibleForTesting;
+import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.storage.notedb.AccountsNoteDbImpl;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
+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.project.LockManager;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.util.io.NullOutputStream;
+
+public class MaxAuthTokenLifetimeApplier implements Runnable {
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  @VisibleForTesting public static final String DEFAULT_ID = "default";
+
+  private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
+  private final AuthTokenAccessor tokenAccessor;
+  private final LockManager lockManager;
+  private final AccountsNoteDbImpl accounts;
+  private final Instant expiryInstant;
+
+  private MultiProgressMonitor mpm;
+  private Task doneTask;
+  private Task failedTask;
+
+  public interface Factory {
+    public MaxAuthTokenLifetimeApplier create(Instant expiryInstant);
+  }
+
+  @AssistedInject
+  public MaxAuthTokenLifetimeApplier(
+      MultiProgressMonitor.Factory multiProgressMonitorFactory,
+      AuthTokenAccessor tokenAccessor,
+      LockManager lockManager,
+      AccountsNoteDbImpl accounts,
+      @Assisted Instant expiryInstant) {
+    this.multiProgressMonitorFactory = multiProgressMonitorFactory;
+    this.tokenAccessor = tokenAccessor;
+    this.lockManager = lockManager;
+    this.accounts = accounts;
+    this.expiryInstant = expiryInstant;
+  }
+
+  @Override
+  public void run() {
+    Lock lock = lockManager.getLock("ReduceMaxAuthTokenLifetime");
+    if (!lock.tryLock()) {
+      logger.atWarning().log("Task applying limit to auth token lifetime already running.");
+      return;
+    }
+    try {
+      Set<Account.Id> todo = accounts.allIds();
+
+      Stopwatch sw = Stopwatch.createStarted();
+      mpm =
+          multiProgressMonitorFactory.create(
+              NullOutputStream.INSTANCE, TaskKind.MIGRATION, "Limiting auth token lifetime", true);
+      doneTask = mpm.beginSubTask("done", MultiProgressMonitor.UNKNOWN);
+      failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+      for (Account.Id accountId : todo) {
+        adaptTokenLifetime(accountId, expiryInstant);
+        logger.atInfo().atMostEvery(30, TimeUnit.SECONDS).log(
+            "Adapted lifetime of %d auth tokens (%d failed).",
+            doneTask.getCount(), failedTask.getCount());
+      }
+      doneTask.end();
+      failedTask.end();
+      mpm.end();
+      sw.stop();
+      logger.atInfo().log(
+          "Finished adapting lifetime of %d auth tokens. Total duration was %d min.",
+          doneTask.getCount(), sw.elapsed(TimeUnit.MINUTES));
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log("Could not load accounts.");
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  private void adaptTokenLifetime(Account.Id accountId, Instant maxAllowedExpirationInstant) {
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Limiting lifetime of auth tokens",
+            Metadata.builder().projectName("All-Users").accountId(accountId.get()).build())) {
+      for (AuthToken authToken : tokenAccessor.getTokens(accountId)) {
+        if (authToken.expirationDate().isEmpty()
+            || authToken.expirationDate().get().isAfter(maxAllowedExpirationInstant)) {
+          AuthToken updatedToken;
+          try {
+            updatedToken =
+                AuthToken.create(
+                    authToken.id(),
+                    authToken.hashedToken(),
+                    Optional.of(maxAllowedExpirationInstant));
+            tokenAccessor.updateToken(accountId, updatedToken);
+            doneTask.update(1);
+          } catch (InvalidAuthTokenException | IOException | ConfigInvalidException e) {
+            logger.atSevere().withCause(e).log(
+                "Failed to limit lifetime of token %s for account %d",
+                authToken.id(), accountId.get());
+            failedTask.update(1);
+          }
+        }
+      }
+    } catch (IOException | ConfigInvalidException e) {
+      logger.atSevere().withCause(e).log(
+          "Failed to limit lifetime of tokens for account %d", accountId.get());
+      failedTask.update(1);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/PasswordMigrator.java b/java/com/google/gerrit/server/account/PasswordMigrator.java
new file mode 100644
index 0000000..38c3df5
--- /dev/null
+++ b/java/com/google/gerrit/server/account/PasswordMigrator.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+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.project.LockManager;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.io.NullOutputStream;
+
+public class PasswordMigrator implements Runnable {
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  @VisibleForTesting public static final String DEFAULT_ID = "default";
+
+  private final GitRepositoryManager repoManager;
+  private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
+  private final AuthTokenAccessor tokenAccessor;
+  private final ExternalIds externalIds;
+  private final ExternalIdFactory externalIdFactory;
+  private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
+  private final AllUsersName allUsers;
+  private final ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
+  private final Optional<Instant> expirationDate;
+  private final LockManager lockManager;
+
+  private MultiProgressMonitor mpm;
+  private Task doneTask;
+  private Task failedTask;
+
+  public interface Factory {
+    public PasswordMigrator create(Optional<Instant> expiryDate);
+  }
+
+  @AssistedInject
+  public PasswordMigrator(
+      GitRepositoryManager repoManager,
+      MultiProgressMonitor.Factory multiProgressMonitorFactory,
+      AuthTokenAccessor tokenAccessor,
+      ExternalIds externalIds,
+      ExternalIdFactory externalIdFactory,
+      AllUsersName allUsers,
+      ExternalIdNotes.FactoryNoReindex externalIdNotesFactory,
+      Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
+      @Assisted Optional<Instant> expirationDate,
+      LockManager lockManager) {
+    this.repoManager = repoManager;
+    this.multiProgressMonitorFactory = multiProgressMonitorFactory;
+    this.tokenAccessor = tokenAccessor;
+    this.externalIds = externalIds;
+    this.externalIdFactory = externalIdFactory;
+    this.allUsers = allUsers;
+    this.externalIdNotesFactory = externalIdNotesFactory;
+    this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
+    this.expirationDate = expirationDate;
+    this.lockManager = lockManager;
+  }
+
+  @Override
+  public void run() {
+    Lock lock = lockManager.getLock("MigratePasswordsToTokens");
+    if (!lock.tryLock()) {
+      logger.atWarning().log("Migration of passwords to tokens already running.");
+      return;
+    }
+    try {
+      ImmutableSet<ExternalId> todo;
+      try {
+        todo = getAllUsernameExternalIds();
+      } catch (IOException | ConfigInvalidException e) {
+        logger.atSevere().withCause(e).log("Unable to read external IDs.");
+        return;
+      }
+
+      if (todo.isEmpty()) {
+        logger.atInfo().log("No accounts with HTTP passwords exist. Nothing to do.");
+        return;
+      }
+
+      Stopwatch sw = Stopwatch.createStarted();
+      mpm =
+          multiProgressMonitorFactory.create(
+              NullOutputStream.INSTANCE, TaskKind.MIGRATION, "Migrating HTTP passwords", true);
+      doneTask = mpm.beginSubTask("passwords", todo.size());
+      failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+      List<ExternalId> failedToMigrate = new ArrayList<>();
+      for (ExternalId extId : todo) {
+        if (!createToken(extId, expirationDate)) {
+          failedToMigrate.add(extId);
+        }
+        logger.atInfo().atMostEvery(30, TimeUnit.SECONDS).log(
+            "Migrated %d/%d HTTP passwords (%d failed).",
+            doneTask.getCount(), todo.size(), failedTask.getCount());
+      }
+      doneTask.end();
+      failedTask.end();
+      mpm.end();
+      logger.atInfo().log(
+          "Finished creating tokens. %d/%d finished successfully in %d min. %d tasks" + " failed.",
+          doneTask.getCount(), todo.size(), sw.elapsed(TimeUnit.MINUTES), failedTask.getCount());
+      logger.atInfo().log("Starting to delete HTTP passwords from External IDs.");
+      try {
+        deletePasswordsInExternalId(failedToMigrate);
+      } catch (ConfigInvalidException e) {
+        logger.atSevere().withCause(e).log(
+            "Migration of HTTP passwords failed. Unable to load External IDs.");
+      }
+      sw.stop();
+      logger.atInfo().log(
+          "Finished deleting HTTP passwords from External IDs. Total migration to tokens took %d"
+              + " min.",
+          sw.elapsed(TimeUnit.MINUTES));
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  private boolean createToken(ExternalId extId, Optional<Instant> expirationDate) {
+    String hashedPassword = extId.password();
+    if (hashedPassword == null) {
+      return true;
+    }
+    Account.Id accountId = extId.accountId();
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Creating token from HTTP password",
+            Metadata.builder().projectName("All-Users").accountId(accountId.get()).build())) {
+      if (tokenAccessor.getToken(accountId, PasswordMigrator.DEFAULT_ID).isPresent()) {
+        logger.atFine().log("HTTP password of account %d was already migrated.", accountId.get());
+      } else {
+        @SuppressWarnings("unused")
+        var unused =
+            tokenAccessor.addToken(
+                accountId, PasswordMigrator.DEFAULT_ID, hashedPassword, expirationDate);
+      }
+      doneTask.update(1);
+    } catch (IOException | ConfigInvalidException | InvalidAuthTokenException e) {
+      logger.atSevere().withCause(e).log(
+          "Failed to migrate HTTP password to token for account %d", accountId.get());
+      failedTask.update(1);
+      return false;
+    }
+    return true;
+  }
+
+  private void deletePasswordsInExternalId(List<ExternalId> exclude) throws ConfigInvalidException {
+
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Deleting Passwords from external IDs",
+            Metadata.builder()
+                .projectName("All-Users")
+                .noteDbRefName("refs/meta/external-ids")
+                .build())) {
+      ImmutableSet<ExternalId> extIdsWithPassword;
+      try {
+        extIdsWithPassword = getAllUsernameExternalIds();
+      } catch (IOException | ConfigInvalidException e) {
+        logger.atSevere().withCause(e).log(
+            "Unable to read external IDs. Can't delete passwords from external IDs.");
+        return;
+      }
+      Set<ExternalId> extIdsToUpdate =
+          extIdsWithPassword.stream().filter(e -> !exclude.contains(e)).collect(Collectors.toSet());
+      Set<ExternalId> updatedExtIds =
+          extIdsToUpdate.stream()
+              .map(e -> externalIdFactory.createWithEmail(e.key(), e.accountId(), e.email()))
+              .collect(Collectors.toSet());
+      try (Repository repo = repoManager.openRepository(allUsers)) {
+        ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
+        extIdNotes.replaceByKeys(
+            extIdsToUpdate.stream().map(e -> e.key()).collect(Collectors.toSet()), updatedExtIds);
+        try (MetaDataUpdate metaDataUpdate = metaDataUpdateServerFactory.get().create(allUsers)) {
+          metaDataUpdate.setMessage("Migrate HTTP passwords to tokens");
+          extIdNotes.commit(metaDataUpdate);
+        }
+      } catch (IOException | DuplicateExternalIdKeyException e) {
+        logger.atSevere().withCause(e).log("Unable to replace External IDs.");
+      }
+    }
+  }
+
+  private ImmutableSet<ExternalId> getAllUsernameExternalIds()
+      throws IOException, ConfigInvalidException {
+    return ImmutableSet.copyOf(
+        externalIds.all().stream()
+            .filter(e -> e.key().scheme().equals(SCHEME_USERNAME) && e.password() != null)
+            .collect(Collectors.toSet()));
+  }
+}
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 38b79f0..5807d34 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -14,14 +14,12 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
@@ -42,10 +40,16 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -147,14 +151,46 @@
 
   @Override
   public Collection<GroupReference> suggest(String name, ProjectState project) {
-    Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+    List<List<GroupReference>> suggestionsByBackend = new ArrayList<>();
     backends.runEach(
         g -> {
           Collection<GroupReference> suggestions = g.suggest(name, project);
           suggestCount.increment(name(g), suggestions.size());
-          groups.addAll(suggestions);
+          if (!suggestions.isEmpty()) {
+            suggestionsByBackend.add(
+                suggestions.stream()
+                    .sorted(Comparator.comparing(gr -> gr.getName()))
+                    .sorted(Comparator.comparingInt(gr -> gr.getName().length()))
+                    .collect(Collectors.toList()));
+          }
         });
-    return groups;
+
+    // Using LinkedHashSet to preserve insertion order and avoid duplicates.
+    Set<GroupReference> interleaved = new LinkedHashSet<>();
+    int maxSize = 0;
+    for (List<GroupReference> list : suggestionsByBackend) {
+      maxSize = Math.max(maxSize, list.size());
+    }
+
+    for (int i = 0; i < maxSize; i++) {
+      for (List<GroupReference> list : suggestionsByBackend) {
+        if (i < list.size()) {
+          interleaved.add(list.get(i));
+        }
+      }
+    }
+
+    List<GroupReference> result = new ArrayList<>(interleaved);
+    Optional<GroupReference> exactMatch =
+        result.stream().filter(g -> g.getName().equalsIgnoreCase(name)).findFirst();
+
+    exactMatch.ifPresent(
+        group -> {
+          result.remove(group);
+          result.add(0, group);
+        });
+
+    return result;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/account/VersionedAuthTokens.java b/java/com/google/gerrit/server/account/VersionedAuthTokens.java
new file mode 100644
index 0000000..dc056b3
--- /dev/null
+++ b/java/com/google/gerrit/server/account/VersionedAuthTokens.java
@@ -0,0 +1,227 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.base.Preconditions.checkState;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.RefNames;
+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.meta.VersionedMetaData;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * 'tokens.config' file in the refs/users/CD/ABCD branches of the All-Users repository.
+ *
+ * <p>The `tokens.config' file stores the authentication tokens of the user. The file uses the git
+ * config format, where each token is a subsection.
+ */
+public class VersionedAuthTokens extends VersionedMetaData {
+
+  public interface Factory {
+    VersionedAuthTokens create(Account.Id accountId);
+  }
+
+  public static final String FILE_NAME = "tokens.config";
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsersName;
+  private final Optional<Duration> maxAuthTokenLifetime;
+  private final int maxTokens;
+
+  private final Account.Id accountId;
+  private final String ref;
+  private Map<String, AuthToken> tokens;
+
+  @Inject
+  public VersionedAuthTokens(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      AuthConfig authConfig,
+      @Assisted Account.Id accountId) {
+    this.repoManager = repoManager;
+    this.allUsersName = allUsersName;
+    this.maxAuthTokenLifetime = authConfig.getMaxAuthTokenLifetime();
+    this.maxTokens = authConfig.getMaxAuthTokensPerAccount();
+
+    this.accountId = accountId;
+    this.ref = RefNames.refsUsers(accountId);
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public VersionedAuthTokens load() throws IOException, ConfigInvalidException {
+    try (Repository git = repoManager.openRepository(allUsersName)) {
+      load(allUsersName, git);
+    }
+    return this;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    tokens = parse(readUTF8(FILE_NAME));
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    if (Strings.isNullOrEmpty(commit.getMessage())) {
+      commit.setMessage("Updated authentication tokens\n");
+    }
+
+    Config tokenConfig = new Config();
+    for (AuthToken token : tokens.values()) {
+      tokenConfig.setString("token", token.id(), "hash", token.hashedToken());
+      if (token.expirationDate().isPresent()) {
+        tokenConfig.setString(
+            "token", token.id(), "expiration", token.expirationDate().get().toString());
+      }
+    }
+
+    saveUTF8(FILE_NAME, tokenConfig.toText());
+    return true;
+  }
+
+  public static Map<String, AuthToken> parse(String s) throws ConfigInvalidException {
+    Config tokenConfig = new Config();
+    tokenConfig.fromText(s);
+    Map<String, AuthToken> tokens = new HashMap<>(tokenConfig.getSubsections("token").size());
+    for (String id : tokenConfig.getSubsections("token")) {
+      String expiration = tokenConfig.getString("token", id, "expiration");
+      Optional<Instant> expirationInstant =
+          expiration != null ? Optional.of(Instant.parse(expiration)) : Optional.empty();
+      try {
+        tokens.put(
+            id,
+            AuthToken.create(id, tokenConfig.getString("token", id, "hash"), expirationInstant));
+      } catch (InvalidAuthTokenException e) {
+        // Tokens were validated on creation.
+      }
+    }
+    return tokens;
+  }
+
+  /** Returns all authentication tokens. */
+  ImmutableList<AuthToken> getTokens() {
+    checkLoaded();
+    return ImmutableList.copyOf(tokens.values());
+  }
+
+  /**
+   * Returns the token with the given id.
+   *
+   * @param id id / name of the token
+   * @return the token, <code>null</code> if there is no token with this id
+   */
+  @Nullable
+  AuthToken getToken(String id) {
+    checkLoaded();
+    return tokens.get(id);
+  }
+
+  /**
+   * Adds a new token.
+   *
+   * @param id the id of the token
+   * @param hashedToken the hashed token to be added
+   * @param expiration the expiration instant of the token
+   * @return the new Token
+   * @throws InvalidAuthTokenException if the token or its ID is invalid
+   */
+  AuthToken addToken(String id, String hashedToken, Optional<Instant> expiration)
+      throws InvalidAuthTokenException {
+    checkLoaded();
+
+    AuthToken token = AuthToken.create(id, hashedToken, expiration);
+    return addToken(token);
+  }
+
+  /**
+   * Adds a new token.
+   *
+   * @param token the token to be added
+   * @return the new Token
+   * @throws InvalidAuthTokenException if the token is invalid, e.g. if the ID already exists or the
+   *     lifetime does not comply with the server's configuration.
+   */
+  @CanIgnoreReturnValue
+  AuthToken addToken(AuthToken token) throws InvalidAuthTokenException {
+    checkLoaded();
+
+    if (tokens.size() >= maxTokens) {
+      throw new InvalidAuthTokenException(
+          String.format("Maximum number of tokens (%d) already reached.", maxTokens));
+    }
+
+    if (tokens.containsKey(token.id())) {
+      throw new AuthTokenConflictException(token.id(), accountId);
+    }
+
+    if (maxAuthTokenLifetime.isPresent()) {
+      if (token.expirationDate().isEmpty()) {
+        throw new InvalidAuthTokenException("Tokens with unlimited lifetime are not permitted.");
+      } else if (token
+          .expirationDate()
+          .get()
+          .isAfter(Instant.now().plus(maxAuthTokenLifetime.get()))) {
+        throw new InvalidAuthTokenException(
+            String.format(
+                "Lifetime of token exceeds maximum allowed lifetime of %s days %s hours %s"
+                    + " minutes.",
+                maxAuthTokenLifetime.get().toDays(),
+                maxAuthTokenLifetime.get().toHoursPart(),
+                maxAuthTokenLifetime.get().toMinutesPart()));
+      }
+    }
+
+    tokens.put(token.id(), token);
+    return token;
+  }
+
+  /**
+   * Deletes the token with the given id.
+   *
+   * @param id the id
+   * @return <code>true</code> if a token with this id was found and deleted, <code>false
+   *     </code> if no token with the given id exists
+   */
+  boolean deleteToken(String id) {
+    checkLoaded();
+    return tokens.remove(id) != null;
+  }
+
+  private void checkLoaded() {
+    checkState(tokens != null, "Tokens not loaded yet");
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 1a6428c..eb9af6b 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -18,8 +18,6 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.auto.value.AutoValue;
-import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
@@ -37,8 +35,19 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 
-@AutoValue
-public abstract class ExternalId implements Serializable {
+/** The canonical External ID representation. */
+public record ExternalId(
+    Key key,
+    Account.Id accountId,
+    boolean isCaseInsensitive,
+    @Nullable String email,
+    @Deprecated @Nullable String password,
+    /**
+     * ID of the note blob in the external IDs branch that stores this external ID. {@code null} if
+     * the external ID was created in code and is not yet stored in Git.
+     */
+    @Nullable ObjectId blobId)
+    implements Serializable {
   // If these regular expressions are modified the same modifications should be done to the
   // corresponding regular expressions in the
   // com.google.gerrit.client.account.UsernameField class.
@@ -86,15 +95,13 @@
   }
 
   /**
-   * Returns all IDs of the provided external IDs that have the {@link ExternalId#SCHEME_MAILTO}
-   * scheme as a distinct stream.
+   * Returns all emails of the provided external IDs as a distinct stream.
    *
    * @param extIds external IDs
-   * @return distinct stream of all IDs of the provided external IDs that have the {@link
-   *     ExternalId#SCHEME_MAILTO} scheme
+   * @return distinct stream of all emails of the provided external IDs
    */
   public static Stream<String> getEmails(Collection<ExternalId> extIds) {
-    return extIds.stream().filter(e -> e.isScheme(SCHEME_MAILTO)).map(e -> e.key().id()).distinct();
+    return extIds.stream().map(e -> e.email()).filter(Objects::nonNull).distinct();
   }
 
   private static final long serialVersionUID = 1L;
@@ -152,8 +159,8 @@
   /** Scheme for Google OAuth external IDs. */
   public static final String SCHEME_GOOGLE_OAUTH = "google-oauth";
 
-  @AutoValue
-  public abstract static class Key implements Serializable {
+  /** Key for an external ID. */
+  public record Key(String scheme, String id, boolean isCaseInsensitive) implements Serializable {
     private static final long serialVersionUID = 1L;
 
     /**
@@ -166,7 +173,7 @@
      */
     @VisibleForTesting
     public static Key create(@Nullable String scheme, String id, boolean isCaseInsensitive) {
-      return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id, isCaseInsensitive);
+      return new Key(Strings.emptyToNull(scheme), id, isCaseInsensitive);
     }
 
     /**
@@ -182,17 +189,10 @@
       return create(externalId.substring(0, c), externalId.substring(c + 1), isCaseInsensitive);
     }
 
-    public abstract @Nullable String scheme();
-
-    public abstract String id();
-
-    public abstract boolean isCaseInsensitive();
-
     public boolean isScheme(String scheme) {
       return scheme.equals(scheme());
     }
 
-    @Memoized
     public ObjectId sha1() {
       return sha1(isCaseInsensitive());
     }
@@ -207,7 +207,6 @@
       return ObjectId.fromRaw(Hashing.sha1().hashString(keyString, UTF_8).asBytes());
     }
 
-    @Memoized
     public ObjectId caseSensitiveSha1() {
       return sha1(false);
     }
@@ -242,7 +241,6 @@
     }
 
     @Override
-    @Memoized
     public int hashCode() {
       return Objects.hash(sha1());
     }
@@ -252,13 +250,14 @@
     }
   }
 
+  @Deprecated
   public static ExternalId create(
       Key key,
       Account.Id accountId,
       @Nullable String email,
       @Nullable String hashedPassword,
       @Nullable ObjectId blobId) {
-    return new AutoValue_ExternalId(
+    return new ExternalId(
         key,
         accountId,
         key.isCaseInsensitive(),
@@ -267,21 +266,11 @@
         blobId);
   }
 
-  public abstract Key key();
-
-  public abstract Account.Id accountId();
-
-  public abstract boolean isCaseInsensitive();
-
-  public abstract @Nullable String email();
-
-  public abstract @Nullable String password();
-
-  /**
-   * ID of the note blob in the external IDs branch that stores this external ID. {@code null} if
-   * the external ID was created in code and is not yet stored in Git.
-   */
-  public abstract @Nullable ObjectId blobId();
+  public static ExternalId create(
+      Key key, Account.Id accountId, @Nullable String email, @Nullable ObjectId blobId) {
+    return new ExternalId(
+        key, accountId, key.isCaseInsensitive(), Strings.emptyToNull(email), null, blobId);
+  }
 
   public void checkThatBlobIdIsSet() {
     checkState(blobId() != null, "No blob ID set for external ID %s", key().get());
@@ -309,7 +298,6 @@
         && Objects.equals(password(), o.password());
   }
 
-  @Memoized
   @Override
   public int hashCode() {
     return Objects.hash(key(), accountId(), isCaseInsensitive(), email(), password());
@@ -329,7 +317,6 @@
    * </pre>
    */
   @Override
-  @Memoized
   public String toString() {
     Config c = new Config();
     writeToConfig(c);
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index 00a7f6c..6fd68e8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -40,6 +40,7 @@
    * @param hashedPassword the hashed password of the external ID, may be {@code null}
    * @return the created external ID
    */
+  @Deprecated
   ExternalId create(
       String scheme,
       String id,
@@ -57,21 +58,6 @@
   ExternalId create(ExternalId.Key key, Account.Id accountId);
 
   /**
-   * Creates an external ID.
-   *
-   * @param key the external Id key
-   * @param accountId the ID of the account to which the external ID belongs
-   * @param email the email of the external ID, may be {@code null}
-   * @param hashedPassword the hashed password of the external ID, may be {@code null}
-   * @return the created external ID
-   */
-  ExternalId create(
-      ExternalId.Key key,
-      Account.Id accountId,
-      @Nullable String email,
-      @Nullable String hashedPassword);
-
-  /**
    * Creates an external ID adding a hashed password computed from a plain password.
    *
    * @param key the external Id key
@@ -80,6 +66,7 @@
    * @param plainPassword the plain HTTP password, may be {@code null}
    * @return the created external ID
    */
+  @Deprecated
   ExternalId createWithPassword(
       ExternalId.Key key,
       Account.Id accountId,
@@ -94,9 +81,19 @@
    * @param plainPassword the plain HTTP password, may be {@code null}
    * @return the created external ID
    */
+  @Deprecated
   ExternalId createUsername(String id, Account.Id accountId, @Nullable String plainPassword);
 
   /**
+   * Create a external ID for a username (scheme "username").
+   *
+   * @param id the external ID, must not contain colons (':')
+   * @param accountId the ID of the account to which the external ID belongs
+   * @return the created external ID
+   */
+  ExternalId createUsername(String id, Account.Id accountId);
+
+  /**
    * Creates an external ID with an email.
    *
    * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
@@ -129,5 +126,6 @@
   ExternalId createEmail(Account.Id accountId, String email);
 
   /** Whether this {@link ExternalIdFactory} supports passwords. */
+  @Deprecated
   boolean arePasswordsAllowed();
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
index 68d8b0c..560a1c7 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
@@ -16,6 +16,8 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.ImplementedBy;
 import javax.inject.Inject;
@@ -79,12 +81,7 @@
    */
   public ExternalId.Key create(
       @Nullable String scheme, String id, boolean userNameCaseInsensitive) {
-    if (scheme != null
-        && (scheme.equals(ExternalId.SCHEME_USERNAME) || scheme.equals(ExternalId.SCHEME_GERRIT))) {
-      return ExternalId.Key.create(scheme, id, userNameCaseInsensitive);
-    }
-
-    return ExternalId.Key.create(scheme, id, false);
+    return createKey(scheme, id, userNameCaseInsensitive);
   }
 
   /**
@@ -94,10 +91,45 @@
    * @return the external Id key object
    */
   public ExternalId.Key parse(String externalId) {
+    return parse(externalId, isUserNameCaseInsensitive);
+  }
+
+  /**
+   * Parses an external ID key from its String representation. Prefer the non-static {@link
+   * #parse(String)} when possible
+   *
+   * @param externalId String representation of external ID key (e.g. username:johndoe)
+   * @param isUserNameCaseInsensitive whether the scheme used by the target host is case sensitive
+   * @return the external Id key object
+   */
+  @UsedAt(Project.GOOGLE)
+  public static ExternalId.Key parse(String externalId, boolean isUserNameCaseInsensitive) {
     int c = externalId.indexOf(':');
     if (c < 1 || c >= externalId.length() - 1) {
-      return create(null, externalId);
+      return createKey(null, externalId, isUserNameCaseInsensitive);
     }
-    return create(externalId.substring(0, c), externalId.substring(c + 1));
+    return createKey(
+        externalId.substring(0, c), externalId.substring(c + 1), isUserNameCaseInsensitive);
+  }
+
+  /**
+   * Creates an external ID key.
+   *
+   * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
+   *     ExternalId#SCHEME_USERNAME}.
+   * @param id the external ID, must not contain colons (':')
+   * @param userNameCaseInsensitive whether the external ID key is matched case insensitively
+   * @return the created external ID key
+   */
+  private static ExternalId.Key createKey(
+      @Nullable String scheme, String id, boolean userNameCaseInsensitive) {
+    if (scheme != null
+        && (scheme.equals(ExternalId.SCHEME_USERNAME)
+            || scheme.equals(
+                com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT))) {
+      return ExternalId.Key.create(scheme, id, userNameCaseInsensitive);
+    }
+
+    return ExternalId.Key.create(scheme, id, false);
   }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
index 2d3e241..3e71b33 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -20,6 +20,5 @@
   @Override
   protected void configure() {
     bind(ExternalIdKeyFactory.class);
-    bind(PasswordVerifier.class);
   }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java b/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
deleted file mode 100644
index 8d939e9..0000000
--- a/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account.externalids;
-
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
-
-import com.google.common.base.Strings;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.account.HashedPassword;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.inject.Inject;
-import java.util.Collection;
-
-/** Checks if a given username and password match a user's external IDs. */
-public class PasswordVerifier {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  public static final int MAX_PASSWORD_LENGTH_ACCORDING_TO_BCRYPT_LIMITS = 72;
-
-  private final ExternalIdKeyFactory externalIdKeyFactory;
-
-  private AuthConfig authConfig;
-
-  @Inject
-  public PasswordVerifier(ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
-    this.externalIdKeyFactory = externalIdKeyFactory;
-    this.authConfig = authConfig;
-  }
-
-  /** Returns {@code true} if there is an external ID matching both the username and password. */
-  public boolean checkPassword(
-      Collection<ExternalId> externalIds, String username, @Nullable String password) {
-    if (password == null || password.length() >= MAX_PASSWORD_LENGTH_ACCORDING_TO_BCRYPT_LIMITS) {
-      return false;
-    }
-
-    for (ExternalId id : externalIds) {
-      // Only process the "username:$USER" entry, which is unique.
-      if (!id.isScheme(SCHEME_USERNAME)) {
-        continue;
-      }
-
-      if (!id.key().equals(externalIdKeyFactory.create(SCHEME_USERNAME, username))) {
-        if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
-          continue;
-        }
-
-        if (!id.key().equals(externalIdKeyFactory.create(SCHEME_USERNAME, username, false))) {
-          continue;
-        }
-      }
-
-      String hashedStr = id.password();
-      if (!Strings.isNullOrEmpty(hashedStr)) {
-        try {
-          return HashedPassword.decode(hashedStr).checkPassword(password);
-        } catch (HashedPassword.DecoderException e) {
-          logger.atSevere().log("DecoderException for user %s: %s ", username, e.getMessage());
-          return false;
-        }
-      }
-    }
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
index d3de715..aa321e7 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
@@ -20,6 +20,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.InlineMe;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.HashedPassword;
@@ -48,33 +49,24 @@
   }
 
   @Override
+  public ExternalId create(ExternalId.Key key, Account.Id accountId) {
+    return create(key, accountId, null, null);
+  }
+
+  @Override
   public ExternalId create(String scheme, String id, Account.Id accountId) {
     return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
   }
 
   @Override
+  @Deprecated
   public ExternalId create(
       String scheme,
       String id,
       Account.Id accountId,
       @Nullable String email,
       @Nullable String hashedPassword) {
-    return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
-  }
-
-  @Override
-  public ExternalId create(ExternalId.Key key, Account.Id accountId) {
-    return create(key, accountId, null, null);
-  }
-
-  @Override
-  public ExternalId create(
-      ExternalId.Key key,
-      Account.Id accountId,
-      @Nullable String email,
-      @Nullable String hashedPassword) {
-    return create(
-        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
+    return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword, null);
   }
 
   ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
@@ -92,6 +84,7 @@
    *     {@code null} if the external ID was created in code and is not yet stored in Git.
    * @return the created external ID
    */
+  @Deprecated
   public ExternalId create(
       ExternalId.Key key,
       Account.Id accountId,
@@ -102,7 +95,23 @@
         key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
   }
 
+  /**
+   * Creates an external ID.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+   *     {@code null} if the external ID was created in code and is not yet stored in Git.
+   * @return the created external ID
+   */
+  public ExternalId create(
+      ExternalId.Key key, Account.Id accountId, @Nullable String email, @Nullable ObjectId blobId) {
+    return ExternalId.create(key, accountId, Strings.emptyToNull(email), blobId);
+  }
+
   @Override
+  @Deprecated
   public ExternalId createWithPassword(
       ExternalId.Key key,
       Account.Id accountId,
@@ -111,10 +120,11 @@
     plainPassword = Strings.emptyToNull(plainPassword);
     String hashedPassword =
         plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
-    return create(key, accountId, email, hashedPassword);
+    return create(key, accountId, email, hashedPassword, null);
   }
 
   @Override
+  @Deprecated
   public ExternalId createUsername(
       String id, Account.Id accountId, @Nullable String plainPassword) {
     return createWithPassword(
@@ -125,6 +135,11 @@
   }
 
   @Override
+  public ExternalId createUsername(String id, Account.Id accountId) {
+    return create(externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id), accountId);
+  }
+
+  @Override
   public ExternalId createWithEmail(
       String scheme, String id, Account.Id accountId, @Nullable String email) {
     return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
@@ -224,7 +239,9 @@
   }
 
   @Override
-  public boolean arePasswordsAllowed() {
+  @Deprecated
+  @InlineMe(replacement = "true")
+  public final boolean arePasswordsAllowed() {
     return true;
   }
 
diff --git a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
index eea78e5..f989b74 100644
--- a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
+++ b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
@@ -49,8 +49,7 @@
         ident,
         (ins, noteMap) -> {
           ExternalId extId =
-              ExternalId.create(
-                  ExternalId.Key.parse(externalId, false), accountId, null, null, null);
+              ExternalId.create(ExternalId.Key.parse(externalId, false), accountId, null, null);
           ObjectId noteId = extId.key().sha1();
           Config c = new Config();
           extId.writeToConfig(c);
@@ -72,8 +71,7 @@
         ident,
         (ins, noteMap) -> {
           ExternalId extId =
-              ExternalId.create(
-                  ExternalId.Key.parse(externalId, false), accountId, null, null, null);
+              ExternalId.create(ExternalId.Key.parse(externalId, false), accountId, null, null);
           ObjectId noteId = ExternalId.Key.parse(externalId + "x", false).sha1();
           Config c = new Config();
           extId.writeToConfig(c);
diff --git a/java/com/google/gerrit/server/api/ApiUtil.java b/java/com/google/gerrit/server/api/ApiUtil.java
index 8a8e37c..8722e94 100644
--- a/java/com/google/gerrit/server/api/ApiUtil.java
+++ b/java/com/google/gerrit/server/api/ApiUtil.java
@@ -15,7 +15,17 @@
 package com.google.gerrit.server.api;
 
 import com.google.common.base.Throwables;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+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.UnprocessableEntityException;
+import com.google.gerrit.server.ExceptionHook.Status;
+import com.google.gerrit.server.ExceptionHookImpl;
+import com.google.gerrit.server.auth.AuthException;
+import java.util.Optional;
 
 /** Static utilities for API implementations. */
 public class ApiUtil {
@@ -31,9 +41,33 @@
    */
   public static RestApiException asRestApiException(String msg, Exception e)
       throws RuntimeException {
+    e = mapExceptionUsingDefaultExceptionHook(e);
     Throwables.throwIfUnchecked(e);
     return e instanceof RestApiException ? (RestApiException) e : RestApiException.wrap(msg, e);
   }
 
+  private static Exception mapExceptionUsingDefaultExceptionHook(Exception originalException) {
+    ExceptionHookImpl impl = new ExceptionHookImpl();
+    Optional<Status> status = impl.getStatus(originalException);
+    if (status.isPresent()) {
+      if (status.get().statusCode() == 400) {
+        return new BadRequestException(originalException.getMessage(), originalException);
+      } else if (status.get().statusCode() == 403) {
+        return new AuthException(originalException.getMessage(), originalException);
+      } else if (status.get().statusCode() == 404) {
+        return new ResourceNotFoundException(originalException.getMessage(), originalException);
+      } else if (status.get().statusCode() == 405) {
+        return new MethodNotAllowedException(originalException.getMessage(), originalException);
+      } else if (status.get().statusCode() == 409) {
+        return new ResourceConflictException(originalException.getMessage(), originalException);
+      } else if (status.get().statusCode() == 412) {
+        return new PreconditionFailedException(originalException.getMessage(), originalException);
+      } else if (status.get().statusCode() == 422) {
+        return new UnprocessableEntityException(originalException.getMessage(), originalException);
+      }
+    }
+    return originalException;
+  }
+
   private ApiUtil() {}
 }
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 5defc26..f5798fc 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
@@ -29,6 +30,8 @@
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
 import com.google.gerrit.extensions.api.accounts.StatusInput;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -52,9 +55,12 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.GpgApiAdapter;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.AddSshKey;
 import com.google.gerrit.server.restapi.account.CreateEmail;
+import com.google.gerrit.server.restapi.account.CreateToken;
 import com.google.gerrit.server.restapi.account.DeleteAccount;
 import com.google.gerrit.server.restapi.account.DeleteActive;
 import com.google.gerrit.server.restapi.account.DeleteDraftComments;
@@ -74,6 +80,7 @@
 import com.google.gerrit.server.restapi.account.GetPreferences;
 import com.google.gerrit.server.restapi.account.GetSshKeys;
 import com.google.gerrit.server.restapi.account.GetState;
+import com.google.gerrit.server.restapi.account.GetTokens;
 import com.google.gerrit.server.restapi.account.GetWatchedProjects;
 import com.google.gerrit.server.restapi.account.Index;
 import com.google.gerrit.server.restapi.account.PostWatchedProjects;
@@ -91,8 +98,10 @@
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class AccountApiImpl implements AccountApi {
   interface Factory {
@@ -138,6 +147,8 @@
   private final GetGroups getGroups;
   private final EmailApiImpl.Factory emailApi;
   private final PutName putName;
+  private final GetTokens getTokens;
+  private final CreateToken createToken;
   private final PutHttpPassword putHttpPassword;
   private final DeleteAccount deleteAccount;
 
@@ -181,6 +192,8 @@
       GetGroups getGroups,
       EmailApiImpl.Factory emailApi,
       PutName putName,
+      CreateToken createToken,
+      GetTokens getTokens,
       PutHttpPassword putPassword,
       DeleteAccount deleteAccount,
       @Assisted AccountResource account) {
@@ -223,6 +236,8 @@
     this.getGroups = getGroups;
     this.emailApi = emailApi;
     this.putName = putName;
+    this.createToken = createToken;
+    this.getTokens = getTokens;
     this.putHttpPassword = putPassword;
     this.deleteAccount = deleteAccount;
   }
@@ -612,8 +627,32 @@
     }
   }
 
+  @Override
+  @CanIgnoreReturnValue
+  public AuthTokenInfo createToken(AuthTokenInput input) throws RestApiException {
+    try {
+      return createToken.apply(account, IdString.fromDecoded(input.id), input).value();
+    } catch (InvalidAuthTokenException
+        | IOException
+        | ConfigInvalidException
+        | PermissionBackendException
+        | RestApiException e) {
+      throw asRestApiException("Cannot create token", e);
+    }
+  }
+
+  @Override
+  public List<AuthTokenInfo> getTokens() throws RestApiException {
+    try {
+      return getTokens.apply(account.getUser());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get tokens", e);
+    }
+  }
+
   @Nullable
   @Override
+  @Deprecated
   public String generateHttpPassword() throws RestApiException {
     HttpPasswordInput input = new HttpPasswordInput();
     input.generate = true;
@@ -629,6 +668,7 @@
 
   @Nullable
   @Override
+  @Deprecated
   public String setHttpPassword(String password) throws RestApiException {
     HttpPasswordInput input = new HttpPasswordInput();
     input.generate = false;
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index d373a12..9ce3d79 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.CustomKeyedValuesInput;
 import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.FlowApi;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.api.changes.MoveInput;
@@ -52,13 +53,16 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitMessageInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
+import com.google.gerrit.extensions.common.EvaluateChangeQueryExpressionResultInfo;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.FlowInput;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.InputWithMessage;
+import com.google.gerrit.extensions.common.IsFlowsEnabledInfo;
 import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RebaseChainInfo;
 import com.google.gerrit.extensions.common.RevertSubmissionInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInput;
 import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
@@ -83,6 +87,7 @@
 import com.google.gerrit.server.restapi.change.CreateMergePatchSet;
 import com.google.gerrit.server.restapi.change.DeleteChange;
 import com.google.gerrit.server.restapi.change.DeletePrivate;
+import com.google.gerrit.server.restapi.change.EvaluateChangeQueryExpression;
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.GetCustomKeyedValues;
 import com.google.gerrit.server.restapi.change.GetHashtags;
@@ -94,7 +99,6 @@
 import com.google.gerrit.server.restapi.change.Index;
 import com.google.gerrit.server.restapi.change.ListChangeComments;
 import com.google.gerrit.server.restapi.change.ListChangeDrafts;
-import com.google.gerrit.server.restapi.change.ListChangeRobotComments;
 import com.google.gerrit.server.restapi.change.ListReviewers;
 import com.google.gerrit.server.restapi.change.Move;
 import com.google.gerrit.server.restapi.change.PostCustomKeyedValues;
@@ -114,6 +118,10 @@
 import com.google.gerrit.server.restapi.change.SetWorkInProgress;
 import com.google.gerrit.server.restapi.change.SubmittedTogether;
 import com.google.gerrit.server.restapi.change.SuggestChangeReviewers;
+import com.google.gerrit.server.restapi.flow.CreateFlow;
+import com.google.gerrit.server.restapi.flow.FlowCollection;
+import com.google.gerrit.server.restapi.flow.IsFlowsEnabled;
+import com.google.gerrit.server.restapi.flow.ListFlows;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -134,9 +142,11 @@
   private final Changes changeApi;
   private final Reviewers reviewers;
   private final Revisions revisions;
+  private final FlowCollection flowCollection;
   private final ReviewerApiImpl.Factory reviewerApi;
   private final RevisionApiImpl.Factory revisionApi;
   private final ChangeMessageApiImpl.Factory changeMessageApi;
+  private final FlowApiImpl.Factory flowApi;
   private final ChangeMessages changeMessages;
   private final SuggestChangeReviewers suggestReviewers;
   private final ListReviewers listReviewers;
@@ -166,7 +176,6 @@
   private final AttentionSetApiImpl.Factory attentionSetApi;
   private final AddToAttentionSet addToAttentionSet;
   private final Provider<ListChangeComments> listCommentsProvider;
-  private final ListChangeRobotComments listChangeRobotComments;
   private final Provider<ListChangeDrafts> listDraftsProvider;
   private final ChangeEditApiImpl.Factory changeEditApi;
   private final Check check;
@@ -179,6 +188,10 @@
   private final SetReadyForReview setReady;
   private final GetMessage getMessage;
   private final PutMessage putMessage;
+  private final CreateFlow createFlow;
+  private final ListFlows listFlows;
+  private final IsFlowsEnabled isFlowsEnabled;
+  private final Provider<EvaluateChangeQueryExpression> evaluateChangeQueryExpressionProvider;
   private final Provider<GetPureRevert> getPureRevertProvider;
   private final DynamicOptionParser dynamicOptionParser;
   private final Injector injector;
@@ -189,9 +202,11 @@
       Changes changeApi,
       Reviewers reviewers,
       Revisions revisions,
+      FlowCollection flowCollection,
       ReviewerApiImpl.Factory reviewerApi,
       RevisionApiImpl.Factory revisionApi,
       ChangeMessageApiImpl.Factory changeMessageApi,
+      FlowApiImpl.Factory flowApi,
       ChangeMessages changeMessages,
       SuggestChangeReviewers suggestReviewers,
       ListReviewers listReviewers,
@@ -220,7 +235,6 @@
       AttentionSetApiImpl.Factory attentionSetApi,
       AddToAttentionSet addToAttentionSet,
       Provider<ListChangeComments> listCommentsProvider,
-      ListChangeRobotComments listChangeRobotComments,
       Provider<ListChangeDrafts> listDraftsProvider,
       ChangeEditApiImpl.Factory changeEditApi,
       Check check,
@@ -233,6 +247,10 @@
       SetReadyForReview setReady,
       GetMessage getMessage,
       PutMessage putMessage,
+      CreateFlow createFlow,
+      ListFlows listFlows,
+      IsFlowsEnabled isFlowsEnabled,
+      Provider<EvaluateChangeQueryExpression> evaluateChangeQueryExpressionProvider,
       Provider<GetPureRevert> getPureRevertProvider,
       DynamicOptionParser dynamicOptionParser,
       @Assisted ChangeResource change,
@@ -243,9 +261,11 @@
     this.revertSubmission = revertSubmission;
     this.reviewers = reviewers;
     this.revisions = revisions;
+    this.flowCollection = flowCollection;
     this.reviewerApi = reviewerApi;
     this.revisionApi = revisionApi;
     this.changeMessageApi = changeMessageApi;
+    this.flowApi = flowApi;
     this.changeMessages = changeMessages;
     this.suggestReviewers = suggestReviewers;
     this.listReviewers = listReviewers;
@@ -272,7 +292,6 @@
     this.attentionSetApi = attentionSetApi;
     this.addToAttentionSet = addToAttentionSet;
     this.listCommentsProvider = listCommentsProvider;
-    this.listChangeRobotComments = listChangeRobotComments;
     this.listDraftsProvider = listDraftsProvider;
     this.changeEditApi = changeEditApi;
     this.check = check;
@@ -285,6 +304,10 @@
     this.setReady = setReady;
     this.getMessage = getMessage;
     this.putMessage = putMessage;
+    this.createFlow = createFlow;
+    this.listFlows = listFlows;
+    this.isFlowsEnabled = isFlowsEnabled;
+    this.evaluateChangeQueryExpressionProvider = evaluateChangeQueryExpressionProvider;
     this.getPureRevertProvider = getPureRevertProvider;
     this.dynamicOptionParser = dynamicOptionParser;
     this.change = change;
@@ -316,6 +339,65 @@
   }
 
   @Override
+  public FlowInfo createFlow(FlowInput flowInput) throws RestApiException {
+    try {
+      return createFlow.apply(change, flowInput).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse reviewer", e);
+    }
+  }
+
+  @Override
+  public FlowApi flow(String flowUuid) throws RestApiException {
+    try {
+      return flowApi.create(flowCollection.parse(change, IdString.fromDecoded(flowUuid)));
+    } catch (Exception e) {
+      throw asRestApiException("Cannot parse flow", e);
+    }
+  }
+
+  @Override
+  public IsFlowsEnabledInfo isFlowsEnabled() throws RestApiException {
+    try {
+      return isFlowsEnabled.apply(change).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot check if flows are enabled", e);
+    }
+  }
+
+  @Override
+  public List<FlowInfo> flows() throws RestApiException {
+    try {
+      return listFlows.apply(change).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list flows", e);
+    }
+  }
+
+  @Override
+  public EvaluateChangeQueryExpressionRequest evaluateChangeQueryExpression() {
+    return new EvaluateChangeQueryExpressionRequest() {
+      @Override
+      public EvaluateChangeQueryExpressionResultInfo get() throws RestApiException {
+        return ChangeApiImpl.this.evaluateChangeQueryExpression(this);
+      }
+    };
+  }
+
+  public EvaluateChangeQueryExpressionResultInfo evaluateChangeQueryExpression(
+      EvaluateChangeQueryExpressionRequest request) throws RestApiException {
+    try {
+      EvaluateChangeQueryExpression evaluateChangeQueryExpression =
+          evaluateChangeQueryExpressionProvider.get();
+      evaluateChangeQueryExpression.expression = request.getExpression();
+      evaluateChangeQueryExpression.useIndex = request.getUseIndex();
+      return evaluateChangeQueryExpression.apply(change).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot evaluate change query expression", e);
+    }
+  }
+
+  @Override
   public void abandon(AbandonInput in) throws RestApiException {
     try {
       @SuppressWarnings("unused")
@@ -684,15 +766,6 @@
   }
 
   @Override
-  public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
-    try {
-      return listChangeRobotComments.apply(change).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot get robot comments", e);
-    }
-  }
-
-  @Override
   public DraftsRequest draftsRequest() {
     return new DraftsRequest() {
       @Override
diff --git a/java/com/google/gerrit/server/api/changes/ChangesModule.java b/java/com/google/gerrit/server/api/changes/ChangesModule.java
index 729396b..038dc31 100644
--- a/java/com/google/gerrit/server/api/changes/ChangesModule.java
+++ b/java/com/google/gerrit/server/api/changes/ChangesModule.java
@@ -24,7 +24,6 @@
 
     factory(ChangeApiImpl.Factory.class);
     factory(CommentApiImpl.Factory.class);
-    factory(RobotCommentApiImpl.Factory.class);
     factory(DraftApiImpl.Factory.class);
     factory(RevisionApiImpl.Factory.class);
     factory(FileApiImpl.Factory.class);
@@ -33,5 +32,6 @@
     factory(ChangeEditApiImpl.Factory.class);
     factory(ChangeMessageApiImpl.Factory.class);
     factory(AttentionSetApiImpl.Factory.class);
+    factory(FlowApiImpl.Factory.class);
   }
 }
diff --git a/java/com/google/gerrit/server/api/changes/FlowApiImpl.java b/java/com/google/gerrit/server/api/changes/FlowApiImpl.java
new file mode 100644
index 0000000..7e7a616
--- /dev/null
+++ b/java/com/google/gerrit/server/api/changes/FlowApiImpl.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.changes;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.api.changes.FlowApi;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.restapi.flow.DeleteFlow;
+import com.google.gerrit.server.restapi.flow.FlowResource;
+import com.google.gerrit.server.restapi.flow.GetFlow;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class FlowApiImpl implements FlowApi {
+  interface Factory {
+    FlowApiImpl create(FlowResource flowResource);
+  }
+
+  private final FlowResource flowResource;
+  private final GetFlow getFlow;
+  private final DeleteFlow deleteFlow;
+
+  @Inject
+  FlowApiImpl(GetFlow getFlow, DeleteFlow deleteFlow, @Assisted FlowResource flowResource) {
+    this.getFlow = getFlow;
+    this.deleteFlow = deleteFlow;
+    this.flowResource = flowResource;
+  }
+
+  @Override
+  public FlowInfo get() throws RestApiException {
+    try {
+      return getFlow.apply(flowResource).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot get flow", e);
+    }
+  }
+
+  @Override
+  public void delete() throws RestApiException {
+    try {
+      deleteFlow.apply(flowResource, new Input());
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete flow", e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index c84bde2..1198ac9 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewResult;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.RevisionReviewerApi;
-import com.google.gerrit.extensions.api.changes.RobotCommentApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ArchiveFormat;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -52,12 +51,10 @@
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.common.MergeableInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
@@ -86,7 +83,6 @@
 import com.google.gerrit.server.restapi.change.ListPortedDrafts;
 import com.google.gerrit.server.restapi.change.ListRevisionComments;
 import com.google.gerrit.server.restapi.change.ListRevisionDrafts;
-import com.google.gerrit.server.restapi.change.ListRobotComments;
 import com.google.gerrit.server.restapi.change.Mergeable;
 import com.google.gerrit.server.restapi.change.PostReview;
 import com.google.gerrit.server.restapi.change.PreviewFix;
@@ -94,7 +90,6 @@
 import com.google.gerrit.server.restapi.change.Rebase;
 import com.google.gerrit.server.restapi.change.Reviewed;
 import com.google.gerrit.server.restapi.change.RevisionReviewers;
-import com.google.gerrit.server.restapi.change.RobotComments;
 import com.google.gerrit.server.restapi.change.Submit;
 import com.google.gerrit.server.restapi.change.TestSubmitRule;
 import com.google.gerrit.server.restapi.change.TestSubmitType;
@@ -133,7 +128,6 @@
   private final Mergeable mergeable;
   private final FileApiImpl.Factory fileApi;
   private final ListRevisionComments listComments;
-  private final ListRobotComments listRobotComments;
   private final ListPortedComments listPortedComments;
   private final ListPortedDrafts listPortedDrafts;
   private final ApplyStoredFix applyStoredFix;
@@ -147,8 +141,6 @@
   private final DraftApiImpl.Factory draftFactory;
   private final Comments comments;
   private final CommentApiImpl.Factory commentFactory;
-  private final RobotComments robotComments;
-  private final RobotCommentApiImpl.Factory robotCommentFactory;
   private final GetRevisionActions revisionActions;
   private final TestSubmitType testSubmitType;
   private final TestSubmitType.Get getSubmitType;
@@ -181,7 +173,6 @@
       Mergeable mergeable,
       FileApiImpl.Factory fileApi,
       ListRevisionComments listComments,
-      ListRobotComments listRobotComments,
       ListPortedComments listPortedComments,
       ListPortedDrafts listPortedDrafts,
       ApplyStoredFix applyStoredFix,
@@ -195,8 +186,6 @@
       DraftApiImpl.Factory draftFactory,
       Comments comments,
       CommentApiImpl.Factory commentFactory,
-      RobotComments robotComments,
-      RobotCommentApiImpl.Factory robotCommentFactory,
       GetRevisionActions revisionActions,
       TestSubmitType testSubmitType,
       TestSubmitType.Get getSubmitType,
@@ -227,8 +216,6 @@
     this.mergeable = mergeable;
     this.fileApi = fileApi;
     this.listComments = listComments;
-    this.robotComments = robotComments;
-    this.listRobotComments = listRobotComments;
     this.listPortedComments = listPortedComments;
     this.listPortedDrafts = listPortedDrafts;
     this.applyStoredFix = applyStoredFix;
@@ -242,7 +229,6 @@
     this.draftFactory = draftFactory;
     this.comments = comments;
     this.commentFactory = commentFactory;
-    this.robotCommentFactory = robotCommentFactory;
     this.revisionActions = revisionActions;
     this.testSubmitType = testSubmitType;
     this.getSubmitType = getSubmitType;
@@ -433,15 +419,6 @@
   }
 
   @Override
-  public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
-    try {
-      return listRobotComments.apply(revision).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot retrieve robot comments", e);
-    }
-  }
-
-  @Override
   public List<CommentInfo> commentsAsList() throws RestApiException {
     try {
       return listComments.getComments(revision);
@@ -460,15 +437,6 @@
   }
 
   @Override
-  public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException {
-    try {
-      return listRobotComments.getComments(revision);
-    } catch (Exception e) {
-      throw asRestApiException("Cannot retrieve robot comments", e);
-    }
-  }
-
-  @Override
   public Map<String, List<CommentInfo>> portedComments() throws RestApiException {
     try {
       return listPortedComments.apply(revision).value();
@@ -565,15 +533,6 @@
   }
 
   @Override
-  public RobotCommentApi robotComment(String id) throws RestApiException {
-    try {
-      return robotCommentFactory.create(robotComments.parse(revision, IdString.fromDecoded(id)));
-    } catch (Exception e) {
-      throw asRestApiException("Cannot retrieve robot comment", e);
-    }
-  }
-
-  @Override
   public BinaryResult patch() throws RestApiException {
     try {
       return getPatch.apply(revision).value();
@@ -720,9 +679,4 @@
       throw asRestApiException("Cannot get archive", e);
     }
   }
-
-  @Override
-  public String etag() throws RestApiException {
-    throw new NotImplementedException();
-  }
 }
diff --git a/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java b/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
deleted file mode 100644
index ec13061..0000000
--- a/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.api.changes;
-
-import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
-
-import com.google.gerrit.extensions.api.changes.RobotCommentApi;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.change.RobotCommentResource;
-import com.google.gerrit.server.restapi.change.GetRobotComment;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-public class RobotCommentApiImpl implements RobotCommentApi {
-  interface Factory {
-    RobotCommentApiImpl create(RobotCommentResource c);
-  }
-
-  private final GetRobotComment getComment;
-  private final RobotCommentResource comment;
-
-  @Inject
-  RobotCommentApiImpl(GetRobotComment getComment, @Assisted RobotCommentResource comment) {
-    this.getComment = getComment;
-    this.comment = comment;
-  }
-
-  @Override
-  public RobotCommentInfo get() throws RestApiException {
-    try {
-      return getComment.apply(comment).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot retrieve robot comment", e);
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/api/config/ServerImpl.java b/java/com/google/gerrit/server/api/config/ServerImpl.java
index 1e925d5..22b9c71 100644
--- a/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.api.config.CachesApi;
@@ -28,7 +29,9 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.common.CacheInfo;
 import com.google.gerrit.extensions.common.ExperimentInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.webui.TopMenu;
@@ -42,6 +45,8 @@
 import com.google.gerrit.server.restapi.config.GetServerInfo;
 import com.google.gerrit.server.restapi.config.ListCaches;
 import com.google.gerrit.server.restapi.config.ListExperiments;
+import com.google.gerrit.server.restapi.config.ListGlobalLabels;
+import com.google.gerrit.server.restapi.config.ListGlobalSubmitRequirements;
 import com.google.gerrit.server.restapi.config.ListTopMenus;
 import com.google.gerrit.server.restapi.config.SetDiffPreferences;
 import com.google.gerrit.server.restapi.config.SetEditPreferences;
@@ -66,6 +71,8 @@
   private final ExperimentApiImpl.Factory experimentApi;
   private final ExperimentsCollection experimentsCollection;
   private final Provider<ListExperiments> listExperimentsProvider;
+  private final Provider<ListGlobalLabels> listGlobalLabelsProvider;
+  private final Provider<ListGlobalSubmitRequirements> listGlobalSubmitRequirementsProvider;
   private final CachesApiImpl.Factory cachesApi;
   private final CachesCollection cachesCollection;
   private final Provider<ListCaches> listCachesProvider;
@@ -84,6 +91,8 @@
       ExperimentApiImpl.Factory experimentApi,
       ExperimentsCollection experimentsCollection,
       Provider<ListExperiments> listExperimentsProvider,
+      Provider<ListGlobalLabels> listGlobalLabelsProvider,
+      Provider<ListGlobalSubmitRequirements> listGlobalSubmitRequirementsProvider,
       CachesApiImpl.Factory cachesApi,
       CachesCollection cachesCollection,
       Provider<ListCaches> listCachesProvider) {
@@ -99,6 +108,8 @@
     this.experimentApi = experimentApi;
     this.experimentsCollection = experimentsCollection;
     this.listExperimentsProvider = listExperimentsProvider;
+    this.listGlobalLabelsProvider = listGlobalLabelsProvider;
+    this.listGlobalSubmitRequirementsProvider = listGlobalSubmitRequirementsProvider;
     this.cachesApi = cachesApi;
     this.cachesCollection = cachesCollection;
     this.listCachesProvider = listCachesProvider;
@@ -227,6 +238,38 @@
   }
 
   @Override
+  public ListGlobalLabelsRequest listGlobalLabels() throws RestApiException {
+    return new ListGlobalLabelsRequest() {
+      @Override
+      public ImmutableList<LabelDefinitionInfo> get() throws RestApiException {
+        try {
+          ListGlobalLabels listGlobalLabels = listGlobalLabelsProvider.get();
+          return listGlobalLabels.apply(new ConfigResource()).value();
+        } catch (Exception e) {
+          throw asRestApiException("Cannot retrieve global labels", e);
+        }
+      }
+    };
+  }
+
+  @Override
+  public ListGlobalSubmitRequirementsRequest listGlobalSubmitRequirements()
+      throws RestApiException {
+    return new ListGlobalSubmitRequirementsRequest() {
+      @Override
+      public ImmutableList<SubmitRequirementInfo> get() throws RestApiException {
+        try {
+          ListGlobalSubmitRequirements listGlobalSubmitRequirements =
+              listGlobalSubmitRequirementsProvider.get();
+          return listGlobalSubmitRequirements.apply(new ConfigResource()).value();
+        } catch (Exception e) {
+          throw asRestApiException("Cannot retrieve global submit requirements", e);
+        }
+      }
+    };
+  }
+
+  @Override
   public CachesApi caches(String name) throws RestApiException {
     try {
       return cachesApi.create(
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 44882ba..ea9d09e 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -36,6 +36,8 @@
 import com.google.gerrit.extensions.api.projects.DashboardApi;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
 import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
+import com.google.gerrit.extensions.api.projects.DeleteChangesInput;
+import com.google.gerrit.extensions.api.projects.DeleteChangesResult;
 import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
 import com.google.gerrit.extensions.api.projects.DescriptionInput;
 import com.google.gerrit.extensions.api.projects.HeadInput;
@@ -73,6 +75,7 @@
 import com.google.gerrit.server.restapi.project.CreateAccessChange;
 import com.google.gerrit.server.restapi.project.CreateProject;
 import com.google.gerrit.server.restapi.project.DeleteBranches;
+import com.google.gerrit.server.restapi.project.DeleteChanges;
 import com.google.gerrit.server.restapi.project.DeleteTags;
 import com.google.gerrit.server.restapi.project.GetAccess;
 import com.google.gerrit.server.restapi.project.GetConfig;
@@ -137,6 +140,7 @@
   private final Provider<ListBranches> listBranches;
   private final Provider<ListTags> listTags;
   private final DeleteBranches deleteBranches;
+  private final DeleteChanges deleteChanges;
   private final DeleteTags deleteTags;
   private final CommitsCollection commitsCollection;
   private final CommitApiImpl.Factory commitApi;
@@ -183,6 +187,7 @@
       Provider<ListBranches> listBranches,
       Provider<ListTags> listTags,
       DeleteBranches deleteBranches,
+      DeleteChanges deleteChanges,
       DeleteTags deleteTags,
       CommitsCollection commitsCollection,
       CommitApiImpl.Factory commitApi,
@@ -227,6 +232,7 @@
         listBranches,
         listTags,
         deleteBranches,
+        deleteChanges,
         deleteTags,
         project,
         commitsCollection,
@@ -275,6 +281,7 @@
       Provider<ListBranches> listBranches,
       Provider<ListTags> listTags,
       DeleteBranches deleteBranches,
+      DeleteChanges deleteChanges,
       DeleteTags deleteTags,
       CommitsCollection commitsCollection,
       CommitApiImpl.Factory commitApi,
@@ -319,6 +326,7 @@
         listBranches,
         listTags,
         deleteBranches,
+        deleteChanges,
         deleteTags,
         null,
         commitsCollection,
@@ -366,6 +374,7 @@
       Provider<ListBranches> listBranches,
       Provider<ListTags> listTags,
       DeleteBranches deleteBranches,
+      DeleteChanges deleteChanges,
       DeleteTags deleteTags,
       ProjectResource project,
       CommitsCollection commitsCollection,
@@ -410,6 +419,7 @@
     this.listBranches = listBranches;
     this.listTags = listTags;
     this.deleteBranches = deleteBranches;
+    this.deleteChanges = deleteChanges;
     this.deleteTags = deleteTags;
     this.commitsCollection = commitsCollection;
     this.commitApi = commitApi;
@@ -656,6 +666,16 @@
   }
 
   @Override
+  public Map<DeleteChangesResult, Collection<String>> deleteChanges(DeleteChangesInput in)
+      throws RestApiException {
+    try {
+      return deleteChanges.apply(project, in).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete changes", e);
+    }
+  }
+
+  @Override
   public void deleteTags(DeleteTagsInput in) throws RestApiException {
     try {
       @SuppressWarnings("unused")
diff --git a/java/com/google/gerrit/server/auth/InternalAuthBackend.java b/java/com/google/gerrit/server/auth/InternalAuthBackend.java
index 0618750..6e2d7ef 100644
--- a/java/com/google/gerrit/server/auth/InternalAuthBackend.java
+++ b/java/com/google/gerrit/server/auth/InternalAuthBackend.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.externalids.PasswordVerifier;
+import com.google.gerrit.server.account.AuthTokenVerifier;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -26,14 +26,14 @@
 public class InternalAuthBackend implements AuthBackend {
   private final AccountCache accountCache;
   private final AuthConfig authConfig;
-  private final PasswordVerifier passwordVerifier;
+  private final AuthTokenVerifier tokenVerifier;
 
   @Inject
   InternalAuthBackend(
-      AccountCache accountCache, AuthConfig authConfig, PasswordVerifier passwordVerifier) {
+      AccountCache accountCache, AuthConfig authConfig, AuthTokenVerifier tokenVerifier) {
     this.accountCache = accountCache;
     this.authConfig = authConfig;
-    this.passwordVerifier = passwordVerifier;
+    this.tokenVerifier = tokenVerifier;
   }
 
   @Override
@@ -69,7 +69,7 @@
               + ": account inactive or not provisioned in Gerrit");
     }
 
-    if (!passwordVerifier.checkPassword(who.externalIds(), username, req.getPassword().get())) {
+    if (!tokenVerifier.checkToken(who.account().id(), req.getPassword().get())) {
       throw new InvalidCredentialsException();
     }
     return new AuthUser(AuthUser.UUID.create(username), username);
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 4eded88..ab4c852 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -36,6 +36,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import java.nio.file.Path;
 import java.time.Duration;
 import java.util.ArrayList;
@@ -46,6 +47,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -84,6 +86,7 @@
   private final Set<CacheOptions> options;
   private final boolean pruneOnStartup;
   private final Schedule schedule;
+  private final AtomicBoolean isDiskCacheReadOnly;
 
   @Inject
   H2CacheFactory(
@@ -93,7 +96,8 @@
       @Nullable @CacheCleanupExecutor ScheduledExecutorService cleanupExecutor,
       @Nullable @CacheStoreExecutor ExecutorService storeExecutor,
       @Nullable @CacheDir Path cacheDir,
-      Set<CacheOptions> options) {
+      Set<CacheOptions> options,
+      @Named("DiskCacheReadOnly") AtomicBoolean isDiskCacheReadOnly) {
     super(memCacheFactory, cfg, cacheDir);
     h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
     h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
@@ -107,6 +111,7 @@
     this.executor = storeExecutor;
     this.cleanup = cleanupExecutor;
     this.options = options;
+    this.isDiskCacheReadOnly = isDiskCacheReadOnly;
   }
 
   @Override
@@ -141,19 +146,20 @@
           cleanup.shutdownNow();
         }
 
-        List<Runnable> pending = executor.shutdownNow();
-        if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
-          if (pending != null && !pending.isEmpty()) {
-            logger.atInfo().log("Finishing %d disk cache updates", pending.size());
-            for (Runnable update : pending) {
-              update.run();
-            }
+        executor.shutdown();
+        if (!executor.awaitTermination(15, TimeUnit.MINUTES)) {
+          logger.atInfo().log(
+              "Timeout elapsed waiting for storage tasks to terminate, shutting down now.");
+          List<Runnable> pending = executor.shutdownNow();
+          logger.atInfo().log("Canceling %d pending storage tasks.", pending.size());
+          if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
+            logger.atInfo().log("Timeout elapsed waiting for cancelled tasks to terminate.");
           }
-        } else {
-          logger.atInfo().log("Timeout waiting for disk cache to close");
         }
       } catch (InterruptedException e) {
         logger.atWarning().log("Interrupted waiting for disk cache to shutdown");
+        executor.shutdownNow();
+        Thread.currentThread().interrupt();
       }
     }
     synchronized (caches) {
@@ -218,6 +224,7 @@
     if (h2AutoServer) {
       url.append(";AUTO_SERVER=TRUE");
     }
+    url.append(";DB_CLOSE_DELAY=-1");
     Duration refreshAfterWrite = def.refreshAfterWrite();
     if (has(def.configKey(), "refreshAfterWrite")) {
       long refreshAfterWriteInSec =
@@ -245,7 +252,8 @@
         expireAfterWrite,
         refreshAfterWrite,
         options.contains(CacheOptions.BUILD_BLOOM_FILTER),
-        options.contains(CacheOptions.TRACK_LAST_ACCESS));
+        options.contains(CacheOptions.TRACK_LAST_ACCESS),
+        isDiskCacheReadOnly);
   }
 
   private boolean has(String name, String var) {
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index aa7e120..39afb38 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -55,6 +55,7 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -160,7 +161,7 @@
             () -> {
               ValueHolder<V> h = store.getIfPresent(key);
               if (h == null) {
-                h = new ValueHolder<>(valueLoader.call(), Instant.ofEpochMilli(TimeUtil.nowMs()));
+                h = new ValueHolder<>(valueLoader.call(), TimeUtil.now());
                 ValueHolder<V> fh = h;
                 executor.execute(() -> store.put(key, fh));
               }
@@ -171,7 +172,7 @@
 
   @Override
   public void put(K key, V val) {
-    final ValueHolder<V> h = new ValueHolder<>(val, Instant.ofEpochMilli(TimeUtil.nowMs()));
+    final ValueHolder<V> h = new ValueHolder<>(val, TimeUtil.now());
     mem.put(key, h);
     executor.execute(() -> store.put(key, h));
   }
@@ -259,7 +260,7 @@
               "Loading value from cache", Metadata.builder().cacheKey(key.toString()).build())) {
         ValueHolder<V> h = store.getIfPresent(key);
         if (h == null) {
-          h = new ValueHolder<>(loader.load(key), Instant.ofEpochMilli(TimeUtil.nowMs()));
+          h = new ValueHolder<>(loader.load(key), TimeUtil.now());
           ValueHolder<V> fh = h;
           executor.execute(() -> store.put(key, fh));
         }
@@ -282,7 +283,7 @@
         }
         try {
           Map<K, V> remaining = loader.loadAll(notInMemory);
-          Instant instant = Instant.ofEpochMilli(TimeUtil.nowMs());
+          Instant instant = TimeUtil.now();
           storeInDatabase(remaining, instant);
           remaining
               .entrySet()
@@ -343,6 +344,8 @@
     private final ConcurrentBloomFilter<K> bloomFilter;
     private final boolean buildBloomFilter;
     private boolean trackLastAccess;
+    private final AtomicBoolean isDiskCacheReadOnly;
+    private volatile boolean ensuredSchemaCreation;
 
     SqlStore(
         String jdbcUrl,
@@ -355,7 +358,8 @@
         @Nullable Duration expireAfterWrite,
         @Nullable Duration refreshAfterWrite,
         boolean buildBloomFilter,
-        boolean trackLastAccess) {
+        boolean trackLastAccess,
+        AtomicBoolean isDiskCacheReadOnly) {
       this.url = jdbcUrl;
       this.keyType = createKeyType(keyType, keySerializer);
       this.valueSerializer = valueSerializer;
@@ -365,6 +369,7 @@
       this.refreshAfterWrite = refreshAfterWrite;
       this.buildBloomFilter = buildBloomFilter;
       this.trackLastAccess = trackLastAccess;
+      this.isDiskCacheReadOnly = isDiskCacheReadOnly;
 
       int cores = Runtime.getRuntime().availableProcessors();
       int keep = Math.min(cores, 16);
@@ -385,6 +390,38 @@
       return new ObjectKeyTypeImpl<>(serializer);
     }
 
+    private void createSchema() throws SQLException {
+      if (!ensuredSchemaCreation) {
+        synchronized (this) {
+          if (ensuredSchemaCreation) {
+            return;
+          }
+          try (SqlHandle h = new SqlHandle(url)) {
+            try (Statement stmt = h.conn.createStatement()) {
+              stmt.addBatch(
+                  "CREATE TABLE IF NOT EXISTS data"
+                      + "(k "
+                      + keyType.columnType()
+                      + " NOT NULL PRIMARY KEY HASH"
+                      + ",v OTHER NOT NULL"
+                      + ",created TIMESTAMP NOT NULL"
+                      + ",accessed TIMESTAMP NOT NULL"
+                      + ")");
+              stmt.addBatch(
+                  "ALTER TABLE data ADD COLUMN IF NOT EXISTS "
+                      + "space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v)");
+              stmt.addBatch(
+                  "ALTER TABLE data ADD COLUMN IF NOT EXISTS version INT DEFAULT 0 NOT NULL");
+              stmt.addBatch("CREATE INDEX IF NOT EXISTS version_key ON data(version, k)");
+              stmt.addBatch("CREATE INDEX IF NOT EXISTS accessed ON data(accessed)");
+              stmt.executeBatch();
+              ensuredSchemaCreation = true;
+            }
+          }
+        }
+      }
+    }
+
     void open() {
       bloomFilter.initIfNeeded();
     }
@@ -394,6 +431,17 @@
       while ((h = handles.poll()) != null) {
         h.close();
       }
+      shutdown();
+    }
+
+    private void shutdown() {
+      try (SqlHandle h = new SqlHandle(url)) {
+        try (Statement stmt = h.conn.createStatement()) {
+          stmt.execute("SHUTDOWN");
+        }
+      } catch (SQLException e) {
+        logger.atSevere().withCause(e).log("Cannot shutdown cache %s", url);
+      }
     }
 
     boolean mightContain(K key) {
@@ -536,6 +584,9 @@
     }
 
     private void touch(SqlHandle c, K key) throws IOException, SQLException {
+      if (isDiskCacheReadOnly.get()) {
+        return;
+      }
       if (c.touch == null) {
         c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=? AND version=?");
       }
@@ -550,7 +601,7 @@
     }
 
     void put(K key, ValueHolder<V> holder) {
-      if (holder.clean) {
+      if (isDiskCacheReadOnly.get() || holder.clean) {
         return;
       }
 
@@ -584,6 +635,9 @@
     }
 
     void invalidate(K key) {
+      if (isDiskCacheReadOnly.get()) {
+        return;
+      }
       SqlHandle c = null;
       try {
         c = acquire();
@@ -611,6 +665,9 @@
     }
 
     void invalidateAll() {
+      if (isDiskCacheReadOnly.get()) {
+        return;
+      }
       SqlHandle c = null;
       try {
         c = acquire();
@@ -627,6 +684,9 @@
     }
 
     synchronized void prune(Cache<K, ?> mem) {
+      if (isDiskCacheReadOnly.get()) {
+        return;
+      }
       SqlHandle c = null;
       try {
         c = acquire();
@@ -712,8 +772,9 @@
     }
 
     private SqlHandle acquire() throws SQLException {
+      createSchema();
       SqlHandle h = handles.poll();
-      return h != null ? h : new SqlHandle(url, keyType);
+      return h != null ? h : new SqlHandle(url);
     }
 
     private void release(SqlHandle h) {
@@ -731,7 +792,7 @@
     }
   }
 
-  static class SqlHandle {
+  static class SqlHandle implements AutoCloseable {
     private final String url;
     Connection conn;
     PreparedStatement get;
@@ -739,30 +800,13 @@
     PreparedStatement touch;
     PreparedStatement invalidate;
 
-    SqlHandle(String url, KeyType<?> type) throws SQLException {
+    SqlHandle(String url) throws SQLException {
       this.url = url;
       this.conn = org.h2.Driver.load().connect(url, null);
-      try (Statement stmt = conn.createStatement()) {
-        stmt.addBatch(
-            "CREATE TABLE IF NOT EXISTS data"
-                + "(k "
-                + type.columnType()
-                + " NOT NULL PRIMARY KEY HASH"
-                + ",v OTHER NOT NULL"
-                + ",created TIMESTAMP NOT NULL"
-                + ",accessed TIMESTAMP NOT NULL"
-                + ")");
-        stmt.addBatch(
-            "ALTER TABLE data ADD COLUMN IF NOT EXISTS "
-                + "space BIGINT AS OCTET_LENGTH(k) + OCTET_LENGTH(v)");
-        stmt.addBatch("ALTER TABLE data ADD COLUMN IF NOT EXISTS version INT DEFAULT 0 NOT NULL");
-        stmt.addBatch("CREATE INDEX IF NOT EXISTS version_key ON data(version, k)");
-        stmt.addBatch("CREATE INDEX IF NOT EXISTS accessed ON data(accessed)");
-        stmt.executeBatch();
-      }
     }
 
-    void close() {
+    @Override
+    public void close() {
       get = closeStatement(get);
       put = closeStatement(put);
       touch = closeStatement(touch);
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheModule.java b/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
index 68017f4..baf08e6 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.name.Names;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -36,6 +37,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.lib.Config;
 
 @ModuleImpl(name = CacheModule.PERSISTENT_MODULE)
@@ -54,6 +56,9 @@
 
   @Override
   protected void configure() {
+    bind(AtomicBoolean.class)
+        .annotatedWith(Names.named("DiskCacheReadOnly"))
+        .toInstance(new AtomicBoolean(false));
     bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
     listener().to(H2CacheFactory.class);
   }
diff --git a/java/com/google/gerrit/server/cancellation/BUILD b/java/com/google/gerrit/server/cancellation/BUILD
index c48456c..82265f6 100644
--- a/java/com/google/gerrit/server/cancellation/BUILD
+++ b/java/com/google/gerrit/server/cancellation/BUILD
@@ -11,5 +11,6 @@
         "//lib:guava",
         "//lib/commons:text",
         "//lib/errorprone:annotations",
+        "//lib/flogger:api",
     ],
 )
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/server/cancellation/PerformanceSummaryProvider.java
similarity index 71%
rename from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
rename to java/com/google/gerrit/server/cancellation/PerformanceSummaryProvider.java
index 3ec74ba..46b4d9b 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/server/cancellation/PerformanceSummaryProvider.java
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.server.cancellation;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+import java.util.Optional;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+/** Provides a performance summary for the purpose of logging. */
+public interface PerformanceSummaryProvider {
+  Optional<String> getPerformanceSummary();
+}
diff --git a/java/com/google/gerrit/server/cancellation/RequestStateContext.java b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
index 39cd441..20be925 100644
--- a/java/com/google/gerrit/server/cancellation/RequestStateContext.java
+++ b/java/com/google/gerrit/server/cancellation/RequestStateContext.java
@@ -16,8 +16,10 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -55,10 +57,15 @@
  * </ul>
  */
 public class RequestStateContext implements AutoCloseable {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   /** The {@link RequestStateProvider}s that have been registered for the thread. */
   private static final ThreadLocal<Set<RequestStateProvider>> threadLocalRequestStateProviders =
       new ThreadLocal<>();
 
+  private static final ThreadLocal<PerformanceSummaryProvider> performanceSummaryProvider =
+      new ThreadLocal<>();
+
   /** Whether currently a non-cancellable operation is being performed. */
   private static final ThreadLocal<Boolean> inNonCancellableOperation = new ThreadLocal<>();
 
@@ -84,6 +91,7 @@
             requestStateProvider ->
                 requestStateProvider.checkIfCancelled(
                     (reason, message) -> {
+                      logPerformanceSummary();
                       throw new RequestCancelledException(reason, message);
                     }));
   }
@@ -123,6 +131,18 @@
     return new RequestStateContext();
   }
 
+  private static void logPerformanceSummary() {
+    if (performanceSummaryProvider.get() == null) {
+      return;
+    }
+
+    Optional<String> performanceSummary = performanceSummaryProvider.get().getPerformanceSummary();
+    if (performanceSummary.isPresent()) {
+      logger.atWarning().log(
+          "Performance Summary for cancelled request:\n\n%s", performanceSummary.get());
+    }
+  }
+
   /**
    * The {@link RequestStateProvider}s that have been registered by this {@code
    * RequestStateContext}.
@@ -149,6 +169,19 @@
   }
 
   /**
+   * Sets a provider from which a performance summary can be retrieved for the purpose of logging.
+   *
+   * @param performanceSummaryProvider the {@link PerformanceSummaryProvider} that should be set
+   * @return the {@code RequestStateContext} instance for chaining calls
+   */
+  @CanIgnoreReturnValue
+  public RequestStateContext setPerformanceSummaryProvider(
+      PerformanceSummaryProvider performanceSummaryProvider) {
+    RequestStateContext.performanceSummaryProvider.set(performanceSummaryProvider);
+    return this;
+  }
+
+  /**
    * Closes this {@code RequestStateContext}.
    *
    * <p>Ensures that all {@link RequestStateProvider}s that have been registered by this {@code
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 41e02ca..52c7454 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -1024,11 +1024,14 @@
       for (Account.Id id : removable) {
         result.add(accountLoader.get(id));
       }
-      // Reviewers added by email are always removable
+      // Reviewers added by email
       for (Collection<AccountInfo> infos : out.reviewers.values()) {
         for (AccountInfo info : infos) {
           if (info._accountId == null) {
-            result.add(info);
+            if (canRemoveAnyReviewer
+                || removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), null, 0)) {
+              result.add(info);
+            }
           }
         }
       }
diff --git a/java/com/google/gerrit/server/change/ChangeMessages.java b/java/com/google/gerrit/server/change/ChangeMessages.java
index 787f036..c228ebe 100644
--- a/java/com/google/gerrit/server/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -14,25 +14,22 @@
 
 package com.google.gerrit.server.change;
 
-import org.eclipse.jgit.nls.NLS;
-import org.eclipse.jgit.nls.TranslationBundle;
+public class ChangeMessages {
+  public static String revertChangeDefaultMessage = "Revert \"{0}\"\n\nThis reverts commit {1}.";
+  public static String revertSubmissionDefaultMessage = "This reverts commit {0}.";
+  public static String revertSubmissionUserMessage = "Revert \"{0}\"\n\n{1}";
+  public static String revertSubmissionOfRevertSubmissionUserMessage = "Revert^{0} \"{1}\"\n\n{2}";
 
-public class ChangeMessages extends TranslationBundle {
-  public static ChangeMessages get() {
-    return NLS.getBundleFor(ChangeMessages.class);
-  }
+  public static String reviewerCantSeeChange = "{0} does not have permission to see this change";
+  public static String reviewerInvalid = "{0} is not a valid user identifier";
+  public static String reviewerNotFoundUserOrGroup =
+      "{0} does not identify a registered user or group";
 
-  public String revertChangeDefaultMessage;
-  public String revertSubmissionDefaultMessage;
-  public String revertSubmissionUserMessage;
-  public String revertSubmissionOfRevertSubmissionUserMessage;
-
-  public String reviewerCantSeeChange;
-  public String reviewerInvalid;
-  public String reviewerNotFoundUserOrGroup;
-
-  public String groupRemovalIsNotAllowed;
-  public String groupIsNotAllowed;
-  public String groupHasTooManyMembers;
-  public String groupManyMembersConfirmation;
+  public static String groupRemovalIsNotAllowed =
+      "Groups can't be removed from reviewers, so can't remove {0}.";
+  public static String groupIsNotAllowed = "The group {0} cannot be added as reviewer.";
+  public static String groupHasTooManyMembers =
+      "The group {0} has too many members to add them all as reviewers.";
+  public static String groupManyMembersConfirmation =
+      "The group {0} has {1} members. Do you want to add them all as reviewers?";
 }
diff --git a/java/com/google/gerrit/server/change/CommentThread.java b/java/com/google/gerrit/server/change/CommentThread.java
index 51422b1..2c9f6b5 100644
--- a/java/com/google/gerrit/server/change/CommentThread.java
+++ b/java/com/google/gerrit/server/change/CommentThread.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
@@ -34,6 +35,7 @@
  */
 @AutoValue
 public abstract class CommentThread<T extends Comment> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   /** Comments in the thread in exactly the order they appear in the thread. */
   public abstract ImmutableList<T> comments();
@@ -45,6 +47,16 @@
             comments().stream()
                 .filter(HumanComment.class::isInstance)
                 .map(HumanComment.class::cast));
+    if (lastHumanComment.isPresent()) {
+      logger.atFine().log(
+          "last human comment in comment thread: %s -> {parentUuid = %s, unresolved = %s}",
+          lastHumanComment.get().key,
+          lastHumanComment.get().parentUuid,
+          lastHumanComment.get().unresolved);
+    } else {
+      logger.atFine().log("no human comment in comment thread");
+    }
+
     // We often use false == null for boolean fields. It's also a safe fall-back if no human comment
     // is part of the thread.
     return lastHumanComment.map(comment -> comment.unresolved).orElse(false);
diff --git a/java/com/google/gerrit/server/change/CommentsValidator.java b/java/com/google/gerrit/server/change/CommentsValidator.java
index 0cdd9b6..c02fb83 100644
--- a/java/com/google/gerrit/server/change/CommentsValidator.java
+++ b/java/com/google/gerrit/server/change/CommentsValidator.java
@@ -89,8 +89,7 @@
   private void ensureValidInReplyTo(ChangeNotes changeNotes, String inReplyTo)
       throws BadRequestException {
     if (inReplyTo != null
-        && !commentsUtil.getPublishedHumanComment(changeNotes, inReplyTo).isPresent()
-        && !commentsUtil.getRobotComment(changeNotes, inReplyTo).isPresent()) {
+        && !commentsUtil.getPublishedHumanComment(changeNotes, inReplyTo).isPresent()) {
       throw new BadRequestException(
           String.format("Invalid inReplyTo, comment %s not found", inReplyTo));
     }
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index 0fe526c..b1395b5 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.ChangeEmail;
@@ -27,6 +28,8 @@
 import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.OutgoingEmail;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.PostUpdateContext;
 import com.google.inject.Inject;
@@ -43,6 +46,7 @@
   private final EmailFactories emailFactories;
   private final MessageIdGenerator messageIdGenerator;
   private final ChangeMessagesUtil changeMessagesUtil;
+  private final RemoveReviewerControl removeReviewerControl;
 
   private final Address reviewer;
   private String mailMessage;
@@ -53,15 +57,18 @@
       EmailFactories emailFactories,
       MessageIdGenerator messageIdGenerator,
       ChangeMessagesUtil changeMessagesUtil,
+      RemoveReviewerControl removeReviewerControl,
       @Assisted Address reviewer) {
     this.emailFactories = emailFactories;
     this.messageIdGenerator = messageIdGenerator;
     this.changeMessagesUtil = changeMessagesUtil;
+    this.removeReviewerControl = removeReviewerControl;
     this.reviewer = reviewer;
   }
 
   @Override
-  public boolean updateChange(ChangeContext ctx) {
+  public boolean updateChange(ChangeContext ctx) throws PermissionBackendException, AuthException {
+    removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), null);
     change = ctx.getChange();
     PatchSet.Id psId = ctx.getChange().currentPatchSetId();
     ChangeUpdate update = ctx.getUpdate(psId);
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 343d1a9..4087f84 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -161,7 +161,7 @@
     boolean votesRemoved = false;
     for (PatchSetApproval a : approvals(ctx, reviewerId)) {
       // Check if removing this vote is OK
-      removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
+      removeReviewerControl.checkRemoveReviewerApproval(ctx.getNotes(), ctx.getUser(), a);
       if (a.patchSetId().equals(patchSet.id()) && a.value() != 0) {
         oldApprovals.put(a.label(), a.value());
         removedVotesMsg
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
index d9c30d7..cda6191 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
@@ -112,6 +112,9 @@
           fileDiff.newMode().isPresent() && !fileDiff.newMode().get().equals(Patch.FileMode.MISSING)
               ? fileDiff.newMode().get().getMode()
               : null;
+      fileDiff.oldSha().ifPresent(sha -> fileInfo.oldSha = sha.name());
+      fileDiff.newSha().ifPresent(sha -> fileInfo.newSha = sha.name());
+
       if (fileDiff.patchType().get() == Patch.PatchType.BINARY) {
         fileInfo.binary = true;
       } else {
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index ce4e923..861bb70 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static java.util.stream.Collectors.toList;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
@@ -31,6 +31,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelTypes;
 import com.google.gerrit.entities.LabelValue;
@@ -53,6 +54,7 @@
 import com.google.gerrit.server.project.DeleteVoteControl;
 import com.google.gerrit.server.project.RemoveReviewerControl;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.PrologSubmitRuleUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.time.Instant;
@@ -78,15 +80,21 @@
   private final PermissionBackend permissionBackend;
   private final DeleteVoteControl deleteVoteControl;
   private final RemoveReviewerControl removeReviewerControl;
+  private final PrologSubmitRuleUtil ruleUtil;
+  private final LabelsJsonRules labelsJsonRules;
 
   @Inject
   LabelsJson(
       PermissionBackend permissionBackend,
       DeleteVoteControl deleteVoteControl,
-      RemoveReviewerControl removeReviewerControl) {
+      RemoveReviewerControl removeReviewerControl,
+      PrologSubmitRuleUtil ruleUtil,
+      LabelsJsonRules labelsJsonRules) {
     this.permissionBackend = permissionBackend;
     this.deleteVoteControl = deleteVoteControl;
     this.removeReviewerControl = removeReviewerControl;
+    this.ruleUtil = ruleUtil;
+    this.labelsJsonRules = labelsJsonRules;
   }
 
   /**
@@ -98,6 +106,11 @@
   Map<String, LabelInfo> labelsFor(
       AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
       throws PermissionBackendException {
+    // Delegate to old implementation if submit rules are enabled.
+    if (ruleUtil.isProjectRulesEnabled()) {
+      return labelsJsonRules.labelsFor(accountLoader, cd, standard, detailed);
+    }
+
     if (!standard && !detailed) {
       return null;
     }
@@ -106,11 +119,9 @@
         TraceContext.newTimer(
             "Get labels", Metadata.builder().changeId(cd.change().getId().get()).build())) {
       LabelTypes labelTypes = cd.getLabelTypes();
-      Map<String, LabelWithStatus> withStatus =
-          cd.change().isMerged()
-              ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
-              : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
-      return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
+      return cd.change().isMerged()
+          ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
+          : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
     }
   }
 
@@ -138,6 +149,11 @@
         }
         Set<LabelPermission.WithValue> can =
             permissionBackend.absentUser(filterApprovalsBy).change(cd).test(labelType);
+        for (LabelPermission.WithValue val : can) {
+          logger.atFine().log(
+              "User %s For label %s can vote %s ", filterApprovalsBy, val.label(), val.value());
+        }
+
         for (LabelValue v : labelType.getValues()) {
           boolean ok = can.contains(new LabelPermission.WithValue(labelType, v));
           if (isMerged) {
@@ -225,18 +241,18 @@
     label.all.add(approval);
   }
 
-  private Map<String, LabelWithStatus> labelsForUnsubmittedChange(
+  private Map<String, LabelInfo> labelsForUnsubmittedChange(
       AccountLoader accountLoader,
       ChangeData cd,
       LabelTypes labelTypes,
       boolean standard,
       boolean detailed)
       throws PermissionBackendException {
-    Map<String, LabelWithStatus> labels =
+    Map<String, LabelInfo> labels =
         initLabels(accountLoader, cd, labelTypes, /* includeAccountInfo= */ standard || detailed);
     setAllApprovals(accountLoader, cd, labels, detailed);
 
-    for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
+    for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
       Optional<LabelType> type = labelTypes.byLabel(e.getKey());
       if (!type.isPresent()) {
         continue;
@@ -276,14 +292,14 @@
     return ai;
   }
 
-  private void setLabelValues(LabelType type, LabelWithStatus l) {
-    l.label().defaultValue = type.getDefaultValue();
-    l.label().values = new LinkedHashMap<>();
+  private void setLabelValues(LabelType type, LabelInfo l) {
+    l.defaultValue = type.getDefaultValue();
+    l.values = new LinkedHashMap<>();
     for (LabelValue v : type.getValues()) {
-      l.label().values.put(v.formatValue(), v.getText());
+      l.values.put(v.formatValue(), v.getText());
     }
-    if (isOnlyZero(l.label().values.keySet())) {
-      l.label().values = null;
+    if (isOnlyZero(l.values.keySet())) {
+      l.values = null;
     }
   }
 
@@ -297,7 +313,7 @@
     return result;
   }
 
-  private Map<String, LabelWithStatus> labelsForSubmittedChange(
+  private Map<String, LabelInfo> labelsForSubmittedChange(
       AccountLoader accountLoader,
       ChangeData cd,
       LabelTypes labelTypes,
@@ -315,14 +331,12 @@
       }
     }
 
-    Set<String> labelNames = new HashSet<>();
     SetMultimap<Account.Id, PatchSetApproval> current =
         MultimapBuilder.hashKeys().hashSetValues().build();
     for (PatchSetApproval a : cd.currentApprovals()) {
       allUsers.add(a.accountId());
       Optional<LabelType> type = labelTypes.byLabel(a.labelId());
       if (type.isPresent()) {
-        labelNames.add(type.get().getName());
         // Not worth the effort to distinguish between votable/non-votable for 0
         // values on closed changes, since they can't vote anyway.
         current.put(a.accountId(), a);
@@ -330,27 +344,14 @@
     }
 
     // Since voting on merged changes is allowed all labels which apply to
-    // the change must be returned. All applying labels can be retrieved from
-    // the submit records, which is what initLabels does.
+    // the change must be returned. This is calculated based on labelTypes.
     // It's not possible to only compute the labels based on the approvals
     // since merged changes may not have approvals for all labels (e.g. if not
     // all labels are required for submit or if the change was auto-closed due
     // to direct push or if new labels were defined after the change was
     // merged).
-    Map<String, LabelWithStatus> labels;
-    labels = initLabels(accountLoader, cd, labelTypes, standard);
-
-    // Also include all labels for which approvals exists. E.g. there can be
-    // approvals for labels that are ignored by a Prolog submit rule and hence
-    // it wouldn't be included in the submit records.
-    for (String name : labelNames) {
-      if (!labels.containsKey(name)) {
-        labels.put(name, LabelWithStatus.create(new LabelInfo(), null));
-      }
-    }
-
+    Map<String, LabelInfo> labels = initLabels(accountLoader, cd, labelTypes, standard);
     labels.entrySet().stream()
-        .filter(e -> labelTypes.byLabel(e.getKey()).isPresent())
         .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()).get(), e.getValue()));
 
     for (Account.Id accountId : allUsers) {
@@ -359,10 +360,10 @@
       if (detailed) {
         pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
       }
-      for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
+      for (Map.Entry<String, LabelInfo> entry : labels.entrySet()) {
         ApprovalInfo ai = approvalInfo(accountLoader, accountId, 0, null, null, null);
         byLabel.put(entry.getKey(), ai);
-        addApproval(entry.getValue().label(), ai);
+        addApproval(entry.getValue(), ai);
       }
       for (PatchSetApproval psa : current.get(accountId)) {
         Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
@@ -391,66 +392,61 @@
     return labels;
   }
 
-  private Map<String, LabelWithStatus> initLabels(
+  private Map<String, LabelInfo> initLabels(
       AccountLoader accountLoader,
       ChangeData cd,
       LabelTypes labelTypes,
       boolean includeAccountInfo) {
-    Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
-    for (SubmitRecord rec : submitRecords(cd)) {
-      if (rec.labels == null) {
-        continue;
-      }
-      for (SubmitRecord.Label r : rec.labels) {
-        LabelWithStatus p = labels.get(r.label);
-        if (p == null || p.status().compareTo(r.status) < 0) {
-          LabelInfo n = new LabelInfo();
-          if (includeAccountInfo) {
-            switch (r.status) {
-              case OK:
-                n.approved = accountLoader.get(r.appliedBy);
-                break;
-              case REJECT:
-                n.rejected = accountLoader.get(r.appliedBy);
-                n.blocking = true;
-                break;
-              case IMPOSSIBLE:
-              case MAY:
-              case NEED:
-              default:
-                break;
-            }
-          }
+    Map<String, LabelInfo> labels = new TreeMap<>(labelTypes.nameComparator());
 
-          n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null;
-          labels.put(r.label, LabelWithStatus.create(n, r.status));
+    for (LabelType t : labelTypes.getLabelTypes()) {
+      LabelFunction labelFunction = t.getFunction();
+      checkState(
+          labelFunction != null,
+          "Unable to find the LabelFunction for label %s, change %s",
+          t.getName(),
+          cd.getId());
+
+      List<PatchSetApproval> approvals = cd.currentApprovals();
+      ImmutableList<PatchSetApproval> approvalsForLabel = getApprovalsForLabel(approvals, t);
+      LabelFunction.LabelStatus labelStatus = labelFunction.checkNoRule(t, approvalsForLabel);
+
+      LabelInfo labelInfo = new LabelInfo();
+      if (includeAccountInfo) {
+        switch (labelStatus.status()) {
+          case OK:
+            labelInfo.approved = accountLoader.get(labelStatus.voter());
+            break;
+          case REJECT:
+            labelInfo.rejected = accountLoader.get(labelStatus.voter());
+            labelInfo.blocking = true;
+            break;
+          case IMPOSSIBLE:
+          case MAY:
+          case NEED:
+          default:
+            break;
         }
       }
+
+      labelInfo.optional = labelStatus.status() == SubmitRecord.Label.Status.MAY ? true : null;
+      labelInfo.description = t.getDescription().orElse(null);
+      labels.put(t.getName(), labelInfo);
     }
-    setLabelsDescription(labels, labelTypes);
+
     return labels;
   }
 
-  private void setLabelsDescription(
-      Map<String, LabelsJson.LabelWithStatus> labels, LabelTypes labelTypes) {
-    for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
-      String labelName = entry.getKey();
-      Optional<LabelType> type = labelTypes.byLabel(labelName);
-      if (!type.isPresent()) {
-        continue;
-      }
-      LabelWithStatus labelWithStatus = entry.getValue();
-      labelWithStatus.label().description = type.get().getDescription().orElse(null);
-    }
+  private static ImmutableList<PatchSetApproval> getApprovalsForLabel(
+      List<PatchSetApproval> approvals, LabelType t) {
+    return approvals.stream()
+        .filter(psa -> psa.label().equals(t.getLabelId().get()))
+        .collect(toImmutableList());
   }
 
   private void setLabelScores(
-      AccountLoader accountLoader,
-      LabelType type,
-      LabelWithStatus l,
-      short score,
-      Account.Id accountId) {
-    if (l.label().approved != null || l.label().rejected != null) {
+      AccountLoader accountLoader, LabelType type, LabelInfo l, short score, Account.Id accountId) {
+    if (l.approved != null || l.rejected != null) {
       return;
     }
 
@@ -461,24 +457,21 @@
 
     if (score != 0) {
       if (score == type.getMin().getValue()) {
-        l.label().rejected = accountLoader.get(accountId);
+        l.rejected = accountLoader.get(accountId);
       } else if (score == type.getMax().getValue()) {
-        l.label().approved = accountLoader.get(accountId);
+        l.approved = accountLoader.get(accountId);
       } else if (score < 0) {
-        l.label().disliked = accountLoader.get(accountId);
-        l.label().value = score;
-      } else if (score > 0 && l.label().disliked == null) {
-        l.label().recommended = accountLoader.get(accountId);
-        l.label().value = score;
+        l.disliked = accountLoader.get(accountId);
+        l.value = score;
+      } else if (score > 0 && l.disliked == null) {
+        l.recommended = accountLoader.get(accountId);
+        l.value = score;
       }
     }
   }
 
   private void setAllApprovals(
-      AccountLoader accountLoader,
-      ChangeData cd,
-      Map<String, LabelWithStatus> labels,
-      boolean detailed)
+      AccountLoader accountLoader, ChangeData cd, Map<String, LabelInfo> labels, boolean detailed)
       throws PermissionBackendException {
     checkState(
         !cd.change().isMerged(),
@@ -508,7 +501,7 @@
         perm = permissionBackend.absentUser(accountId).change(cd);
         pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
       }
-      for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
+      for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
         Optional<LabelType> lt = labelTypes.byLabel(e.getKey());
         if (!lt.isPresent()) {
           // Ignore submit record for undefined label; likely the submit rule
@@ -541,16 +534,12 @@
           value = perm != null && perm.test(new LabelPermission(lt.get())) ? 0 : null;
         }
         addApproval(
-            e.getValue().label(),
+            e.getValue(),
             approvalInfo(accountLoader, accountId, value, permittedVotingRange, tag, date));
       }
     }
   }
 
-  private List<SubmitRecord> submitRecords(ChangeData cd) {
-    return cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
-  }
-
   private Map<String, VotingRangeInfo> getPermittedVotingRanges(
       Map<String, Collection<String>> permittedLabels) {
     Map<String, VotingRangeInfo> permittedVotingRanges =
@@ -573,16 +562,4 @@
     }
     return permittedVotingRanges;
   }
-
-  @AutoValue
-  abstract static class LabelWithStatus {
-    private static LabelWithStatus create(LabelInfo label, SubmitRecord.Label.Status status) {
-      return new AutoValue_LabelsJson_LabelWithStatus(label, status);
-    }
-
-    abstract LabelInfo label();
-
-    @Nullable
-    abstract SubmitRecord.Label.Status status();
-  }
 }
diff --git a/java/com/google/gerrit/server/change/LabelsJsonRules.java b/java/com/google/gerrit/server/change/LabelsJsonRules.java
new file mode 100644
index 0000000..d952c82
--- /dev/null
+++ b/java/com/google/gerrit/server/change/LabelsJsonRules.java
@@ -0,0 +1,595 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Collectors.toList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelTypes;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.VotingRangeInfo;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountLoader;
+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.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.DeleteVoteControl;
+import com.google.gerrit.server.project.RemoveReviewerControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Produces label-related entities, like {@link LabelInfo}s, which is serialized to JSON afterwards.
+ *
+ * <p>This is the implementation that uses SubmitRules that have been deprecated, and should only be
+ * used in gerrit deployment where Prolog rules are still being used.
+ */
+@Singleton
+public class LabelsJsonRules {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final PermissionBackend permissionBackend;
+  private final DeleteVoteControl deleteVoteControl;
+  private final RemoveReviewerControl removeReviewerControl;
+
+  @Inject
+  LabelsJsonRules(
+      PermissionBackend permissionBackend,
+      DeleteVoteControl deleteVoteControl,
+      RemoveReviewerControl removeReviewerControl) {
+    this.permissionBackend = permissionBackend;
+    this.deleteVoteControl = deleteVoteControl;
+    this.removeReviewerControl = removeReviewerControl;
+  }
+
+  /**
+   * Returns all {@link LabelInfo}s for a single change. Uses the provided {@link AccountLoader} to
+   * lazily populate accounts. Callers have to call {@link AccountLoader#fill()} afterwards to
+   * populate all accounts in the returned {@link LabelInfo}s.
+   */
+  @Nullable
+  Map<String, LabelInfo> labelsFor(
+      AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
+      throws PermissionBackendException {
+    if (!standard && !detailed) {
+      return null;
+    }
+
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Get labels", Metadata.builder().changeId(cd.change().getId().get()).build())) {
+      LabelTypes labelTypes = cd.getLabelTypes();
+      Map<String, LabelWithStatus> withStatus =
+          cd.change().isMerged()
+              ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
+              : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
+      return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
+    }
+  }
+
+  /**
+   * Returns A map of all label names and the values that the provided user has permission to vote
+   * on.
+   *
+   * @param filterApprovalsBy a Gerrit user ID.
+   * @param cd {@link ChangeData} corresponding to a specific gerrit change.
+   * @return A Map where the key contain a label name, and the value is a list of the permissible
+   *     vote values that the user can vote on.
+   */
+  Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
+      throws PermissionBackendException {
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Get permitted labels",
+            Metadata.builder().changeId(cd.change().getId().get()).build())) {
+      SetMultimap<String, String> permitted = LinkedHashMultimap.create();
+      boolean isMerged = cd.change().isMerged();
+      Map<String, Short> currentUserVotes = currentLabels(filterApprovalsBy, cd);
+      for (LabelType labelType : cd.getLabelTypes().getLabelTypes()) {
+        if (isMerged && !labelType.isAllowPostSubmit()) {
+          continue;
+        }
+        Set<LabelPermission.WithValue> can =
+            permissionBackend.absentUser(filterApprovalsBy).change(cd).test(labelType);
+        for (LabelPermission.WithValue val : can) {
+          logger.atFine().log(
+              "User %s For label %s can vote %s ", filterApprovalsBy, val.label(), val.value());
+        }
+
+        for (LabelValue v : labelType.getValues()) {
+          boolean ok = can.contains(new LabelPermission.WithValue(labelType, v));
+          if (isMerged) {
+            // Votes cannot be decreased if the change is merged. Only accept the label value if
+            // it's
+            // greater or equal than the user's latest vote.
+            short prev = currentUserVotes.getOrDefault(labelType.getName(), (short) 0);
+            ok &= v.getValue() >= prev;
+          }
+          if (ok) {
+            permitted.put(labelType.getName(), v.formatValue());
+          }
+        }
+      }
+      clearOnlyZerosEntries(permitted);
+      return permitted.asMap();
+    }
+  }
+
+  /**
+   * Returns A map of all labels that the provided user has permission to remove.
+   *
+   * @param accountLoader to load the reviewers' data with.
+   * @param user a Gerrit user.
+   * @param cd {@link ChangeData} corresponding to a specific gerrit change.
+   * @return A Map of {@code labelName} -> {Map of {@code value} -> List of {@link AccountInfo}}
+   *     that the user can remove votes from.
+   */
+  Map<String, Map<String, List<AccountInfo>>> removableLabels(
+      AccountLoader accountLoader, CurrentUser user, ChangeData cd)
+      throws PermissionBackendException {
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Get removable labels",
+            Metadata.builder().changeId(cd.change().getId().get()).build())) {
+      if (cd.change().isMerged()) {
+        return new HashMap<>();
+      }
+
+      Map<String, Map<String, List<AccountInfo>>> res = new HashMap<>();
+      LabelTypes labelTypes = cd.getLabelTypes();
+      for (PatchSetApproval approval : cd.currentApprovals()) {
+        Optional<LabelType> labelType = labelTypes.byLabel(approval.labelId());
+        if (!labelType.isPresent()) {
+          continue;
+        }
+        if (!(deleteVoteControl.testDeleteVotePermissions(user, cd, approval, labelType.get())
+            || removeReviewerControl.testRemoveReviewer(
+                cd, user, approval.accountId(), approval.value()))) {
+          continue;
+        }
+        if (!res.containsKey(approval.label())) {
+          res.put(approval.label(), new HashMap<>());
+        }
+        String labelValue = LabelValue.formatValue(approval.value());
+        if (!res.get(approval.label()).containsKey(labelValue)) {
+          res.get(approval.label()).put(labelValue, new ArrayList<>());
+        }
+        res.get(approval.label()).get(labelValue).add(accountLoader.get(approval.accountId()));
+      }
+      return res;
+    }
+  }
+
+  private static void clearOnlyZerosEntries(SetMultimap<String, String> permitted) {
+    List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
+    for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
+      if (isOnlyZero(e.getValue())) {
+        toClear.add(e.getKey());
+      }
+    }
+    for (String label : toClear) {
+      permitted.removeAll(label);
+    }
+  }
+
+  private static boolean isOnlyZero(Collection<String> values) {
+    return values.isEmpty() || (values.size() == 1 && values.contains(" 0"));
+  }
+
+  private static void addApproval(LabelInfo label, ApprovalInfo approval) {
+    if (label.all == null) {
+      label.all = new ArrayList<>();
+    }
+    label.all.add(approval);
+  }
+
+  private Map<String, LabelWithStatus> labelsForUnsubmittedChange(
+      AccountLoader accountLoader,
+      ChangeData cd,
+      LabelTypes labelTypes,
+      boolean standard,
+      boolean detailed)
+      throws PermissionBackendException {
+    Map<String, LabelWithStatus> labels =
+        initLabels(accountLoader, cd, labelTypes, /* includeAccountInfo= */ standard || detailed);
+    setAllApprovals(accountLoader, cd, labels, detailed);
+
+    for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
+      Optional<LabelType> type = labelTypes.byLabel(e.getKey());
+      if (!type.isPresent()) {
+        continue;
+      }
+      if (standard || detailed) {
+        for (PatchSetApproval psa : cd.currentApprovals()) {
+          if (type.get().matches(psa)) {
+            short val = psa.value();
+            Account.Id accountId = psa.accountId();
+            setLabelScores(accountLoader, type.get(), e.getValue(), val, accountId);
+          }
+        }
+      }
+      setLabelValues(type.get(), e.getValue());
+    }
+    return labels;
+  }
+
+  private Integer parseRangeValue(String value) {
+    if (value.startsWith("+")) {
+      value = value.substring(1);
+    } else if (value.startsWith(" ")) {
+      value = value.trim();
+    }
+    return Ints.tryParse(value);
+  }
+
+  private ApprovalInfo approvalInfo(
+      AccountLoader accountLoader,
+      Account.Id id,
+      @Nullable Integer value,
+      @Nullable VotingRangeInfo permittedVotingRange,
+      @Nullable String tag,
+      @Nullable Instant date) {
+    ApprovalInfo ai = new ApprovalInfo(id.get(), value, permittedVotingRange, tag, date);
+    accountLoader.put(ai);
+    return ai;
+  }
+
+  private void setLabelValues(LabelType type, LabelWithStatus l) {
+    l.label().defaultValue = type.getDefaultValue();
+    l.label().values = new LinkedHashMap<>();
+    for (LabelValue v : type.getValues()) {
+      l.label().values.put(v.formatValue(), v.getText());
+    }
+    if (isOnlyZero(l.label().values.keySet())) {
+      l.label().values = null;
+    }
+  }
+
+  private Map<String, Short> currentLabels(@Nullable Account.Id accountId, ChangeData cd) {
+    Map<String, Short> result = new HashMap<>();
+    for (PatchSetApproval psa : cd.currentApprovals()) {
+      if (accountId == null || psa.accountId().equals(accountId)) {
+        result.put(psa.label(), psa.value());
+      }
+    }
+    return result;
+  }
+
+  private Map<String, LabelWithStatus> labelsForSubmittedChange(
+      AccountLoader accountLoader,
+      ChangeData cd,
+      LabelTypes labelTypes,
+      boolean standard,
+      boolean detailed)
+      throws PermissionBackendException {
+    Set<Account.Id> allUsers = new HashSet<>();
+    if (detailed) {
+      // Users expect to see all reviewers on closed changes, even if they
+      // didn't vote on the latest patch set. If we don't need detailed labels,
+      // we aren't including 0 votes for all users below, so we can just look at
+      // the latest patch set (in the next loop).
+      for (PatchSetApproval psa : cd.approvals().values()) {
+        allUsers.add(psa.accountId());
+      }
+    }
+
+    Set<String> labelNames = new HashSet<>();
+    SetMultimap<Account.Id, PatchSetApproval> current =
+        MultimapBuilder.hashKeys().hashSetValues().build();
+    for (PatchSetApproval a : cd.currentApprovals()) {
+      allUsers.add(a.accountId());
+      Optional<LabelType> type = labelTypes.byLabel(a.labelId());
+      if (type.isPresent()) {
+        labelNames.add(type.get().getName());
+        // Not worth the effort to distinguish between votable/non-votable for 0
+        // values on closed changes, since they can't vote anyway.
+        current.put(a.accountId(), a);
+      }
+    }
+
+    // Since voting on merged changes is allowed all labels which apply to
+    // the change must be returned. All applying labels can be retrieved from
+    // the submit records, which is what initLabels does.
+    // It's not possible to only compute the labels based on the approvals
+    // since merged changes may not have approvals for all labels (e.g. if not
+    // all labels are required for submit or if the change was auto-closed due
+    // to direct push or if new labels were defined after the change was
+    // merged).
+    Map<String, LabelWithStatus> labels;
+    labels = initLabels(accountLoader, cd, labelTypes, standard);
+
+    // Also include all labels for which approvals exists. E.g. there can be
+    // approvals for labels that are ignored by a Prolog submit rule and hence
+    // it wouldn't be included in the submit records.
+    for (String name : labelNames) {
+      if (!labels.containsKey(name)) {
+        labels.put(name, LabelWithStatus.create(new LabelInfo(), null));
+      }
+    }
+
+    labels.entrySet().stream()
+        .filter(e -> labelTypes.byLabel(e.getKey()).isPresent())
+        .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()).get(), e.getValue()));
+
+    for (Account.Id accountId : allUsers) {
+      Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size());
+      Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
+      if (detailed) {
+        pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
+      }
+      for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
+        ApprovalInfo ai = approvalInfo(accountLoader, accountId, 0, null, null, null);
+        byLabel.put(entry.getKey(), ai);
+        addApproval(entry.getValue().label(), ai);
+      }
+      for (PatchSetApproval psa : current.get(accountId)) {
+        Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
+        if (!type.isPresent()) {
+          continue;
+        }
+
+        short val = psa.value();
+        ApprovalInfo info = byLabel.get(type.get().getName());
+        if (info != null) {
+          info.value = Integer.valueOf(val);
+          info.permittedVotingRange = pvr.getOrDefault(type.get().getName(), null);
+          info.setDate(psa.granted());
+          info.tag = psa.tag().orElse(null);
+          if (psa.postSubmit()) {
+            info.postSubmit = true;
+          }
+        }
+        if (!standard) {
+          continue;
+        }
+
+        setLabelScores(accountLoader, type.get(), labels.get(type.get().getName()), val, accountId);
+      }
+    }
+    return labels;
+  }
+
+  private Map<String, LabelWithStatus> initLabels(
+      AccountLoader accountLoader,
+      ChangeData cd,
+      LabelTypes labelTypes,
+      boolean includeAccountInfo) {
+    Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
+    for (SubmitRecord rec : submitRecords(cd)) {
+      if (rec.labels == null) {
+        continue;
+      }
+      for (SubmitRecord.Label r : rec.labels) {
+        LabelWithStatus p = labels.get(r.label);
+        if (p == null || p.status().compareTo(r.status) < 0) {
+          LabelInfo n = new LabelInfo();
+          if (includeAccountInfo) {
+            switch (r.status) {
+              case OK:
+                n.approved = accountLoader.get(r.appliedBy);
+                break;
+              case REJECT:
+                n.rejected = accountLoader.get(r.appliedBy);
+                n.blocking = true;
+                break;
+              case IMPOSSIBLE:
+              case MAY:
+              case NEED:
+              default:
+                break;
+            }
+          }
+
+          n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null;
+          labels.put(r.label, LabelWithStatus.create(n, r.status));
+        }
+      }
+    }
+    setLabelsDescription(labels, labelTypes);
+    return labels;
+  }
+
+  private void setLabelsDescription(Map<String, LabelWithStatus> labels, LabelTypes labelTypes) {
+    for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
+      String labelName = entry.getKey();
+      Optional<LabelType> type = labelTypes.byLabel(labelName);
+      if (!type.isPresent()) {
+        continue;
+      }
+      LabelWithStatus labelWithStatus = entry.getValue();
+      labelWithStatus.label().description = type.get().getDescription().orElse(null);
+    }
+  }
+
+  private void setLabelScores(
+      AccountLoader accountLoader,
+      LabelType type,
+      LabelWithStatus l,
+      short score,
+      Account.Id accountId) {
+    if (l.label().approved != null || l.label().rejected != null) {
+      return;
+    }
+
+    if (type.getMin() == null || type.getMax() == null) {
+      // Can't set score for unknown or misconfigured type.
+      return;
+    }
+
+    if (score != 0) {
+      if (score == type.getMin().getValue()) {
+        l.label().rejected = accountLoader.get(accountId);
+      } else if (score == type.getMax().getValue()) {
+        l.label().approved = accountLoader.get(accountId);
+      } else if (score < 0) {
+        l.label().disliked = accountLoader.get(accountId);
+        l.label().value = score;
+      } else if (score > 0 && l.label().disliked == null) {
+        l.label().recommended = accountLoader.get(accountId);
+        l.label().value = score;
+      }
+    }
+  }
+
+  private void setAllApprovals(
+      AccountLoader accountLoader,
+      ChangeData cd,
+      Map<String, LabelWithStatus> labels,
+      boolean detailed)
+      throws PermissionBackendException {
+    checkState(
+        !cd.change().isMerged(),
+        "should not call setAllApprovals on %s change",
+        ChangeUtil.status(cd.change()));
+
+    // Include a user in the output for this label if either:
+    //  - They are an explicit reviewer.
+    //  - They ever voted on this change.
+    Set<Account.Id> allUsers = new HashSet<>();
+    allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
+    for (PatchSetApproval psa : cd.approvals().values()) {
+      allUsers.add(psa.accountId());
+    }
+
+    Table<Account.Id, String, PatchSetApproval> current =
+        HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
+    for (PatchSetApproval psa : cd.currentApprovals()) {
+      current.put(psa.accountId(), psa.label(), psa);
+    }
+
+    LabelTypes labelTypes = cd.getLabelTypes();
+    for (Account.Id accountId : allUsers) {
+      Map<String, VotingRangeInfo> pvr = null;
+      PermissionBackend.ForChange perm = null;
+      if (detailed) {
+        perm = permissionBackend.absentUser(accountId).change(cd);
+        pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
+      }
+      for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
+        Optional<LabelType> lt = labelTypes.byLabel(e.getKey());
+        if (!lt.isPresent()) {
+          // Ignore submit record for undefined label; likely the submit rule
+          // author didn't intend for the label to show up in the table.
+          continue;
+        }
+        Integer value;
+        VotingRangeInfo permittedVotingRange =
+            pvr == null ? null : pvr.getOrDefault(lt.get().getName(), null);
+        String tag = null;
+        Instant date = null;
+        PatchSetApproval psa = current.get(accountId, lt.get().getName());
+        if (psa != null) {
+          value = Integer.valueOf(psa.value());
+          if (value == 0) {
+            // This may be a dummy approval that was inserted when the reviewer
+            // was added. Explicitly check whether the user can vote on this
+            // label.
+            value = perm != null && perm.test(new LabelPermission(lt.get())) ? 0 : null;
+          }
+          tag = psa.tag().orElse(null);
+          date = psa.granted();
+          if (psa.postSubmit()) {
+            logger.atWarning().log("unexpected post-submit approval on open change: %s", psa);
+          }
+        } else {
+          // Either the user cannot vote on this label, or they were added as a
+          // reviewer but have not responded yet. Explicitly check whether the
+          // user can vote on this label.
+          value = perm != null && perm.test(new LabelPermission(lt.get())) ? 0 : null;
+        }
+        addApproval(
+            e.getValue().label(),
+            approvalInfo(accountLoader, accountId, value, permittedVotingRange, tag, date));
+      }
+    }
+  }
+
+  private List<SubmitRecord> submitRecords(ChangeData cd) {
+    return cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
+  }
+
+  private Map<String, VotingRangeInfo> getPermittedVotingRanges(
+      Map<String, Collection<String>> permittedLabels) {
+    Map<String, VotingRangeInfo> permittedVotingRanges =
+        Maps.newHashMapWithExpectedSize(permittedLabels.size());
+    for (String label : permittedLabels.keySet()) {
+      List<Integer> permittedVotingRange =
+          permittedLabels.get(label).stream()
+              .map(this::parseRangeValue)
+              .filter(java.util.Objects::nonNull)
+              .sorted()
+              .collect(toList());
+
+      if (permittedVotingRange.isEmpty()) {
+        permittedVotingRanges.put(label, null);
+      } else {
+        int minPermittedValue = permittedVotingRange.get(0);
+        int maxPermittedValue = Iterables.getLast(permittedVotingRange);
+        permittedVotingRanges.put(label, new VotingRangeInfo(minPermittedValue, maxPermittedValue));
+      }
+    }
+    return permittedVotingRanges;
+  }
+
+  @AutoValue
+  abstract static class LabelWithStatus {
+    private static LabelWithStatus create(LabelInfo label, SubmitRecord.Label.Status status) {
+      return new AutoValue_LabelsJsonRules_LabelWithStatus(label, status);
+    }
+
+    abstract LabelInfo label();
+
+    @Nullable
+    abstract SubmitRecord.Label.Status status();
+  }
+}
diff --git a/java/com/google/gerrit/server/change/ParentDataProvider.java b/java/com/google/gerrit/server/change/ParentDataProvider.java
index c795d80..16b9f71 100644
--- a/java/com/google/gerrit/server/change/ParentDataProvider.java
+++ b/java/com/google/gerrit/server/change/ParentDataProvider.java
@@ -50,19 +50,18 @@
    */
   public ParentCommitData get(
       Project.NameKey project, Repository repo, ObjectId parentCommitId, String targetBranch) {
-    boolean inTargetBranch = isMergedInTargetBranch(project, repo, parentCommitId, targetBranch);
     Optional<ParentCommitData> fromGerritChange =
         getFromGerritChange(project, parentCommitId, targetBranch);
-    if (fromGerritChange.isEmpty()) {
-      return ParentCommitData.builder()
-          .branchName(Optional.of(targetBranch))
-          .commitId(Optional.of(parentCommitId))
-          .isMergedInTargetBranch(inTargetBranch)
-          .autoBuild();
+    if (fromGerritChange.isPresent()) {
+      return fromGerritChange.get();
     }
-    return fromGerritChange
-        .map(f -> f.toBuilder().isMergedInTargetBranch(inTargetBranch).autoBuild())
-        .get();
+
+    boolean inTargetBranch = isMergedInTargetBranch(project, repo, parentCommitId, targetBranch);
+    return ParentCommitData.builder()
+        .branchName(Optional.of(targetBranch))
+        .commitId(Optional.of(parentCommitId))
+        .isMergedInTargetBranch(inTargetBranch)
+        .autoBuild();
   }
 
   /** Returns true if the parent commit {@code parentCommitId} is merged in the target branch. */
@@ -113,6 +112,9 @@
             .changeNumber(Optional.of(singleData.getId().get()))
             .patchSetNumber(Optional.of(patchSetNumber))
             .changeStatus(Optional.of(singleData.change().getStatus()))
+            .isMergedInTargetBranch(
+                patchSetNumber == singleData.change().currentPatchSetId().get()
+                    && singleData.change().isMerged())
             .autoBuild());
   }
 }
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 28337e2..4716a8c 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.MergeUtil.MergeBase;
 import com.google.gerrit.server.git.MergeUtilFactory;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.DiffNotAvailableException;
@@ -414,7 +415,7 @@
                 "Patch Set %d: Patch Set %d was rebased",
                 rebasePatchSetId.get(), originalPatchSetId.get()));
 
-    if (user.isImpersonating()) {
+    if (user.isImpersonated()) {
       stringBuilder.append(
           String.format(
               " on behalf of %s", AccountTemplateUtil.getAccountTemplate(user.getAccountId())));
@@ -579,6 +580,8 @@
               ctx.getRevWalk(),
               ctx.getInserter(),
               dc,
+              "BASE",
+              MergeBase.create(parentCommit),
               "PATCH SET",
               original,
               "BASE",
@@ -620,7 +623,7 @@
     }
     ObjectId objectId = ctx.getInserter().insert(cb);
     CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
-    commit.setConflicts(original, base, filesWithGitConflicts);
+    commit.setConflicts(parentCommit, original, base, strategy, filesWithGitConflicts);
     logger.atFine().log("rebased commit=%s", commit.name());
     return commit;
   }
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 9046cca..316d884 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -160,7 +161,9 @@
 
     CurrentUser caller = rsrc.getUser();
     Account.Id uploaderId = rsrc.getPatchSet().uploader();
-    IdentifiedUser uploader = userFactory.runAs(/* remotePeer= */ null, uploaderId, caller);
+    IdentifiedUser uploader =
+        userFactory.runAs(
+            /* remotePeer= */ null, uploaderId, caller, ImpersonationPermissionMode.THIS_USER);
     logger.atFine().log(
         "%s is rebasing patch set %s of project %s on behalf of uploader %s",
         caller.getLoggableName(),
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index 030c2e9..6cc2f87 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -326,7 +326,7 @@
     return fail(
         input,
         FailureType.OTHER,
-        MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, input.reviewer));
+        MessageFormat.format(ChangeMessages.reviewerCantSeeChange, input.reviewer));
   }
 
   @Nullable
@@ -352,7 +352,7 @@
         return fail(
             input,
             FailureType.NOT_FOUND,
-            MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
+            MessageFormat.format(ChangeMessages.reviewerNotFoundUserOrGroup, input.reviewer));
       }
       return null;
     }
@@ -361,14 +361,14 @@
       return fail(
           input,
           FailureType.OTHER,
-          MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName()));
+          MessageFormat.format(ChangeMessages.groupIsNotAllowed, group.getName()));
     }
 
     if (input.state().equals(REMOVED)) {
       return fail(
           input,
           FailureType.OTHER,
-          MessageFormat.format(ChangeMessages.get().groupRemovalIsNotAllowed, group.getName()));
+          MessageFormat.format(ChangeMessages.groupRemovalIsNotAllowed, group.getName()));
     }
 
     Set<Account> reviewers = new HashSet<>();
@@ -388,7 +388,7 @@
       return fail(
           input,
           FailureType.OTHER,
-          MessageFormat.format(ChangeMessages.get().groupHasTooManyMembers, group.getName()));
+          MessageFormat.format(ChangeMessages.groupHasTooManyMembers, group.getName()));
     }
 
     // if maxWithoutCheck is set to 0, we never ask for confirmation
@@ -403,7 +403,7 @@
           FailureType.OTHER,
           true,
           MessageFormat.format(
-              ChangeMessages.get().groupManyMembersConfirmation, group.getName(), members.size()));
+              ChangeMessages.groupManyMembersConfirmation, group.getName(), members.size()));
     }
 
     for (Account member : members) {
@@ -425,7 +425,7 @@
       return fail(
           input,
           FailureType.OTHER,
-          MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, input.reviewer));
+          MessageFormat.format(ChangeMessages.reviewerCantSeeChange, input.reviewer));
     }
 
     Address adr = Address.tryParse(input.reviewer);
@@ -433,7 +433,7 @@
       return fail(
           input,
           FailureType.NOT_FOUND,
-          MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
+          MessageFormat.format(ChangeMessages.reviewerInvalid, input.reviewer));
     }
     return new ReviewerModification(input, notes, user, null, ImmutableList.of(adr), true, false);
   }
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index d6cbeda..7f6e35c 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -370,8 +370,11 @@
                 conflicts -> {
                   ConflictsInfo conflictsInfo = new ConflictsInfo();
                   conflictsInfo.containsConflicts = conflicts.containsConflicts();
+                  conflictsInfo.base = conflicts.base().map(ObjectId::getName).orElse(null);
                   conflictsInfo.ours = conflicts.ours().map(ObjectId::getName).orElse(null);
                   conflictsInfo.theirs = conflicts.theirs().map(ObjectId::getName).orElse(null);
+                  conflictsInfo.mergeStrategy = conflicts.mergeStrategy().orElse(null);
+                  conflictsInfo.noBaseReason = conflicts.noBaseReason().orElse(null);
                   return conflictsInfo;
                 })
             .orElse(null);
diff --git a/java/com/google/gerrit/server/change/RobotCommentResource.java b/java/com/google/gerrit/server/change/RobotCommentResource.java
index 3662575..1237619 100644
--- a/java/com/google/gerrit/server/change/RobotCommentResource.java
+++ b/java/com/google/gerrit/server/change/RobotCommentResource.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
@@ -25,27 +22,5 @@
   public static final TypeLiteral<RestView<RobotCommentResource>> ROBOT_COMMENT_KIND =
       new TypeLiteral<>() {};
 
-  private final RevisionResource rev;
-  private final RobotComment comment;
-
-  public RobotCommentResource(RevisionResource rev, RobotComment c) {
-    this.rev = rev;
-    this.comment = c;
-  }
-
-  public PatchSet getPatchSet() {
-    return rev.getPatchSet();
-  }
-
-  public RobotComment getComment() {
-    return comment;
-  }
-
-  public String getId() {
-    return comment.key.uuid;
-  }
-
-  public Account.Id getAuthorId() {
-    return comment.author.getId();
-  }
+  public RobotCommentResource() {}
 }
diff --git a/java/com/google/gerrit/server/comment/CommentContextLoader.java b/java/com/google/gerrit/server/comment/CommentContextLoader.java
index 9cabc9c..1c60b5a 100644
--- a/java/com/google/gerrit/server/comment/CommentContextLoader.java
+++ b/java/com/google/gerrit/server/comment/CommentContextLoader.java
@@ -224,7 +224,7 @@
   private static Optional<Range> getStartAndEndLines(ContextInput comment) {
     if (comment.range() != null) {
       if (comment.range().endLine < comment.range().startLine) {
-        // Seems like comments, created in reply to robot comments sometimes have invalid ranges
+        // Seems like comments sometimes have invalid ranges.
         // Fix here, otherwise the range is invalid and we throw an error later on.
         return Optional.of(Range.create(comment.range().startLine, comment.range().startLine + 1));
       }
diff --git a/java/com/google/gerrit/server/config/AllProjectsName.java b/java/com/google/gerrit/server/config/AllProjectsName.java
index 3a13a58..cc9bb7a 100644
--- a/java/com/google/gerrit/server/config/AllProjectsName.java
+++ b/java/com/google/gerrit/server/config/AllProjectsName.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.errorprone.annotations.Immutable;
-import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 
 /**
  * Special name of the project that all projects derive from.
@@ -23,10 +23,21 @@
  * <p>This class is immutable and thread safe.
  */
 @Immutable
-public class AllProjectsName extends Project.NameKey {
+public record AllProjectsName(String name) implements NameKey {
   private static final long serialVersionUID = 1L;
 
-  public AllProjectsName(String name) {
-    super(name);
+  @Override
+  public int hashCode() {
+    return projectNameHashCode();
+  }
+
+  @Override
+  public boolean equals(Object b) {
+    return projectNameEquals(b);
+  }
+
+  @Override
+  public String toString() {
+    return projectNameToString();
   }
 }
diff --git a/java/com/google/gerrit/server/config/AllUsersName.java b/java/com/google/gerrit/server/config/AllUsersName.java
index 393fb6b..6a6df60 100644
--- a/java/com/google/gerrit/server/config/AllUsersName.java
+++ b/java/com/google/gerrit/server/config/AllUsersName.java
@@ -23,10 +23,21 @@
  * <p>This class is immutable and thread safe.
  */
 @Immutable
-public class AllUsersName extends Project.NameKey {
+public record AllUsersName(String name) implements Project.NameKey {
   private static final long serialVersionUID = 1L;
 
-  public AllUsersName(String name) {
-    super(name);
+  @Override
+  public int hashCode() {
+    return projectNameHashCode();
+  }
+
+  @Override
+  public boolean equals(Object b) {
+    return projectNameEquals(b);
+  }
+
+  @Override
+  public String toString() {
+    return projectNameToString();
   }
 }
diff --git a/java/com/google/gerrit/server/config/AuthConfig.java b/java/com/google/gerrit/server/config/AuthConfig.java
index 996ec71..f43a8b8 100644
--- a/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/java/com/google/gerrit/server/config/AuthConfig.java
@@ -27,11 +27,13 @@
 import com.google.gerrit.server.mail.XsrfException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
@@ -64,7 +66,6 @@
   private final List<OpenIdProviderPattern> allowedOpenIDs;
   private final String cookiePath;
   private final String cookieDomain;
-  private final boolean cookieSecure;
   private final boolean cookieHttpOnly;
   private final SignedToken emailReg;
   private final boolean allowRegisterNewEmail;
@@ -72,6 +73,9 @@
   private final boolean userNameCaseInsensitiveMigrationMode;
   private final int externalIdsRefExpirySecs;
   private GitBasicAuthPolicy gitBasicAuthPolicy;
+  private final Duration maxAuthTokenLifetime;
+  private final int maxAuthTokensPerAccount;
+  private final boolean httpPasswordFallbackEnabled;
 
   @Inject
   AuthConfig(@GerritServerConfig Config cfg) throws XsrfException {
@@ -97,7 +101,6 @@
     allowedOpenIDs = toPatterns(cfg, "allowedOpenID");
     cookiePath = cfg.getString("auth", null, "cookiepath");
     cookieDomain = cfg.getString("auth", null, "cookiedomain");
-    cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
     cookieHttpOnly = cfg.getBoolean("auth", "cookiehttponly", true);
     trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
     enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
@@ -111,6 +114,7 @@
     externalIdsRefExpirySecs =
         (int)
             ConfigUtil.getTimeUnit(cfg, "auth", null, "externalIdsRefExpiry", 0, TimeUnit.SECONDS);
+    httpPasswordFallbackEnabled = cfg.getBoolean("auth", "httpPasswordFallbackEnabled", true);
 
     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP
         && authType != AuthType.LDAP
@@ -137,6 +141,11 @@
     } else {
       emailReg = null;
     }
+
+    maxAuthTokenLifetime =
+        Duration.ofMinutes(
+            ConfigUtil.getTimeUnit(cfg, "auth", null, "maxAuthTokenLifetime", 0, TimeUnit.MINUTES));
+    maxAuthTokensPerAccount = cfg.getInt("auth", "maxAuthTokensPerAccount", 10);
   }
 
   private static List<OpenIdProviderPattern> toPatterns(Config cfg, String name) {
@@ -225,10 +234,6 @@
     return cookieDomain;
   }
 
-  public boolean getCookieSecure() {
-    return cookieSecure;
-  }
-
   public boolean getCookieHttpOnly() {
     return cookieHttpOnly;
   }
@@ -370,6 +375,21 @@
     return allowRegisterNewEmail;
   }
 
+  public Optional<Duration> getMaxAuthTokenLifetime() {
+    if (maxAuthTokenLifetime.isZero() || maxAuthTokenLifetime.isNegative()) {
+      return Optional.empty();
+    }
+    return Optional.of(maxAuthTokenLifetime);
+  }
+
+  public int getMaxAuthTokensPerAccount() {
+    return maxAuthTokensPerAccount;
+  }
+
+  public boolean isHttpPasswordFallbackEnabled() {
+    return httpPasswordFallbackEnabled;
+  }
+
   public Set<String> getTrustedProxyNetworks() {
     return trustedProxyNetworks;
   }
diff --git a/java/com/google/gerrit/server/config/CachedPreferences.java b/java/com/google/gerrit/server/config/CachedPreferences.java
index 62b397a..b113a69 100644
--- a/java/com/google/gerrit/server/config/CachedPreferences.java
+++ b/java/com/google/gerrit/server/config/CachedPreferences.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -37,13 +36,10 @@
  * <p>The config string wrapped by this class might represent different structures. See {@link
  * CachedPreferencesProto} for more details.
  */
-@AutoValue
-public abstract class CachedPreferences {
+public record CachedPreferences(CachedPreferencesProto config) {
   public static final CachedPreferences EMPTY =
       fromCachedPreferencesProto(CachedPreferencesProto.getDefaultInstance());
 
-  protected abstract CachedPreferencesProto config();
-
   public Optional<CachedPreferencesProto> nonEmptyConfig() {
     return config().equals(EMPTY.config()) ? Optional.empty() : Optional.of(config());
   }
@@ -64,7 +60,7 @@
   public static CachedPreferences fromCachedPreferencesProto(
       @Nullable CachedPreferencesProto proto) {
     if (proto != null) {
-      return new AutoValue_CachedPreferences(proto);
+      return new CachedPreferences(proto);
     }
     return EMPTY;
   }
diff --git a/java/com/google/gerrit/server/config/CapabilityConstants.java b/java/com/google/gerrit/server/config/CapabilityConstants.java
index 43c4933..9448725 100644
--- a/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -14,37 +14,30 @@
 
 package com.google.gerrit.server.config;
 
-import org.eclipse.jgit.nls.NLS;
-import org.eclipse.jgit.nls.TranslationBundle;
-
-public class CapabilityConstants extends TranslationBundle {
-  public static CapabilityConstants get() {
-    return NLS.getBundleFor(CapabilityConstants.class);
-  }
-
-  public String accessDatabase;
-  public String administrateServer;
-  public String batchChangesLimit;
-  public String createAccount;
-  public String createGroup;
-  public String deleteGroup;
-  public String createProject;
-  public String emailReviewers;
-  public String flushCaches;
-  public String killTask;
-  public String maintainServer;
-  public String modifyAccount;
-  public String priority;
-  public String readAs;
-  public String queryLimit;
-  public String runAs;
-  public String runGC;
-  public String streamEvents;
-  public String viewAccess;
-  public String viewAllAccounts;
-  public String viewCaches;
-  public String viewConnections;
-  public String viewPlugins;
-  public String viewQueue;
-  public String viewSecondaryEmails;
+public class CapabilityConstants {
+  public static String accessDatabase = "Access Database";
+  public static String administrateServer = "Administrate Server";
+  public static String batchChangesLimit = "Batch Changes Limit";
+  public static String createAccount = "Create Account";
+  public static String createGroup = "Create Group";
+  public static String deleteGroup = "Delete Group";
+  public static String createProject = "Create Project";
+  public static String emailReviewers = "Email Reviewers";
+  public static String flushCaches = "Flush Caches";
+  public static String killTask = "Kill Task";
+  public static String maintainServer = "Maintain Server";
+  public static String modifyAccount = "Modify Account";
+  public static String priority = "Priority";
+  public static String readAs = "Read As";
+  public static String queryLimit = "Query Limit";
+  public static String runAs = "Run As";
+  public static String runGC = "Run Garbage Collection";
+  public static String streamEvents = "Stream Events";
+  public static String viewAccess = "View Access";
+  public static String viewAllAccounts = "View All Accounts";
+  public static String viewCaches = "View Caches";
+  public static String viewConnections = "View Connections";
+  public static String viewPlugins = "View Plugins";
+  public static String viewQueue = "View Queue";
+  public static String viewSecondaryEmails = "View Secondary Emails";
 }
diff --git a/java/com/google/gerrit/server/config/ConfigUtil.java b/java/com/google/gerrit/server/config/ConfigUtil.java
index 121c62e..c0bfa8b 100644
--- a/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -177,21 +177,8 @@
       final long defaultValue,
       final TimeUnit wantUnit) {
     final String valueString = config.getString(section, subsection, setting);
-    if (valueString == null) {
-      return defaultValue;
-    }
-
-    String s = valueString.trim();
-    if (s.length() == 0) {
-      return defaultValue;
-    }
-
-    if (s.startsWith("-") /* negative */) {
-      throw notTimeUnit(section, subsection, setting, valueString);
-    }
-
     try {
-      return getTimeUnit(s, defaultValue, wantUnit);
+      return getTimeUnit(valueString, defaultValue, wantUnit);
     } catch (IllegalArgumentException notTime) {
       throw notTimeUnit(section, subsection, setting, valueString, notTime);
     }
@@ -207,11 +194,24 @@
    * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
    */
   public static long getTimeUnit(String valueString, long defaultValue, TimeUnit wantUnit) {
-    Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
-    if (!m.matches()) {
+    if (valueString == null) {
       return defaultValue;
     }
 
+    String s = valueString.trim();
+    if (s.isEmpty()) {
+      return defaultValue;
+    }
+
+    if (s.startsWith("-") /* negative */) {
+      throw notTimeUnit(valueString);
+    }
+
+    Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(s);
+    if (!m.matches()) {
+      throw notTimeUnit(valueString);
+    }
+
     String digits = m.group(1);
     String unitName = m.group(2).trim();
 
@@ -528,17 +528,6 @@
         why);
   }
 
-  private static IllegalArgumentException notTimeUnit(
-      String section, String subsection, String setting, String valueString) {
-    return notTimeUnit(
-        section
-            + (subsection != null ? "." + subsection : "")
-            + "."
-            + setting
-            + " = "
-            + valueString);
-  }
-
   private static IllegalArgumentException notTimeUnit(String val) {
     return new IllegalArgumentException("Invalid time unit value: " + val);
   }
diff --git a/java/com/google/gerrit/server/config/DefaultBooleanProjectConfig.java b/java/com/google/gerrit/server/config/DefaultBooleanProjectConfig.java
new file mode 100644
index 0000000..814b887
--- /dev/null
+++ b/java/com/google/gerrit/server/config/DefaultBooleanProjectConfig.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Enums;
+import com.google.common.base.Splitter;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+/** Default values for {@link com.google.gerrit.entities.BooleanProjectConfig}'s. */
+@AutoValue
+public abstract class DefaultBooleanProjectConfig {
+  public enum Value {
+    /**
+     * The boolean config is {@code false} by default. Projects may override the default value in
+     * their {@code project.config}.
+     */
+    FALSE,
+    /**
+     * The boolean config is {@code true} by default. Projects may override the default value in
+     * their {@code project.config}.
+     */
+    TRUE,
+    /**
+     * The boolean config is {@code true} for all projects. Projects cannot override the default
+     * value in their {@code project.config} (existing configurations for this parameter on project
+     * level are ignored).
+     */
+    FORCED;
+
+    static Value fromBooleanProjectConfig(BooleanProjectConfig config) {
+      return config.getDefaultValue()
+          ? DefaultBooleanProjectConfig.Value.TRUE
+          : DefaultBooleanProjectConfig.Value.FALSE;
+    }
+  }
+
+  /** Name of the section in {@code project.config} that defines the boolean project config. */
+  public abstract String section();
+
+  /**
+   * Name of the subsection in {@code project.config} that defines the boolean project config. May
+   * be unset if the boolean project config doesn't have a subsection.
+   */
+  public abstract Optional<String> subSection();
+
+  /** Name of the boolean project config in {@code project.config}. */
+  public abstract String name();
+
+  /** The default value for the boolean project config. */
+  public abstract Value defaultValue();
+
+  /**
+   * Tries to parse a string representation of a default boolean project config in the format {code
+   * <section>.<name>=<defaultValue>} or {code <section>.<subSection>.<name>=<defaultValue>}.
+   *
+   * @param s the string representation of the default boolean project config that should be parsed
+   * @return the parsed default boolean project config, or {@code Optional#empty()} if parsing the
+   *     given string was not successful
+   */
+  public static Optional<DefaultBooleanProjectConfig> tryParse(String s) {
+    if (s == null) {
+      return Optional.empty();
+    }
+
+    // Split '<key>=<defaultValue>'.
+    List<String> keyValueList = Splitter.on('=').splitToList(s);
+    if (keyValueList.size() != 2) {
+      return Optional.empty();
+    }
+    String key = keyValueList.get(0);
+    String defaultValue = keyValueList.get(1);
+
+    // Split '<section>.<name>' or '<section>.<subSection>.<name>'.
+    List<String> keyList = Splitter.on('.').splitToList(key);
+    if (keyList.size() != 2 && keyList.size() != 3) {
+      return Optional.empty();
+    }
+    String section = keyList.get(0);
+    String subSection = keyList.size() == 3 ? keyList.get(1) : null;
+    String name = keyList.size() == 3 ? keyList.get(2) : keyList.get(1);
+
+    // Check that none of the parsed parts is empty.
+    if (section.length() == 0
+        || (subSection != null && subSection.length() == 0)
+        || name.length() == 0) {
+      return Optional.empty();
+    }
+
+    // Parse the default value.
+    com.google.common.base.Optional<Value> defaultValueAsEnum =
+        Enums.getIfPresent(Value.class, defaultValue.toUpperCase(Locale.US));
+    if (!defaultValueAsEnum.isPresent()) {
+      return Optional.empty();
+    }
+
+    return Optional.of(create(section, subSection, name, defaultValueAsEnum.get()));
+  }
+
+  private static DefaultBooleanProjectConfig create(
+      String section, @Nullable String subSection, String name, Value defaultValue) {
+    return new AutoValue_DefaultBooleanProjectConfig(
+        section, Optional.ofNullable(subSection), name, defaultValue);
+  }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 3d480d9..39dfba2 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Ticker;
 import com.google.common.cache.Cache;
+import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.ActionVisitor;
@@ -109,6 +110,7 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.ServiceUserClassifierImpl;
+import com.google.gerrit.server.account.VersionedAuthTokens;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalIdModule;
 import com.google.gerrit.server.approval.ApprovalsUtil;
@@ -136,6 +138,7 @@
 import com.google.gerrit.server.extensions.events.AttentionSetObserver;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.flow.FlowService;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergedByPushOp;
@@ -145,6 +148,7 @@
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.receive.PushOptionsValidator;
 import com.google.gerrit.server.git.receive.ReceiveCommitsModule;
 import com.google.gerrit.server.git.validators.CommentCountValidator;
 import com.google.gerrit.server.git.validators.CommentCumulativeSizeValidator;
@@ -440,6 +444,7 @@
     DynamicSet.setOf(binder(), ExternalIncludedIn.class);
     DynamicSet.setOf(binder(), FilterIncludedIn.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
+    DynamicSet.setOf(binder(), PushOptionsValidator.class);
     DynamicSet.setOf(binder(), PluginPushOption.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
     DynamicSet.setOf(binder(), ResolveConflictsWebLink.class);
@@ -459,6 +464,7 @@
     DynamicSet.setOf(binder(), ActionVisitor.class);
     DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
     DynamicItem.itemOf(binder(), LockManager.class);
+    DynamicSet.setOf(binder(), LabelType.class);
     DynamicSet.setOf(binder(), SubmitRule.class);
     DynamicSet.setOf(binder(), SubmitRequirement.class);
     DynamicSet.setOf(binder(), QuotaEnforcer.class);
@@ -477,6 +483,7 @@
     DynamicSet.setOf(binder(), ValidationOptionsListener.class);
     DynamicSet.setOf(binder(), RetryListener.class);
     DynamicSet.bind(binder(), RetryListener.class).to(LoggingRetryListener.class);
+    DynamicItem.itemOf(binder(), FlowService.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
@@ -518,6 +525,7 @@
     factory(MergedByPushOp.Factory.class);
     factory(GitModules.Factory.class);
     factory(VersionedAuthorizedKeys.Factory.class);
+    factory(VersionedAuthTokens.Factory.class);
     factory(StoreSubmitRequirementsOp.Factory.class);
     factory(FileEditsPredicate.Factory.class);
 
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/server/config/GlobalLabelResource.java
similarity index 62%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/server/config/GlobalLabelResource.java
index 3ec74ba..82546b4 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/server/config/GlobalLabelResource.java
@@ -12,9 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.server.config;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+public class GlobalLabelResource implements RestResource {
+  public static final TypeLiteral<RestView<GlobalLabelResource>> GLOBAL_LABEL_KIND =
+      new TypeLiteral<>() {};
+}
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/server/config/GlobalSubmitRequirementResource.java
similarity index 60%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/server/config/GlobalSubmitRequirementResource.java
index 3ec74ba..9f9e9e8 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/server/config/GlobalSubmitRequirementResource.java
@@ -12,9 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.server.config;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+public class GlobalSubmitRequirementResource implements RestResource {
+  public static final TypeLiteral<RestView<GlobalSubmitRequirementResource>>
+      GLOBAL_SUBMIT_REQUIREMENT_KIND = new TypeLiteral<>() {};
+}
diff --git a/java/com/google/gerrit/server/config/LogConfig.java b/java/com/google/gerrit/server/config/LogConfig.java
new file mode 100644
index 0000000..406176b
--- /dev/null
+++ b/java/com/google/gerrit/server/config/LogConfig.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class LogConfig {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String CONFIG_LOG_SECTION = "log";
+
+  private final boolean jsonLogging;
+  private final boolean textLogging;
+  private final boolean compress;
+  private final boolean rotate;
+  private final Duration timeToKeep;
+
+  @Inject
+  public LogConfig(@GerritServerConfig Config cfg) {
+    jsonLogging = cfg.getBoolean(CONFIG_LOG_SECTION, "jsonLogging", false);
+    textLogging = cfg.getBoolean(CONFIG_LOG_SECTION, "textLogging", true) || !jsonLogging;
+    compress = cfg.getBoolean(CONFIG_LOG_SECTION, "compress", true);
+    rotate = cfg.getBoolean(CONFIG_LOG_SECTION, "rotate", true);
+    timeToKeep = getTimeToKeep(cfg);
+  }
+
+  public boolean isJsonLogging() {
+    return jsonLogging;
+  }
+
+  public boolean isTextLogging() {
+    return textLogging;
+  }
+
+  public boolean shouldCompress() {
+    return compress;
+  }
+
+  public boolean shouldRotate() {
+    return rotate;
+  }
+
+  public Duration getTimeToKeep() {
+    return timeToKeep;
+  }
+
+  private Duration getTimeToKeep(Config config) {
+    try {
+      return Duration.ofDays(
+          ConfigUtil.getTimeUnit(config, "log", null, "timeToKeep", -1, TimeUnit.DAYS));
+    } catch (IllegalArgumentException e) {
+      logger.atWarning().withCause(e).log(
+          "Illegal duration value for log deletion. Disabling log deletion.");
+      return Duration.ofDays(-1L);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/config/PreferencesParserUtil.java b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
index 3e39e25..d7f19b0 100644
--- a/java/com/google/gerrit/server/config/PreferencesParserUtil.java
+++ b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
@@ -231,13 +231,13 @@
       my = new ArrayList<>();
     }
     if (my.isEmpty()) {
-      my.add(new MenuItem("Dashboard", "#/dashboard/self", null));
-      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
-      my.add(new MenuItem("Edits", "#/q/has:edit", null));
-      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
-      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
-      my.add(new MenuItem("All Visible Changes", "#/q/is:visible", null));
-      my.add(new MenuItem("Groups", "#/settings/#Groups", null));
+      my.add(new MenuItem("Dashboard", "/dashboard/self", null));
+      my.add(new MenuItem("Draft Comments", "/q/has:draft", null));
+      my.add(new MenuItem("Edits", "/q/has:edit", null));
+      my.add(new MenuItem("Watched Changes", "/q/is:watched+is:open", null));
+      my.add(new MenuItem("Starred Changes", "/q/is:starred", null));
+      my.add(new MenuItem("All Visible Changes", "/q/is:visible", null));
+      my.add(new MenuItem("Groups", "/settings/#Groups", null));
     }
     return my;
   }
@@ -307,8 +307,9 @@
   private static List<MenuItem> my(Config cfg) {
     List<MenuItem> my = new ArrayList<>();
     for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
-      String url = my(cfg, subsection, KEY_URL, "#/");
-      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
+      String url = my(cfg, subsection, KEY_URL, "/");
+      boolean isRelative = url.startsWith("#") || url.startsWith("/");
+      String target = my(cfg, subsection, KEY_TARGET, isRelative ? null : "_blank");
       my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
     }
     return my;
diff --git a/java/com/google/gerrit/server/config/RepositoryConfig.java b/java/com/google/gerrit/server/config/RepositoryConfig.java
index c008f63..5d26449 100644
--- a/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -18,21 +18,28 @@
 import static java.util.Comparator.comparing;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Locale;
 import java.util.Objects;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
 public class RepositoryConfig {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   static final String SECTION_NAME = "repository";
   static final String OWNER_GROUP_NAME = "ownerGroup";
+  static final String DEFAULT_CONFIG_NAME = "defaultConfig";
   static final String DEFAULT_SUBMIT_TYPE_NAME = "defaultSubmitType";
   static final String BASE_PATH_NAME = "basePath";
 
@@ -45,6 +52,57 @@
     this.cfg = cfg;
   }
 
+  /**
+   * Gets the default value that is configured for the given boolean project config that matches the
+   * given project.
+   *
+   * <p>If no default value is configured, {@link DefaultBooleanProjectConfig.Value#FALSE} is
+   * returned.
+   */
+  public DefaultBooleanProjectConfig.Value getDefault(
+      Project.NameKey project, BooleanProjectConfig booleanProjectConfig) {
+    Optional<DefaultBooleanProjectConfig.Value> configuredDefault =
+        Arrays.stream(
+                cfg.getStringList(SECTION_NAME, findSubSection(project.get()), DEFAULT_CONFIG_NAME))
+            .map(DefaultBooleanProjectConfig::tryParse)
+            .filter(Optional::isPresent)
+            .map(Optional::get)
+            .filter(
+                defaultBooleanProjectConfig ->
+                    defaultBooleanProjectConfig.section().equals(booleanProjectConfig.getSection())
+                        && defaultBooleanProjectConfig
+                            .subSection()
+                            .equals(Optional.ofNullable(booleanProjectConfig.getSubSection()))
+                        && defaultBooleanProjectConfig
+                            .name()
+                            .equals(booleanProjectConfig.getName()))
+            .findFirst()
+            .map(DefaultBooleanProjectConfig::defaultValue);
+
+    if (configuredDefault.isPresent()) {
+      if (configuredDefault.get().equals(DefaultBooleanProjectConfig.Value.FORCED)) {
+        logger.atFine().log(
+            "enforcing configured default %s=%s for project %s (settings for %s on project-level"
+                + " are ignored)",
+            booleanProjectConfig.format(),
+            configuredDefault.get().name().toLowerCase(Locale.US),
+            project,
+            booleanProjectConfig.format());
+      } else {
+        logger.atFine().log(
+            "applying configured default %s=%s for project %s (overrides for %s on project-level do"
+                + " apply)",
+            booleanProjectConfig.format(),
+            configuredDefault.get().name().toLowerCase(Locale.US),
+            project,
+            booleanProjectConfig.format());
+      }
+    }
+
+    return configuredDefault.orElse(
+        DefaultBooleanProjectConfig.Value.fromBooleanProjectConfig(booleanProjectConfig));
+  }
+
   public SubmitType getDefaultSubmitType(Project.NameKey project) {
     return cfg.getEnum(
         SECTION_NAME, findSubSection(project.get()), DEFAULT_SUBMIT_TYPE_NAME, DEFAULT_SUBMIT_TYPE);
diff --git a/java/com/google/gerrit/server/config/ScheduleConfig.java b/java/com/google/gerrit/server/config/ScheduleConfig.java
index f07715b..a942d66 100644
--- a/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -31,13 +31,14 @@
 import java.time.temporal.ChronoUnit;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.Random;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 
 /**
  * This class reads a schedule for running a periodic background job from a Git config.
  *
- * <p>A schedule configuration consists of two parameters:
+ * <p>A schedule configuration consists of the following parameters:
  *
  * <ul>
  *   <li>{@code interval}: Interval for running the periodic background job. The interval must be
@@ -71,10 +72,15 @@
  *         <li>{@code <minutes>}: {@code 00}-{@code 59}
  *       </ul>
  *       The timezone cannot be specified but is always the system default time-zone.
+ *   <li>{@code jitter}: A maximum random delay that will be added to the job's start time. If 0 or
+ *       not specified, no jitter is applied.
+ *       <ul>
+ *         <li>The same suffixes as {@code interval} are supported for defining the time unit.
+ *       </ul>
  * </ul>
  *
- * <p>The section and the subsection from which the {@code interval} and {@code startTime}
- * parameters are read can be configured.
+ * <p>The section and the subsection from which the {@code interval}, {@code startTime} and {@code
+ * jitter} parameters are read can be configured.
  *
  * <p>Examples for a schedule configuration:
  *
@@ -83,12 +89,16 @@
  *       <pre>
  * foo.startTime = Fri 10:30
  * foo.interval  = 2 day
+ * foo.jitter    = 30 min
  * </pre>
  *       Assuming that the server is started on {@code Mon 7:00} then {@code startTime - now} is
  *       {@code 4 days 3:30 hours}. This is larger than the interval hence the start time is
  *       preponed by the maximum integral multiple of the interval so that start time is still in
- *       the future, i.e. preponed by 4 days. This yields a start time of {@code Mon 10:30}, next
- *       executions are {@code Wed 10:30}, {@code Fri 10:30}. etc.
+ *       the future, i.e. preponed by 4 days. This yields a start time of {@code Mon 10:30}. After
+ *       this adjustment, a random delay between 0 and 30 minutes is added. For example, if the
+ *       delay is 7 minutes, the final start time becomes {@code Mon 10:37}. Subsequent executions
+ *       will occur at fixed intervals from this updated start time, such as {@code Wed 10:37},
+ *       {@code Fri 10:37}, and so on.
  *   <li>
  *       <pre>
  * foo.startTime = 06:00
@@ -104,9 +114,11 @@
 
   @VisibleForTesting static final String KEY_INTERVAL = "interval";
   @VisibleForTesting static final String KEY_STARTTIME = "startTime";
+  @VisibleForTesting static final String KEY_JITTER = "jitter";
 
   private static final long MISSING_CONFIG = -1L;
   private static final long INVALID_CONFIG = -2L;
+  public static final long DEFAULT_JITTER = 0L;
 
   public static Optional<Schedule> createSchedule(Config config, String section) {
     return builder(config, section).buildSchedule();
@@ -117,6 +129,7 @@
         .setNow(computeNow())
         .setKeyInterval(KEY_INTERVAL)
         .setKeyStartTime(KEY_STARTTIME)
+        .setKeyJitter(KEY_JITTER)
         .setConfig(config)
         .setSection(section);
   }
@@ -132,28 +145,32 @@
 
   abstract String keyStartTime();
 
+  abstract String keyJitter();
+
   abstract ZonedDateTime now();
 
   @Memoized
   public Optional<Schedule> schedule() {
     long interval = computeInterval(config(), section(), subsection(), keyInterval());
+    long jitter = computeJitter(config(), section(), subsection(), keyJitter());
 
     long initialDelay;
     if (interval > 0) {
       initialDelay =
-          computeInitialDelay(config(), section(), subsection(), keyStartTime(), now(), interval);
+          computeInitialDelay(
+              config(), section(), subsection(), keyStartTime(), now(), interval, jitter);
     } else {
       initialDelay = interval;
     }
 
-    if (isInvalidOrMissing(interval, initialDelay)) {
+    if (isInvalidOrMissing(interval, initialDelay, jitter)) {
       return Optional.empty();
     }
 
     return Optional.of(Schedule.create(interval, initialDelay));
   }
 
-  private boolean isInvalidOrMissing(long interval, long initialDelay) {
+  private boolean isInvalidOrMissing(long interval, long initialDelay, long jitter) {
     String key = section() + (subsection() != null ? "." + subsection() : "");
     if (interval == MISSING_CONFIG && initialDelay == MISSING_CONFIG) {
       logger.atInfo().log("No schedule configuration for \"%s\".", key);
@@ -185,7 +202,7 @@
       initialDelay = INVALID_CONFIG;
     }
 
-    if (interval == INVALID_CONFIG || initialDelay == INVALID_CONFIG) {
+    if (interval == INVALID_CONFIG || initialDelay == INVALID_CONFIG || jitter == INVALID_CONFIG) {
       logger.atSevere().log("Invalid schedule configuration for \"%s\" is ignored. ", key);
       return true;
     }
@@ -234,25 +251,38 @@
     }
   }
 
+  private static long computeJitter(
+      Config rc, String section, String subsection, String keyJitter) {
+    try {
+      return ConfigUtil.getTimeUnit(
+          rc, section, subsection, keyJitter, DEFAULT_JITTER, TimeUnit.MILLISECONDS);
+    } catch (IllegalArgumentException e) {
+      logger.atSevere().log("%s", e.getMessage());
+      return INVALID_CONFIG;
+    }
+  }
+
   private static long computeInitialDelay(
       Config rc,
       String section,
       String subsection,
       String keyStartTime,
       ZonedDateTime now,
-      long interval) {
+      long interval,
+      long jitter) {
     String start = rc.getString(section, subsection, keyStartTime);
     if (start == null) {
       return MISSING_CONFIG;
     }
-    return computeInitialDelay(interval, start, now);
+    return computeInitialDelay(interval, start, now, jitter);
   }
 
   private static long computeInitialDelay(long interval, String start) {
-    return computeInitialDelay(interval, start, computeNow());
+    return computeInitialDelay(interval, start, computeNow(), DEFAULT_JITTER);
   }
 
-  private static long computeInitialDelay(long interval, String start, ZonedDateTime now) {
+  private static long computeInitialDelay(
+      long interval, String start, ZonedDateTime now, long jitter) {
     requireNonNull(start);
 
     try {
@@ -266,7 +296,13 @@
         // Day of week is an optional parameter.
       }
       startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
-      long delay = Duration.between(now, startTime).toMillis() % interval;
+      long delay = Duration.between(now, startTime).toMillis();
+      if (jitter > 0) {
+        Random random = new Random();
+        // Increment by 1 to make the upper bound inclusive of the configured value
+        delay += random.nextLong(jitter + 1);
+      }
+      delay %= interval;
       if (delay <= 0) {
         delay += interval;
       }
@@ -293,6 +329,8 @@
 
     public abstract Builder setKeyStartTime(String keyStartTime);
 
+    public abstract Builder setKeyJitter(String keyJitter);
+
     @VisibleForTesting
     abstract Builder setNow(ZonedDateTime now);
 
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index be55ec1..ad271b6 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -44,6 +44,8 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
 import com.google.gerrit.server.edit.tree.DeleteFileModification;
+import com.google.gerrit.server.edit.tree.InvalidFileModeException;
+import com.google.gerrit.server.edit.tree.InvalidGitLinkException;
 import com.google.gerrit.server.edit.tree.RenameFileModification;
 import com.google.gerrit.server.edit.tree.RestoreFileModification;
 import com.google.gerrit.server.edit.tree.TreeCreator;
@@ -52,6 +54,7 @@
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.MergeUtil.MergeBase;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -598,6 +601,14 @@
       newTreeId = treeCreator.createNewTreeAndGetId(repository);
     } catch (InvalidPathException e) {
       throw new BadRequestException(e.getMessage());
+    } catch (InvalidFileModeException e) {
+      throw new BadRequestException(
+          String.format(
+              "file mode %s is invalid: supported values are 100644 (regular file), 100755"
+                  + " (executable file), 120000 (symlink) and 160000 (gitlink)",
+              Integer.toOctalString(e.getInvalidFileMode())));
+    } catch (InvalidGitLinkException e) {
+      throw new BadRequestException(e.getMessage());
     }
 
     if (ObjectId.isEqual(newTreeId, baseCommit.getTree())) {
@@ -647,8 +658,8 @@
           throw new MergeConflictException(
               String.format(
                   "Rebasing change edit onto another patchset results in merge conflicts.\n\n"
-                      + "%s\n\n"
-                      + "Download the edit patchset and rebase manually to preserve changes.",
+                      + "%s\n"
+                      + "Download the edit patchset and rebase manually to preserve changes.\n",
                   MergeUtil.createConflictMessage(conflicts)));
         }
 
@@ -665,6 +676,8 @@
                 revWalk,
                 objectInserter,
                 dc,
+                "BASE",
+                MergeBase.create(revWalk.parseCommit(basePatchSetCommitId)),
                 "PATCH SET",
                 basePatchSetCommit,
                 "EDIT",
@@ -685,7 +698,8 @@
               new PersonIdent(currentEditCommit.getCommitterIdent(), timestamp));
 
       CodeReviewCommit newEditCommit = revWalk.parseCommit(newEditCommitId);
-      newEditCommit.setConflicts(basePatchSetCommit, editCommitId, filesWithGitConflicts);
+      newEditCommit.setConflicts(
+          basePatchSetCommitId, basePatchSetCommit, editCommitId, "resolve", filesWithGitConflicts);
       return newEditCommit;
     }
   }
diff --git a/java/com/google/gerrit/server/edit/ModificationIntention.java b/java/com/google/gerrit/server/edit/ModificationIntention.java
index 531f682..94dc64b 100644
--- a/java/com/google/gerrit/server/edit/ModificationIntention.java
+++ b/java/com/google/gerrit/server/edit/ModificationIntention.java
@@ -20,12 +20,12 @@
 /**
  * Intended modification target.
  *
- * <p>See also {@link ModificationTarget}. Some modifications may have a fixed target (e.g.
- * suggested fixes of robot comments). For other modifications, the presence of a change edit
- * influences their target. The latter comes from the REST endpoints of change edits which work no
- * matter whether a change edit is present or not. If it's not present, a new change edit is created
- * based on the current patchset. As we don't want to create an "empty" commit for the new change
- * edit first, we need this class/interface for the flexible handling.
+ * <p>See also {@link ModificationTarget}. Some modifications may have a fixed target. For other
+ * modifications, the presence of a change edit influences their target. The latter comes from the
+ * REST endpoints of change edits which work no matter whether a change edit is present or not. If
+ * it's not present, a new change edit is created based on the current patchset. As we don't want to
+ * create an "empty" commit for the new change edit first, we need this class/interface for the
+ * flexible handling.
  */
 interface ModificationIntention {
 
diff --git a/java/com/google/gerrit/server/edit/ModificationTarget.java b/java/com/google/gerrit/server/edit/ModificationTarget.java
index 86de812..c506434 100644
--- a/java/com/google/gerrit/server/edit/ModificationTarget.java
+++ b/java/com/google/gerrit/server/edit/ModificationTarget.java
@@ -27,7 +27,7 @@
  *
  * <p>This is currently used in the context of change edits which involves both direct actions on
  * change edits (e.g. creating a change edit; modifying a file of a change edit) as well as indirect
- * creation/modification of them (e.g. via applying a suggested fix of a robot comment.)
+ * creation/modification of them.
  *
  * <p>Depending on the situation and exact action, either an existing {@link ChangeEdit} (-> {@link
  * EditCommit} or a specific patchset commit (-> {@link PatchsetCommit}) is the target of a
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index af0496a..6fe9206 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.edit.tree;
 
 import static com.google.gerrit.entities.Patch.FileMode.EXECUTABLE_FILE;
+import static com.google.gerrit.entities.Patch.FileMode.GITLINK;
 import static com.google.gerrit.entities.Patch.FileMode.REGULAR_FILE;
+import static com.google.gerrit.entities.Patch.FileMode.SYMLINK;
 import static java.util.Objects.requireNonNull;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -31,6 +33,7 @@
 import java.util.List;
 import org.eclipse.jgit.dircache.DirCacheEditor;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -74,7 +77,7 @@
 
   /** A {@code PathEdit} which changes the contents of a file. */
   private static class ChangeContent extends DirCacheEditor.PathEdit {
-
+    private final String filePath;
     private final RawInput newContent;
     private final Repository repository;
     private final Integer newGitFileMode;
@@ -85,13 +88,17 @@
         Repository repository,
         @Nullable Integer newGitFileMode) {
       super(filePath);
+      this.filePath = filePath;
       this.newContent = newContent;
       this.repository = repository;
       this.newGitFileMode = newGitFileMode;
     }
 
     private boolean isValidGitFileMode(int gitFileMode) {
-      return (gitFileMode == EXECUTABLE_FILE.getMode()) || (gitFileMode == REGULAR_FILE.getMode());
+      return gitFileMode == EXECUTABLE_FILE.getMode()
+          || gitFileMode == REGULAR_FILE.getMode()
+          || gitFileMode == GITLINK.getMode()
+          || gitFileMode == SYMLINK.getMode();
     }
 
     @Override
@@ -99,15 +106,24 @@
       try {
         if (newGitFileMode != null && newGitFileMode != 0) {
           if (!isValidGitFileMode(newGitFileMode)) {
-            throw new IllegalStateException("GitFileMode " + newGitFileMode + " is invalid");
+            throw new InvalidFileModeException(
+                String.format("GitFileMode %s is invalid", newGitFileMode), newGitFileMode);
           }
-          dirCacheEntry.setFileMode(FileMode.fromBits(newGitFileMode));
+
+          FileMode fileMode = FileMode.fromBits(newGitFileMode);
+          dirCacheEntry.setFileMode(fileMode);
         }
         if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
           dirCacheEntry.setLength(0);
           dirCacheEntry.setLastModified(Instant.EPOCH);
-          ObjectId newObjectId = ObjectId.fromString(getNewContentBytes(), 0);
-          dirCacheEntry.setObjectId(newObjectId);
+
+          try {
+            ObjectId newObjectId = ObjectId.fromString(getNewContentBytes(), 0);
+            dirCacheEntry.setObjectId(newObjectId);
+          } catch (InvalidObjectIdException e) {
+            throw new InvalidGitLinkException(
+                String.format("The content for gitlink '%s' must be a valid SHA1.", filePath), e);
+          }
         } else {
           if (dirCacheEntry.getRawMode() == 0) {
             dirCacheEntry.setFileMode(FileMode.REGULAR_FILE);
diff --git a/java/com/google/gerrit/server/edit/tree/InvalidFileModeException.java b/java/com/google/gerrit/server/edit/tree/InvalidFileModeException.java
new file mode 100644
index 0000000..3578dfc
--- /dev/null
+++ b/java/com/google/gerrit/server/edit/tree/InvalidFileModeException.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit.tree;
+
+/** Exception to be throw if an invalid or unsupported file mode is being used. */
+public class InvalidFileModeException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  private final int invalidFileMode;
+
+  InvalidFileModeException(String message, int invalidFileMode) {
+    super(message);
+    this.invalidFileMode = invalidFileMode;
+  }
+
+  public int getInvalidFileMode() {
+    return invalidFileMode;
+  }
+}
diff --git a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java b/java/com/google/gerrit/server/edit/tree/InvalidGitLinkException.java
similarity index 63%
copy from java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
copy to java/com/google/gerrit/server/edit/tree/InvalidGitLinkException.java
index 3ec74ba..ae42805 100644
--- a/java/com/google/gerrit/server/permissions/RefPermissionOrLabel.java
+++ b/java/com/google/gerrit/server/edit/tree/InvalidGitLinkException.java
@@ -12,9 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.permissions;
+package com.google.gerrit.server.edit.tree;
 
-import com.google.gerrit.extensions.api.access.GerritPermission;
+/** Exception to be throw if the content of a gitlink is invalid (i.e. if it's not a SHA1). */
+public class InvalidGitLinkException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
 
-/** A {@link RefPermission} or a {@link AbstractLabelPermission}. */
-public interface RefPermissionOrLabel extends GerritPermission {}
+  InvalidGitLinkException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 71256bd..2edc642 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -35,13 +35,22 @@
 import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.util.Optional;
 
-/** Distributes Events to listeners if they are allowed to see them */
+/**
+ * Distributes events to {@link EventListener}s if they are allowed to see them.
+ *
+ * <p>Gerrit invokes listeners whenever it does an update. For example the {@link
+ * com.google.gerrit.extensions.events.ChangeAbandonedListener} is invoked when a change gets
+ * abandoned. See the classes in the {@code com.google.gerrit.extensions.events} package for which
+ * listeners exist.
+ *
+ * <p>{@link StreamEventsApiListener} implements all the available listeners and creates an event
+ * whenever any of the listeners is invoked. The events get provided to all {@link
+ * EventDispatcher}s, {@code EventBroker} being one of them.
+ */
 @Singleton
 public class EventBroker implements EventDispatcher {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -167,8 +176,7 @@
 
   protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) {
     try {
-      Optional<ProjectState> state = projectCache.get(project);
-      if (!state.isPresent() || !state.get().statePermitsRead()) {
+      if (!projectCache.get(project).isPresent()) {
         return false;
       }
 
@@ -182,8 +190,7 @@
     if (change == null) {
       return false;
     }
-    Optional<ProjectState> pe = projectCache.get(change.getProject());
-    if (!pe.isPresent() || !pe.get().statePermitsRead()) {
+    if (!projectCache.get(change.getProject()).isPresent()) {
       return false;
     }
     return permissionBackend
@@ -194,11 +201,9 @@
 
   protected boolean isVisibleTo(BranchNameKey branchName, CurrentUser user)
       throws PermissionBackendException {
-    Optional<ProjectState> pe = projectCache.get(branchName.project());
-    if (!pe.isPresent() || !pe.get().statePermitsRead()) {
+    if (!projectCache.get(branchName.project()).isPresent()) {
       return false;
     }
-
     return permissionBackend.user(user).ref(branchName).test(RefPermission.READ);
   }
 
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index cd91745..6c2b71b 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -23,8 +23,17 @@
   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();
+  /**
+   * Enables the AI Review Prompt button and dialog in the change screen UI.
+   *
+   * <p>This feature is enabled by default but can be disabled via [experiments] disabled =
+   * UiFeature__get_ai_prompt.
+   */
+  public static final String UI_FEATURE_GET_AI_PROMPT = "UiFeature__get_ai_prompt";
+
+  /** Features enabled by default in the current release. */
+  public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
+      ImmutableSet.of(UI_FEATURE_GET_AI_PROMPT);
 
   /**
    * If true, gerrit checks implicit merges on each merge operations.
@@ -60,4 +69,7 @@
   /** Whether we allow fix suggestions in HumanComments. */
   public static final String ALLOW_FIX_SUGGESTIONS_IN_COMMENTS =
       "GerritBackendFeature__allow_fix_suggestions_in_comments";
+
+  /** Whether UI should request Submit Requirements separately from change detail. */
+  public static final String ASYNC_SUBMIT_REQUIREMENTS = "UiFeature__async_submit_requirements";
 }
diff --git a/java/com/google/gerrit/server/flow/BUILD b/java/com/google/gerrit/server/flow/BUILD
new file mode 100644
index 0000000..9bfabf1
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/BUILD
@@ -0,0 +1,22 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "flow",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/exceptions",
+        "//java/com/google/gerrit/extensions:api",
+        "//lib:guava",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
+        "//lib/guice",
+    ],
+)
diff --git a/java/com/google/gerrit/server/flow/Flow.java b/java/com/google/gerrit/server/flow/Flow.java
new file mode 100644
index 0000000..843db84
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/Flow.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * An automation rule on a change that triggers actions on the change when the flow conditions
+ * become satisfied. For example, a flow can be an automation rule that adds a reviewer to the
+ * change when the change has been verified by the CI.
+ *
+ * <p>Flows have multiple stages that each have an expression that defines the condition to be met
+ * and the action to be triggered. Stage are only evaluated if all previous stages have been
+ * satisfied.
+ */
+@AutoValue
+public abstract class Flow {
+  /** The key that uniquely identifies this flow. */
+  public abstract FlowKey key();
+
+  /** The date and time this flow was created. */
+  public abstract Instant createdOn();
+
+  /** The account ID of the user that owns this flow. */
+  public abstract Account.Id ownerId();
+
+  /**
+   * The stages of this flow (sorted by execution order).
+   *
+   * <p>Stage are only evaluated if all previous stages have been satisfied.
+   */
+  public abstract ImmutableList<FlowStage> stages();
+
+  /**
+   * The date and time this flow was last evaluated.
+   *
+   * @return The date and time this flow was last evaluated, {@link Optional#empty()} if the flow
+   *     hasn't been evaluated yet.
+   */
+  public abstract Optional<Instant> lastEvaluatedOn();
+
+  /** Creates a {@link Builder} for this flow instance. */
+  public abstract Builder toBuilder();
+
+  /**
+   * Creates a builder for building a flow.
+   *
+   * @param key The key that uniquely identifies this flow.
+   * @return the builder for building the flow
+   */
+  public static Flow.Builder builder(FlowKey key) {
+    return new AutoValue_Flow.Builder().key(key);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Sets the key that uniquely identifies this flow. */
+    public abstract Builder key(FlowKey key);
+
+    /** Sets the date and time this flow was created. */
+    public abstract Builder createdOn(Instant createdOn);
+
+    /** Set the account ID of the user that owns this flow. */
+    public abstract Builder ownerId(Account.Id ownerId);
+
+    /** Sets the stages of this flow (sorted by execution order). */
+    public abstract Builder stages(ImmutableList<FlowStage> stages);
+
+    /** Sets date and time this flow was last evaluated. */
+    public abstract Builder lastEvaluatedOn(Instant lastEvaluatedOn);
+
+    /** Builds the {@link Flow}. */
+    public abstract Flow build();
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowAction.java b/java/com/google/gerrit/server/flow/FlowAction.java
new file mode 100644
index 0000000..f1294da
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowAction.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.ConvertibleToProto;
+
+/**
+ * An action to be triggered when the condition of a {@link FlowExpression} becomes satisfied.
+ *
+ * <p>Actions can have arbitrary parameters. For example an {@code AddReviewer} action can have the
+ * user to be added as a reviewer as a parameter.
+ */
+@ConvertibleToProto
+@AutoValue
+public abstract class FlowAction {
+  /** The name of the action. */
+  public abstract String name();
+
+  /** Parameters for the action. */
+  public abstract ImmutableList<String> parameters();
+
+  /**
+   * Creates a builder for building a flow action.
+   *
+   * @return the builder for building the flow action
+   */
+  public static FlowAction.Builder builder() {
+    return new AutoValue_FlowAction.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Set the name of the action. */
+    public abstract Builder name(String name);
+
+    /** Sets the expressions for the stages of the flow. */
+    public abstract Builder parameters(ImmutableList<String> parameters);
+
+    abstract ImmutableList.Builder<String> parametersBuilder();
+
+    /** Adds an expression for another stage. */
+    @CanIgnoreReturnValue
+    public Builder addParameter(String param) {
+      parametersBuilder().add(param);
+      return this;
+    }
+
+    /** Builds the {@link FlowAction}. */
+    public abstract FlowAction build();
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowCreation.java b/java/com/google/gerrit/server/flow/FlowCreation.java
new file mode 100644
index 0000000..d780b47
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowCreation.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+
+/** Definition of all properties necessary for creating a {@link Flow}. */
+@AutoValue
+public abstract class FlowCreation {
+  /** The name of the project that contains the change for which the flow applies. */
+  public abstract Project.NameKey projectName();
+
+  /** The ID of the change for which the flow applies. */
+  public abstract Change.Id changeId();
+
+  /** The account ID of the user that owns the flow. */
+  public abstract Account.Id ownerId();
+
+  /** The expressions for the stages of the flow. */
+  public abstract ImmutableList<FlowExpression> stageExpressions();
+
+  /**
+   * Creates a builder for building a flow creation.
+   *
+   * @return the builder for building the flow
+   */
+  public static FlowCreation.Builder builder() {
+    return new AutoValue_FlowCreation.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Sets the name of the project that contains the change for which the flow applies. */
+    public abstract Builder projectName(Project.NameKey projecName);
+
+    /** Set the ID of the change for which the flow applies. */
+    public abstract Builder changeId(Change.Id changeId);
+
+    /** Set the account ID of the user that owns the flow. */
+    public abstract Builder ownerId(Account.Id ownerId);
+
+    /**
+     * Sets the expressions for the stages of the flow.
+     *
+     * <p>Each expressions becomes a stage.
+     */
+    public abstract Builder stageExpressions(ImmutableList<FlowExpression> stages);
+
+    abstract ImmutableList.Builder<FlowExpression> stageExpressionsBuilder();
+
+    /** Adds an expression for another stage. */
+    @CanIgnoreReturnValue
+    public Builder addStageExpression(FlowExpression expression) {
+      stageExpressionsBuilder().add(expression);
+      return this;
+    }
+
+    /** Builds the {@link FlowCreation} without validating the properties. */
+    abstract FlowCreation autoBuild();
+
+    /** Builds the {@link FlowCreation} with validating the properties.. */
+    public FlowCreation build() {
+      FlowCreation flowCreation = autoBuild();
+      checkState(
+          !flowCreation.stageExpressions().isEmpty(),
+          "flow creation must contain at least one stage expression");
+      return flowCreation;
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowExpression.java b/java/com/google/gerrit/server/flow/FlowExpression.java
new file mode 100644
index 0000000..cea48ec
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowExpression.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.ConvertibleToProto;
+import java.util.Optional;
+
+/** Expression defining an action that should be triggered when a condition becomes satisfied. */
+@ConvertibleToProto
+@AutoValue
+public abstract class FlowExpression {
+  /**
+   * The condition which must be satisfied for the action to be triggered.
+   *
+   * <p>Can contain multiple conditions separated by comma.
+   */
+  public abstract String condition();
+
+  /** The action that should be triggered when the condition is satisfied. */
+  public abstract Optional<FlowAction> action();
+
+  public static FlowExpression.Builder builder() {
+    return new AutoValue_FlowExpression.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /**
+     * Sets the condition which must be fulfilled for the action to be triggered.
+     *
+     * <p>Can contain multiple conditions separated by comma.
+     *
+     * <p>Conditions can be Gerrit conditions (e.g. "is the change verified?") as well as conditions
+     * in third-party tools (e.g. "is the issue fixed?").
+     *
+     * <p>Gerrit conditions are expressed as change queries and are satisfied if they match at least
+     * one change (example: {@code change:123 label:Verified+1}).
+     *
+     * <p>The syntax for conditions in third-party tools depends on the flow service implementation.
+     */
+    public abstract Builder condition(String condition);
+
+    /**
+     * Sets the action that should be triggered when the condition is satisfied.
+     *
+     * <p>If multiple actions are wanted, multiple flows or multiple stages should be added.
+     */
+    public abstract Builder action(FlowAction action);
+
+    /** Builds the {@link FlowExpression}. */
+    public abstract FlowExpression build();
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowKey.java b/java/com/google/gerrit/server/flow/FlowKey.java
new file mode 100644
index 0000000..c517274
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowKey.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+
+/** Key that uniquely identifies a {@link Flow}. */
+@AutoValue
+public abstract class FlowKey {
+  /** The name of the project that contains the change for which the flow applies. */
+  public abstract Project.NameKey projectName();
+
+  /** The ID of the change for which the flow applies. */
+  public abstract Change.Id changeId();
+
+  /** The universally unique identifier that identifies the flow. */
+  public abstract String uuid();
+
+  /**
+   * Creates a {@link FlowKey}.
+   *
+   * @param projectName The name of the project that contains the change for which the flow applies.
+   * @param changeId The ID of the change for which the flow applies.
+   * @param uuid The universally unique identifier that identifies the flow.
+   * @return the builder for building the flow key
+   */
+  public static FlowKey create(Project.NameKey projectName, Change.Id changeId, String uuid) {
+    return builder().projectName(projectName).changeId(changeId).uuid(uuid).build();
+  }
+
+  /**
+   * Creates a builder for building a flow key.
+   *
+   * @return the builder for building the flow key
+   */
+  public static FlowKey.Builder builder() {
+    return new AutoValue_FlowKey.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Set the name of the project that contains the change for which the flow applies. */
+    public abstract Builder projectName(Project.NameKey projectName);
+
+    /** Set the ID of the change for which the flow applies. */
+    public abstract Builder changeId(Change.Id changeId);
+
+    /** Set the universally unique identifier that identifies the flow. */
+    public abstract Builder uuid(String uuid);
+
+    /** Builds the {@link FlowKey}. */
+    public abstract FlowKey build();
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowPermissionDeniedException.java b/java/com/google/gerrit/server/flow/FlowPermissionDeniedException.java
new file mode 100644
index 0000000..d22b00f
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowPermissionDeniedException.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+/**
+ * Exception to be thrown either directly or subclassed indicating that a flow operation was denied
+ * due to missing permissions.
+ */
+public class FlowPermissionDeniedException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public FlowPermissionDeniedException(String reason) {
+    super(reason);
+  }
+
+  public FlowPermissionDeniedException(String reason, Throwable why) {
+    super(reason, why);
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowService.java b/java/com/google/gerrit/server/flow/FlowService.java
new file mode 100644
index 0000000..25fd799
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowService.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import java.util.Optional;
+
+/**
+ * Extension point to plug in a service to manage flows.
+ *
+ * <p>A {@link Flow} is an automation rule on a change that triggers actions on the change when the
+ * flow conditions become satisfied. For example, a flow can be an automation rule that adds a
+ * reviewer to the change when the change has been verified by the CI.
+ *
+ * <p>Updating an existing flow is not supported. Instead the flow should be deleted and then be
+ * re-created.
+ */
+@ExtensionPoint
+public interface FlowService {
+
+  /**
+   * Checks if Flows is enabled for change.
+   *
+   * <p>Can be used to disable flows at a change/project level. Implementations can have user
+   * information injected to disable it for a specific user.
+   *
+   * @param projectName The name of the project that contains the change.
+   * @param changeId The ID of the change for which the flows should be listed.
+   * @return If flows is enabled for the user.
+   * @throws RestApiException thrown if checking flow access has failed
+   */
+  public Boolean isFlowsEnabled(Project.NameKey projectName, Change.Id changeId)
+      throws RestApiException;
+
+  /**
+   * Create a new flow.
+   *
+   * @param flowCreation parameters needed for the flow creation
+   * @return the newly created flow
+   * @throws FlowPermissionDeniedException thrown if the caller is not allowed to create the flow
+   * @throws InvalidFlowException thrown is the flow to be created is invalid
+   * @throws StorageException thrown if storing the flow has failed
+   */
+  @CanIgnoreReturnValue
+  Flow createFlow(FlowCreation flowCreation)
+      throws FlowPermissionDeniedException, InvalidFlowException, StorageException;
+
+  /**
+   * Retrieves a flow.
+   *
+   * @param flowKey the key of the flow
+   * @return the flow if it was found and the user can see it, otherwise {@link Optional#empty()}
+   * @throws StorageException thrown if accessing the flow storage has failed
+   */
+  Optional<Flow> getFlow(FlowKey flowKey) throws StorageException;
+
+  /**
+   * Deletes a flow
+   *
+   * @param flowKey the key of the flow
+   * @return the deleted flow, {@link Optional#empty()} if no flow with the given key was found or
+   *     if the flow is not visible to the current user
+   * @throws FlowPermissionDeniedException thrown if the caller can see the flow and is not allowed
+   *     to delete it
+   * @throws StorageException thrown if deleting the flow has failed
+   */
+  @CanIgnoreReturnValue
+  Optional<Flow> deleteFlow(FlowKey flowKey) throws FlowPermissionDeniedException, StorageException;
+
+  /**
+   * Lists the flows for one change.
+   *
+   * <p>The order of the returned flows is stable, but depends on the flow service implementation.
+   *
+   * @param projectName The name of the project that contains the change.
+   * @param changeId The ID of the change for which the flows should be listed.
+   * @return The flows of the change. The service may filter out flows that are not visible to the
+   *     current user.
+   * @throws StorageException thrown if accessing the flow storage has failed
+   */
+  ImmutableList<Flow> listFlows(Project.NameKey projectName, Change.Id changeId)
+      throws StorageException;
+}
diff --git a/java/com/google/gerrit/server/flow/FlowServiceUtil.java b/java/com/google/gerrit/server/flow/FlowServiceUtil.java
new file mode 100644
index 0000000..4192c11
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowServiceUtil.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Optional;
+
+/** Helper class to access the {@link FlowService}. */
+@Singleton
+public class FlowServiceUtil {
+  private final DynamicItem<FlowService> flowService;
+
+  @Inject
+  FlowServiceUtil(DynamicItem<FlowService> flowService) {
+    this.flowService = flowService;
+  }
+
+  /**
+   * Returns the {@link FlowService} if a flow service is bound.
+   *
+   * @return the {@link FlowService}, or {@link Optional#empty()} if no flow service is bound.
+   */
+  public Optional<FlowService> getFlowService() {
+    return Optional.ofNullable(flowService.get());
+  }
+
+  /**
+   * Returns the {@link FlowService} if a flow service has been bound, or throws a {@link
+   * MethodNotAllowedException} otherwise.
+   *
+   * @return the {@link FlowService}
+   * @throws MethodNotAllowedException thrown if no flow service is bound
+   */
+  public FlowService getFlowServiceOrThrow() throws MethodNotAllowedException {
+    return getFlowService()
+        .orElseThrow(() -> new MethodNotAllowedException("No FlowService bound."));
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowStage.java b/java/com/google/gerrit/server/flow/FlowStage.java
new file mode 100644
index 0000000..993334d
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowStage.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A stage in a flow, consisting out of a flow expression that defines an action that should be
+ * triggered when a condition becomes satisfied and a status.
+ *
+ * <p>Stage are only evaluated if all previous stages have been satisfied.
+ */
+@AutoValue
+public abstract class FlowStage {
+  /** The expression defining the condition and the action of this stage. */
+  public abstract FlowExpression expression();
+
+  /** The evaluation status for this stage. */
+  public abstract FlowStageEvaluationStatus status();
+
+  /** Creates a {@link Builder} for this flow stage instance. */
+  public abstract Builder toBuilder();
+
+  public static FlowStage.Builder builder() {
+    return new AutoValue_FlowStage.Builder().status(FlowStageEvaluationStatus.notEvaledStatus());
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Sets the expression defining the condition and the action of this stage. */
+    public abstract Builder expression(FlowExpression expression);
+
+    /** Sets the evaluation status for this stage. */
+    public abstract Builder status(FlowStageEvaluationStatus status);
+
+    /** Builds the {@link FlowStage}. */
+    public abstract FlowStage build();
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/FlowStageEvaluationStatus.java b/java/com/google/gerrit/server/flow/FlowStageEvaluationStatus.java
new file mode 100644
index 0000000..16e1728
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/FlowStageEvaluationStatus.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.ConvertibleToProto;
+import java.time.Instant;
+import java.util.Optional;
+
+/** Data about the evaluation and execution of a single stage in the flow. */
+@ConvertibleToProto
+@AutoValue
+public abstract class FlowStageEvaluationStatus {
+  /** State of a stage in a {@link Flow}; */
+  public enum State {
+    /** Default, unset value. */
+    UNKNOWN(0),
+    /** The condition of the stage is not satisfied yet or the action has not been executed yet. */
+    PENDING(1),
+    /** The condition of the stage is satisfied and the action has been executed. */
+    DONE(2),
+    /** The stage has a non-recoverable error, e.g. performing the action has failed. */
+    FAILED(3),
+    /**
+     * The stage has been terminated without having been executed, e.g. because a previous stage
+     * failed or because it wasn't done within a timeout.
+     */
+    TERMINATED(4);
+
+    State(int v) {
+      this.value = v;
+    }
+
+    public int getValue() {
+      return value;
+    }
+
+    private final int value;
+  }
+
+  /** The state of this stage. */
+  public abstract State state();
+
+  /**
+   * Message contains extra information about the stage execution. Such as error details if the
+   * stage failed.
+   */
+  public abstract Optional<String> message();
+
+  /**
+   * The timestamp at which all previous stages completed and evaluation of the current this stage
+   * started.
+   */
+  public abstract Optional<Instant> startTime();
+
+  /* The timestamp at which the stage became DONE/FAILED/TERMINATED. */
+  public abstract Optional<Instant> endTime();
+
+  /** Creates a {@link Builder} for this instance. */
+  public abstract Builder toBuilder();
+
+  public static FlowStageEvaluationStatus.Builder builder() {
+    return new AutoValue_FlowStageEvaluationStatus.Builder();
+  }
+
+  /** Fresh stage that has never been evaluated. */
+  public static FlowStageEvaluationStatus notEvaledStatus() {
+    return new AutoValue_FlowStageEvaluationStatus.Builder().state(State.PENDING).build();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /** Sets the state for this stage. */
+    public abstract Builder state(State state);
+
+    /** Sets a message for this flow stage, e.g. to inform about execution errors. */
+    public abstract Builder message(String message);
+
+    /**
+     * Sets the timestamp at which all previous stages completed and evaluation of the current this
+     * stage started..
+     */
+    public abstract Builder startTime(Instant message);
+
+    /** Sets the timestamp at which the stage became DONE/FAILED/TERMINATED. */
+    public abstract Builder endTime(Instant message);
+
+    /** Builds the {@link FlowStageEvaluationStatus}. */
+    public abstract FlowStageEvaluationStatus build();
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/InvalidFlowException.java b/java/com/google/gerrit/server/flow/InvalidFlowException.java
new file mode 100644
index 0000000..c3afd77
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/InvalidFlowException.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow;
+
+/**
+ * Exception to be thrown either directly or subclassed indicating that a flow be to created is
+ * invalid (e.g. because a condition cannot be parsed, because an action is unknown or because
+ * mandatory parameters for an action are missing).
+ */
+public class InvalidFlowException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public InvalidFlowException(String reason) {
+    super(reason);
+  }
+
+  public InvalidFlowException(String reason, Throwable why) {
+    super(reason, why);
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/converters/BUILD b/java/com/google/gerrit/server/flow/converters/BUILD
new file mode 100644
index 0000000..5c311bb
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/converters/BUILD
@@ -0,0 +1,19 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "converters",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/entities/converter:proto_converter",
+        "//java/com/google/gerrit/server/flow",
+        "//lib:guava",
+        "//lib:protobuf",
+        "//lib/errorprone:annotations",
+        "//proto:flow_java_proto",
+    ],
+)
diff --git a/java/com/google/gerrit/server/flow/converters/FlowActionProtoConverter.java b/java/com/google/gerrit/server/flow/converters/FlowActionProtoConverter.java
new file mode 100644
index 0000000..8b6632e
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/converters/FlowActionProtoConverter.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow.converters;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.converter.SafeProtoConverter;
+import com.google.gerrit.server.flow.FlowAction;
+import com.google.protobuf.Parser;
+
+@Immutable
+public enum FlowActionProtoConverter
+    implements SafeProtoConverter<com.google.gerrit.server.flow.proto.FlowAction, FlowAction> {
+  INSTANCE;
+
+  @Override
+  public com.google.gerrit.server.flow.proto.FlowAction toProto(FlowAction action) {
+    return com.google.gerrit.server.flow.proto.FlowAction.newBuilder()
+        .setName(action.name())
+        .addAllParameters(action.parameters())
+        .build();
+  }
+
+  @Override
+  public FlowAction fromProto(com.google.gerrit.server.flow.proto.FlowAction proto) {
+    return FlowAction.builder()
+        .name(proto.getName())
+        .parameters(ImmutableList.copyOf(proto.getParametersList()))
+        .build();
+  }
+
+  @Override
+  public Parser<com.google.gerrit.server.flow.proto.FlowAction> getParser() {
+    return com.google.gerrit.server.flow.proto.FlowAction.parser();
+  }
+
+  @Override
+  public Class<com.google.gerrit.server.flow.proto.FlowAction> getProtoClass() {
+    return com.google.gerrit.server.flow.proto.FlowAction.class;
+  }
+
+  @Override
+  public Class<FlowAction> getEntityClass() {
+    return FlowAction.class;
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/converters/FlowExpressionProtoConverter.java b/java/com/google/gerrit/server/flow/converters/FlowExpressionProtoConverter.java
new file mode 100644
index 0000000..c2f566c
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/converters/FlowExpressionProtoConverter.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow.converters;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.converter.ProtoConverter;
+import com.google.gerrit.entities.converter.SafeProtoConverter;
+import com.google.gerrit.server.flow.FlowAction;
+import com.google.gerrit.server.flow.FlowExpression;
+import com.google.protobuf.Parser;
+
+@Immutable
+public enum FlowExpressionProtoConverter
+    implements
+        SafeProtoConverter<com.google.gerrit.server.flow.proto.FlowExpression, FlowExpression> {
+  INSTANCE;
+
+  private final ProtoConverter<com.google.gerrit.server.flow.proto.FlowAction, FlowAction>
+      flowActionConverter = FlowActionProtoConverter.INSTANCE;
+
+  @Override
+  public com.google.gerrit.server.flow.proto.FlowExpression toProto(FlowExpression expression) {
+    var builder =
+        com.google.gerrit.server.flow.proto.FlowExpression.newBuilder()
+            .setCondition(expression.condition());
+    if (expression.action().isPresent()) {
+      builder.setAction(flowActionConverter.toProto(expression.action().get()));
+    }
+    return builder.build();
+  }
+
+  @Override
+  public FlowExpression fromProto(com.google.gerrit.server.flow.proto.FlowExpression proto) {
+    var builder = FlowExpression.builder().condition(proto.getCondition());
+    if (proto.hasAction()) {
+      builder.action(flowActionConverter.fromProto(proto.getAction()));
+    }
+    return builder.build();
+  }
+
+  @Override
+  public Parser<com.google.gerrit.server.flow.proto.FlowExpression> getParser() {
+    return com.google.gerrit.server.flow.proto.FlowExpression.parser();
+  }
+
+  @Override
+  public Class<com.google.gerrit.server.flow.proto.FlowExpression> getProtoClass() {
+    return com.google.gerrit.server.flow.proto.FlowExpression.class;
+  }
+
+  @Override
+  public Class<FlowExpression> getEntityClass() {
+    return FlowExpression.class;
+  }
+}
diff --git a/java/com/google/gerrit/server/flow/converters/FlowStageEvaluationStatusProtoConverter.java b/java/com/google/gerrit/server/flow/converters/FlowStageEvaluationStatusProtoConverter.java
new file mode 100644
index 0000000..0cc887c
--- /dev/null
+++ b/java/com/google/gerrit/server/flow/converters/FlowStageEvaluationStatusProtoConverter.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.flow.converters;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.converter.SafeProtoConverter;
+import com.google.gerrit.server.flow.FlowStageEvaluationStatus;
+import com.google.protobuf.Parser;
+import java.time.Instant;
+
+@Immutable
+public enum FlowStageEvaluationStatusProtoConverter
+    implements
+        SafeProtoConverter<
+            com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus,
+            FlowStageEvaluationStatus> {
+  INSTANCE;
+
+  @Override
+  public com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus toProto(
+      FlowStageEvaluationStatus status) {
+    var builder =
+        com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus.newBuilder()
+            .setState(
+                com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus.State.forNumber(
+                    status.state().getValue()));
+    if (status.message().isPresent()) {
+      builder.setMessage(status.message().get());
+    }
+    if (status.startTime().isPresent()) {
+      builder.setStartTimeMillis(status.startTime().get().toEpochMilli());
+    }
+    if (status.endTime().isPresent()) {
+      builder.setEndTimeMillis(status.endTime().get().toEpochMilli());
+    }
+    return builder.build();
+  }
+
+  @Override
+  public FlowStageEvaluationStatus fromProto(
+      com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus proto) {
+    var builder =
+        FlowStageEvaluationStatus.builder()
+            .state(FlowStageEvaluationStatus.State.valueOf(proto.getState().name()));
+    if (proto.hasMessage()) {
+      builder.message(proto.getMessage());
+    }
+    if (proto.hasStartTimeMillis()) {
+      builder.startTime(Instant.ofEpochMilli(proto.getStartTimeMillis()));
+    }
+    if (proto.hasEndTimeMillis()) {
+      builder.endTime(Instant.ofEpochMilli(proto.getEndTimeMillis()));
+    }
+    return builder.build();
+  }
+
+  @Override
+  public Parser<com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus> getParser() {
+    return com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus.parser();
+  }
+
+  @Override
+  public Class<com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus> getProtoClass() {
+    return com.google.gerrit.server.flow.proto.FlowStageEvaluationStatus.class;
+  }
+
+  @Override
+  public Class<FlowStageEvaluationStatus> getEntityClass() {
+    return FlowStageEvaluationStatus.class;
+  }
+}
diff --git a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java b/java/com/google/gerrit/server/flow/package-info.java
similarity index 93%
rename from java/com/google/gerrit/pgm/init/index/lucene/package-info.java
rename to java/com/google/gerrit/server/flow/package-info.java
index e72368b..9b73f9a 100644
--- a/java/com/google/gerrit/pgm/init/index/lucene/package-info.java
+++ b/java/com/google/gerrit/server/flow/package-info.java
@@ -13,6 +13,6 @@
 // limitations under the License.
 
 @CheckReturnValue
-package com.google.gerrit.pgm.init.index.lucene;
+package com.google.gerrit.server.flow;
 
 import com.google.errorprone.annotations.CheckReturnValue;
diff --git a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
index c5c6888..b64d872 100644
--- a/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/git/ChangesByProjectCacheImpl.java
@@ -15,28 +15,46 @@
 package com.google.gerrit.server.git;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
 import com.google.common.flogger.FluentLogger;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.converter.AccountIdProtoConverter;
+import com.google.gerrit.entities.converter.ChangeProtoConverter;
+import com.google.gerrit.proto.Entities.Project_NameKey;
+import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.proto.Cache.CachedProjectChangesProto;
+import com.google.gerrit.server.cache.proto.Cache.CachedProjectChangesProto.NonPrivateChangesProto;
+import com.google.gerrit.server.cache.proto.Cache.CachedProjectChangesProto.PrivateChangeProto;
+import com.google.gerrit.server.cache.proto.Cache.ReviewerSetProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
 import com.google.gerrit.server.index.change.ChangeField;
 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.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
+import com.google.protobuf.ByteString;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -63,20 +81,25 @@
   public static class Module extends CacheModule {
     @Override
     protected void configure() {
-      cache(CACHE_NAME, Project.NameKey.class, CachedProjectChanges.class)
-          .weigher(ChangesByProjetCacheWeigher.class);
+      persist(CACHE_NAME, Project_NameKey.class, CachedProjectChanges.class)
+          .weigher(ChangesByProjetCacheWeigher.class)
+          .diskLimit(1 << 30) // 1 GiB
+          .keySerializer(new ProtobufSerializer<>(Project_NameKey.parser()))
+          .valueSerializer(CachedProjectChangesSerializer.INSTANCE)
+          .version(1);
+
       bind(ChangesByProjectCache.class).to(ChangesByProjectCacheImpl.class);
     }
   }
 
-  private final Cache<Project.NameKey, CachedProjectChanges> cache;
+  private final Cache<Project_NameKey, CachedProjectChanges> cache;
   private final ChangeData.Factory cdFactory;
   private final UseIndex useIndex;
   private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   ChangesByProjectCacheImpl(
-      @Named(CACHE_NAME) Cache<Project.NameKey, CachedProjectChanges> cache,
+      @Named(CACHE_NAME) Cache<Project_NameKey, CachedProjectChanges> cache,
       ChangeData.Factory cdFactory,
       UseIndex useIndex,
       Provider<InternalChangeQuery> queryProvider) {
@@ -90,38 +113,46 @@
   @Override
   public Stream<ChangeData> streamChangeDatas(Project.NameKey project, Repository repo)
       throws IOException {
-    CachedProjectChanges projectChanges = cache.getIfPresent(project);
+    Project_NameKey projectProto = Project_NameKey.newBuilder().setName(project.get()).build();
+    CachedProjectChanges projectChanges = cache.getIfPresent(projectProto);
     if (projectChanges != null) {
-      return projectChanges
-          .getUpdatedChangeDatas(
-              project, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating")
-          .stream();
+      CachedProjectChanges.ChangeDataUpdateResult result =
+          projectChanges.updateChangeDatas(
+              project, cdFactory, ChangeNotes.Factory.scanChangeIds(repo), "Updating");
+      if (result.anyUpdated()) {
+        cache.put(projectProto, projectChanges);
+      }
+      return result.cds().stream();
     }
     if (UseIndex.TRUE.equals(useIndex)) {
-      return queryChangeDatasAndLoad(project).stream();
+      return queryChangeDatasAndLoad(projectProto).stream();
     }
-    return scanChangeDatasAndLoad(project, repo).stream();
+    return scanChangeDatasAndLoad(projectProto, repo).stream();
   }
 
-  private Collection<ChangeData> scanChangeDatasAndLoad(Project.NameKey project, Repository repo)
-      throws IOException {
+  private Collection<ChangeData> scanChangeDatasAndLoad(
+      Project_NameKey projectProto, Repository repo) throws IOException {
     CachedProjectChanges ours = new CachedProjectChanges();
     CachedProjectChanges projectChanges = ours;
     try {
-      projectChanges = cache.get(project, () -> ours);
+      projectChanges = cache.get(projectProto, () -> ours);
     } catch (ExecutionException e) {
-      logger.atWarning().withCause(e).log("Cannot load %s for %s", CACHE_NAME, project.get());
+      logger.atWarning().withCause(e).log(
+          "Cannot load %s for %s", CACHE_NAME, projectProto.getName());
     }
-    return projectChanges.getUpdatedChangeDatas(
-        project,
-        cdFactory,
-        ChangeNotes.Factory.scanChangeIds(repo),
-        ours == projectChanges ? "Scanning" : "Updating");
+    return projectChanges
+        .updateChangeDatas(
+            Project.NameKey.parse(projectProto.getName()),
+            cdFactory,
+            ChangeNotes.Factory.scanChangeIds(repo),
+            ours == projectChanges ? "Scanning" : "Updating")
+        .cds();
   }
 
-  private List<ChangeData> queryChangeDatasAndLoad(Project.NameKey project) {
+  private List<ChangeData> queryChangeDatasAndLoad(Project_NameKey projectProto) {
+    Project.NameKey project = Project.NameKey.parse(projectProto.getName());
     List<ChangeData> cds = queryChangeDatas(project);
-    cache.put(project, new CachedProjectChanges(cds));
+    cache.put(projectProto, new CachedProjectChanges(cds));
     return cds;
   }
 
@@ -137,7 +168,10 @@
     }
   }
 
-  private static class CachedProjectChanges {
+  @VisibleForTesting
+  public static class CachedProjectChanges {
+    public record ChangeDataUpdateResult(Collection<ChangeData> cds, boolean anyUpdated) {}
+
     Map<String, Map<Change.Id, ObjectId>> metaObjectIdByNonPrivateChangeByBranch =
         new ConcurrentHashMap<>(); // BranchNameKey "normalized" to a String to dedup project
     Map<Change.Id, PrivateChange> privateChangeById = new ConcurrentHashMap<>();
@@ -148,11 +182,12 @@
       cds.stream().forEach(cd -> insert(cd));
     }
 
-    public Collection<ChangeData> getUpdatedChangeDatas(
+    public ChangeDataUpdateResult updateChangeDatas(
         Project.NameKey project,
         ChangeData.Factory cdFactory,
         Map<Change.Id, ObjectId> metaObjectIdByChange,
         String operation) {
+      boolean anyUpdated = false;
       try (TraceTimer timer =
           TraceContext.newTimer(
               operation + " changes of project",
@@ -167,15 +202,17 @@
             if (cd == null || !cached.metaRevisionOrThrow().equals(e.getValue())) {
               cd = cdFactory.create(project, id);
               update(cached, cd);
+              anyUpdated = true;
             }
           } catch (Exception ex) {
+            anyUpdated = true;
             // Do not let a bad change prevent other changes from being available.
             logger.atFinest().withCause(ex).log("Can't load changeData for %s", id);
             continue;
           }
           cds.add(cd);
         }
-        return cds;
+        return new ChangeDataUpdateResult(cds, anyUpdated);
       }
     }
 
@@ -332,11 +369,11 @@
   }
 
   private static class ChangesByProjetCacheWeigher
-      implements Weigher<Project.NameKey, CachedProjectChanges> {
+      implements Weigher<Project_NameKey, CachedProjectChanges> {
     @Override
-    public int weigh(Project.NameKey project, CachedProjectChanges changes) {
+    public int weigh(Project_NameKey project, CachedProjectChanges changes) {
       int size = 0;
-      size += project.get().length();
+      size += project.getName().length();
       size += changes.weigh();
       return size;
     }
@@ -361,4 +398,131 @@
     public static final int OBJECT = 16;
     public static final int REFERENCE = 8;
   }
+
+  static class ReviewerSetSerializer {
+    public static ReviewerSetProto serialize(ReviewerSet reviewerSet) {
+      ReviewerSetProto.Builder builder = ReviewerSetProto.newBuilder();
+
+      reviewerSet
+          .asTable()
+          .cellSet()
+          .forEach(
+              cell -> {
+                ReviewerSetProto.ReviewerProto.Builder reviewerBuilder =
+                    ReviewerSetProto.ReviewerProto.newBuilder()
+                        .setAccountId(
+                            AccountIdProtoConverter.INSTANCE.toProto(
+                                Account.id(cell.getColumnKey().get())))
+                        .setState(cell.getRowKey().name())
+                        .setTimestamp(cell.getValue().toEpochMilli());
+
+                builder.addReviewers(reviewerBuilder.build());
+              });
+
+      return builder.build();
+    }
+
+    public static ReviewerSet deserialize(ReviewerSetProto proto) {
+      Table<ReviewerStateInternal, Account.Id, Instant> table = HashBasedTable.create();
+
+      for (ReviewerSetProto.ReviewerProto reviewer : proto.getReviewersList()) {
+        Account.Id accountId = AccountIdProtoConverter.INSTANCE.fromProto(reviewer.getAccountId());
+        ReviewerStateInternal state = ReviewerStateInternal.valueOf(reviewer.getState());
+        Instant timestamp = Instant.ofEpochMilli(reviewer.getTimestamp());
+
+        table.put(state, accountId, timestamp);
+      }
+
+      return ReviewerSet.fromTable(table);
+    }
+  }
+
+  enum CachedProjectChangesSerializer implements CacheSerializer<CachedProjectChanges> {
+    INSTANCE;
+
+    @Override
+    public byte[] serialize(CachedProjectChanges value) {
+      CachedProjectChangesProto.Builder protoBuilder = CachedProjectChangesProto.newBuilder();
+
+      for (Map.Entry<String, Map<Change.Id, ObjectId>> branchEntry :
+          value.metaObjectIdByNonPrivateChangeByBranch.entrySet()) {
+        NonPrivateChangesProto.Builder nonPrivateChangesBuilder =
+            NonPrivateChangesProto.newBuilder();
+
+        for (Map.Entry<Change.Id, ObjectId> changeEntry : branchEntry.getValue().entrySet()) {
+          nonPrivateChangesBuilder.putChanges(
+              changeEntry.getKey().get(),
+              ObjectIdConverter.create().toByteString(changeEntry.getValue()));
+        }
+
+        protoBuilder.putNonPrivateChangesByBranch(
+            branchEntry.getKey(), nonPrivateChangesBuilder.build());
+      }
+
+      for (Map.Entry<Change.Id, PrivateChange> entry : value.privateChangeById.entrySet()) {
+        PrivateChange pc = entry.getValue();
+        PrivateChangeProto.Builder privateChangeBuilder = PrivateChangeProto.newBuilder();
+
+        privateChangeBuilder.setChange(ChangeProtoConverter.INSTANCE.toProto(pc.change()));
+
+        if (pc.reviewers() != null) {
+          privateChangeBuilder.setReviewers(ReviewerSetSerializer.serialize(pc.reviewers()));
+        }
+
+        privateChangeBuilder.setMetaRevision(
+            ObjectIdConverter.create().toByteString(pc.metaRevision()));
+
+        protoBuilder.putPrivateChanges(entry.getKey().get(), privateChangeBuilder.build());
+      }
+
+      return Protos.toByteArray(protoBuilder.build());
+    }
+
+    @Override
+    public CachedProjectChanges deserialize(byte[] in) {
+      try {
+        CachedProjectChangesProto proto =
+            Protos.parseUnchecked(CachedProjectChangesProto.parser(), in);
+
+        CachedProjectChanges result = new CachedProjectChanges();
+
+        for (Map.Entry<String, NonPrivateChangesProto> branchEntry :
+            proto.getNonPrivateChangesByBranchMap().entrySet()) {
+
+          Map<Change.Id, ObjectId> changesMap = new ConcurrentHashMap<>();
+          for (Map.Entry<Integer, ByteString> changeEntry :
+              branchEntry.getValue().getChangesMap().entrySet()) {
+
+            changesMap.put(
+                Change.id(changeEntry.getKey()),
+                ObjectId.fromRaw(changeEntry.getValue().toByteArray()));
+          }
+
+          result.metaObjectIdByNonPrivateChangeByBranch.put(branchEntry.getKey(), changesMap);
+        }
+
+        for (Map.Entry<Integer, PrivateChangeProto> entry :
+            proto.getPrivateChangesMap().entrySet()) {
+          Change.Id id = Change.id(entry.getKey());
+          PrivateChangeProto privateChangeProto = entry.getValue();
+
+          Change change = ChangeProtoConverter.INSTANCE.fromProto(privateChangeProto.getChange());
+          ReviewerSet reviewers =
+              ReviewerSetSerializer.deserialize(privateChangeProto.getReviewers());
+          ObjectId metaRevision =
+              ObjectId.fromRaw(privateChangeProto.getMetaRevision().toByteArray());
+
+          result.privateChangeById.put(
+              id,
+              new AutoValue_ChangesByProjectCacheImpl_PrivateChange(
+                  change, reviewers, metaRevision));
+        }
+
+        return result;
+      } catch (Exception e) {
+        logger.atWarning().withCause(e).log("Failed to deserialize CachedProjectChanges");
+        return new CachedProjectChanges();
+      }
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/git/CodeReviewCommit.java b/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 2990aa8..9c05763 100644
--- a/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.submit.CommitMergeStatus;
 import java.io.IOException;
@@ -172,23 +173,70 @@
     this.statusMessage = Optional.ofNullable(statusMessage);
   }
 
-  public void setNoConflicts() {
+  public void setNoConflictsForNonMergeCommit() {
     this.conflicts =
         PatchSet.Conflicts.create(
-            Optional.empty(), Optional.empty(), /* containsConflicts= */ false);
+            /* base= */ Optional.empty(),
+            /* ours= */ Optional.empty(),
+            /* theirs= */ Optional.empty(),
+            /* mergeStrategy= */ Optional.empty(),
+            /* noBaseReason= */ Optional.of(NoMergeBaseReason.NO_MERGE_PERFORMED),
+            /* containsConflicts= */ false);
   }
 
   public void setConflicts(
-      ObjectId ours, ObjectId theirs, @Nullable Set<String> filesWithGitConflicts) {
+      ObjectId base,
+      ObjectId ours,
+      ObjectId theirs,
+      String mergeStrategy,
+      @Nullable Set<String> filesWithGitConflicts) {
     if (filesWithGitConflicts != null && !filesWithGitConflicts.isEmpty()) {
       this.conflicts =
           PatchSet.Conflicts.create(
-              Optional.of(ours), Optional.of(theirs), /* containsConflicts= */ true);
+              Optional.of(base),
+              Optional.of(ours),
+              Optional.of(theirs),
+              Optional.of(mergeStrategy),
+              /* noBaseReason= */ Optional.empty(),
+              /* containsConflicts= */ true);
       this.filesWithGitConflicts = ImmutableSet.copyOf(filesWithGitConflicts);
     } else {
       this.conflicts =
           PatchSet.Conflicts.create(
-              Optional.of(ours), Optional.of(theirs), /* containsConflicts= */ false);
+              Optional.of(base),
+              Optional.of(ours),
+              Optional.of(theirs),
+              Optional.of(mergeStrategy),
+              /* noBaseReason= */ Optional.empty(),
+              /* containsConflicts= */ false);
+    }
+  }
+
+  public void setConflictsBaseNotAvailable(
+      ObjectId ours,
+      ObjectId theirs,
+      String mergeStrategy,
+      NoMergeBaseReason noMergeBaseReason,
+      @Nullable Set<String> filesWithGitConflicts) {
+    if (filesWithGitConflicts != null && !filesWithGitConflicts.isEmpty()) {
+      this.conflicts =
+          PatchSet.Conflicts.create(
+              Optional.empty(),
+              Optional.of(ours),
+              Optional.of(theirs),
+              Optional.of(mergeStrategy),
+              Optional.of(noMergeBaseReason),
+              /* containsConflicts= */ true);
+      this.filesWithGitConflicts = ImmutableSet.copyOf(filesWithGitConflicts);
+    } else {
+      this.conflicts =
+          PatchSet.Conflicts.create(
+              Optional.empty(),
+              Optional.of(ours),
+              Optional.of(theirs),
+              Optional.of(mergeStrategy),
+              Optional.of(noMergeBaseReason),
+              /* containsConflicts= */ false);
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 30f2ee8..291d5a9 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.mail.EmailFactories.CHANGE_REVERTED;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
+import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
@@ -69,6 +70,7 @@
 import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -84,6 +86,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.ChangeIdUtil;
@@ -242,6 +245,17 @@
     return id;
   }
 
+  public static String getBugAndIssueFooters(RevCommit commit) {
+    StringBuilder footers = new StringBuilder();
+    for (FooterLine footerLine : commit.getFooterLines()) {
+      String key = footerLine.getKey();
+      if (key.equalsIgnoreCase("Bug") || key.equalsIgnoreCase("Issue")) {
+        footers.append(footerLine.getKey()).append(": ").append(footerLine.getValue()).append("\n");
+      }
+    }
+    return footers.toString();
+  }
+
   /**
    * Creates a revert commit.
    *
@@ -288,8 +302,38 @@
     if (message == null) {
       message =
           MessageFormat.format(
-              ChangeMessages.get().revertChangeDefaultMessage, subject, patch.commitId().name());
+              ChangeMessages.revertChangeDefaultMessage, subject, patch.commitId().name());
     }
+
+    String newFooters = getBugAndIssueFooters(commitToRevert);
+    if (!newFooters.isEmpty()) {
+      StringBuilder footersToAdd = new StringBuilder();
+      for (String footer : Splitter.on('\n').split(newFooters)) {
+        if (footer.isEmpty()) {
+          continue;
+        }
+        boolean alreadyExists =
+            Arrays.stream(message.split("\n"))
+                .anyMatch(line -> line.trim().equalsIgnoreCase(footer.trim()));
+        if (!alreadyExists) {
+          footersToAdd.append(footer).append("\n");
+        }
+      }
+
+      if (footersToAdd.length() > 0) {
+        String trimmedMsg = message.trim();
+        List<String> lines = Splitter.on('\n').splitToList(trimmedMsg);
+        String lastLine = lines.isEmpty() ? "" : lines.get(lines.size() - 1);
+        boolean endsWithFooter = lastLine.matches("^[a-zA-Z0-9-]+:.*");
+
+        if (endsWithFooter) {
+          message = trimmedMsg + "\n" + footersToAdd.toString().trim();
+        } else {
+          message = trimmedMsg + "\n\n" + footersToAdd.toString().trim();
+        }
+      }
+    }
+
     if (generatedChangeId != null) {
       message = ChangeIdUtil.insertId(message, generatedChangeId, true);
     }
@@ -307,7 +351,7 @@
     // The revert commit is based on the commit that is being reverted and has the same tree as the
     // parent of the commit that is being reverted. This means revert commit never contains any
     // conflicts.
-    revertCommit.setNoConflicts();
+    revertCommit.setNoConflictsForNonMergeCommit();
 
     return revertCommit;
   }
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java b/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
index 19f7965..a24bbb9 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
@@ -16,7 +16,7 @@
 
 import com.google.common.flogger.backend.Platform;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.inject.Inject;
@@ -25,15 +25,14 @@
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
-import org.eclipse.jgit.lib.Config;
 
 public class GarbageCollectionLogFile implements LifecycleListener {
   private static final String LOG_NAME = "gc_log";
 
   @Inject
-  public GarbageCollectionLogFile(SitePaths sitePaths, @GerritServerConfig Config config) {
+  public GarbageCollectionLogFile(SitePaths sitePaths, LogConfig config) {
     if (SystemLog.shouldConfigure()) {
-      initLogSystem(sitePaths.logs_dir, config.getBoolean("log", "rotate", true));
+      initLogSystem(sitePaths.logs_dir, config.shouldRotate());
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 4c1f7e3..724eb78 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -18,13 +18,13 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Comparator.naturalOrder;
 import static java.util.stream.Collectors.joining;
 
 import com.google.auto.factory.AutoFactory;
 import com.google.auto.factory.Provided;
+import com.google.auto.value.AutoOneOf;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -43,9 +43,11 @@
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.exceptions.GerritNoMergeBaseException;
 import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -75,6 +77,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.StringJoiner;
 import org.eclipse.jgit.attributes.AttributesNodeProvider;
 import org.eclipse.jgit.diff.Sequence;
 import org.eclipse.jgit.dircache.DirCache;
@@ -121,13 +124,6 @@
 public class MergeUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  /**
-   * Length of abbreviated hex SHA-1s in merged filenames.
-   *
-   * <p>This is a constant so output is stable over time even if the SHA-1 prefix becomes ambiguous.
-   */
-  private static final int NAME_ABBREV_LEN = 6;
-
   private static final String R_HEADS_MASTER = Constants.R_HEADS + Constants.MASTER;
 
   public static boolean useRecursiveMerge(Config cfg) {
@@ -239,7 +235,8 @@
           InvalidMergeStrategyException {
 
     ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig, attributesNodeProvider);
-    m.setBase(originalCommit.getParent(parentIndex));
+    RevCommit baseCommit = originalCommit.getParent(parentIndex);
+    m.setBase(baseCommit);
 
     DirCache dc = DirCache.newInCore();
     if (allowConflicts && m instanceof ResolveMerger) {
@@ -315,6 +312,8 @@
               rw,
               inserter,
               dc,
+              "BASE",
+              MergeBase.create(baseCommit),
               "HEAD",
               mergeTip,
               "CHANGE",
@@ -333,7 +332,8 @@
     cherryPickCommit.setMessage(commitMsg);
     matchAuthorToCommitterDate(project, cherryPickCommit);
     CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit));
-    commit.setConflicts(mergeTip, originalCommit, filesWithGitConflicts);
+    commit.setConflicts(
+        baseCommit, mergeTip, originalCommit, mergeStrategyName(), filesWithGitConflicts);
     logger.atFine().log("CherryPick commitId=%s", commit.name());
     return commit;
   }
@@ -343,6 +343,8 @@
       RevWalk rw,
       ObjectInserter ins,
       DirCache dc,
+      String baseName,
+      MergeBase base,
       String oursName,
       RevCommit ours,
       String theirsName,
@@ -350,23 +352,38 @@
       Map<String, MergeResult<? extends Sequence>> mergeResults,
       boolean diff3Format)
       throws IOException {
+    int nameLength = Math.max(Math.max(oursName.length(), theirsName.length()), baseName.length());
+
+    String baseDescription =
+        switch (base.getKind()) {
+          case BASE_COMMIT -> {
+            rw.parseBody(base.baseCommit());
+            String baseMsg = base.baseCommit().getShortMessage();
+            yield String.format(
+                "%s %s",
+                base.baseCommit().getName(), baseMsg.substring(0, Math.min(baseMsg.length(), 60)));
+          }
+          case NO_BASE_REASON -> base.noBaseReason().getDescription();
+        };
+    String baseNameFormatted =
+        String.format("%-" + nameLength + "s (%s)", baseName, baseDescription);
+
     rw.parseBody(ours);
     rw.parseBody(theirs);
     String oursMsg = ours.getShortMessage();
     String theirsMsg = theirs.getShortMessage();
 
-    int nameLength = Math.max(oursName.length(), theirsName.length());
     String oursNameFormatted =
         String.format(
             "%-" + nameLength + "s (%s %s)",
             oursName,
-            abbreviateName(ours, NAME_ABBREV_LEN),
+            ours.getName(),
             oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
     String theirsNameFormatted =
         String.format(
             "%-" + nameLength + "s (%s %s)",
             theirsName,
-            abbreviateName(theirs, NAME_ABBREV_LEN),
+            theirs.getName(),
             theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
 
     MergeFormatter fmt = new MergeFormatter();
@@ -378,9 +395,10 @@
         // TODO(dborowitz): Respect inCoreLimit here.
         buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
         if (diff3Format) {
-          fmt.formatMergeDiff3(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+          fmt.formatMergeDiff3(
+              buf, p, baseNameFormatted, oursNameFormatted, theirsNameFormatted, UTF_8);
         } else {
-          fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+          fmt.formatMerge(buf, p, baseNameFormatted, oursNameFormatted, theirsNameFormatted, UTF_8);
         }
         buf.close(); // Flush file and close for writes, but leave available for reading.
 
@@ -453,6 +471,7 @@
       CodeReviewRevWalk rw,
       boolean diff3Format)
       throws IOException,
+          GerritNoMergeBaseException,
           MergeIdenticalTreeException,
           MergeConflictException,
           InvalidMergeStrategyException {
@@ -474,7 +493,17 @@
 
     ObjectId tree;
     ImmutableSet<String> filesWithGitConflicts;
-    if (m.merge(false, mergeTip, originalCommit)) {
+    MergeBase mergeBase;
+
+    boolean couldMerge;
+    try {
+      couldMerge = m.merge(false, mergeTip, originalCommit);
+    } catch (NoMergeBaseException e) {
+      throw new GerritNoMergeBaseException(e);
+    }
+
+    if (couldMerge) {
+      mergeBase = MergeBase.create(rw, mergeStrategy, m.getBaseCommitId());
       filesWithGitConflicts = null;
       tree = m.getResultTreeId();
     } else {
@@ -524,11 +553,14 @@
               .map(Map.Entry::getKey)
               .collect(toImmutableSet());
 
+      mergeBase = MergeBase.create(rw, mergeStrategy, m.getBaseCommitId());
       tree =
           mergeWithConflicts(
               rw,
               inserter,
               dc,
+              "BASE",
+              mergeBase,
               "TARGET BRANCH",
               mergeTip,
               "SOURCE BRANCH",
@@ -544,19 +576,37 @@
     mergeCommit.setCommitter(committerIdent);
     mergeCommit.setMessage(commitMsg);
     CodeReviewCommit commit = rw.parseCommit(inserter.insert(mergeCommit));
-    commit.setConflicts(mergeTip, originalCommit, filesWithGitConflicts);
+
+    switch (mergeBase.getKind()) {
+      case BASE_COMMIT ->
+          commit.setConflicts(
+              mergeBase.baseCommit(),
+              mergeTip,
+              originalCommit,
+              mergeStrategy,
+              filesWithGitConflicts);
+      case NO_BASE_REASON ->
+          commit.setConflictsBaseNotAvailable(
+              mergeTip,
+              originalCommit,
+              mergeStrategy,
+              mergeBase.noBaseReason(),
+              filesWithGitConflicts);
+    }
     return commit;
   }
 
-  public static String createConflictMessage(List<String> conflicts) {
-    if (conflicts.isEmpty()) {
-      return "";
+  public static String createConflictMessage(List<String> filesWithConflicts) {
+    StringBuilder sb = new StringBuilder("merge conflict(s)");
+
+    // If the simple-two-way-in-core merge strategy was used, we don't know which files had
+    // conflicts and filesWithConflicts is empty.
+    if (!filesWithConflicts.isEmpty()) {
+      StringJoiner joiner = new StringJoiner("\n* ", ":\n* ", "\n");
+      filesWithConflicts.forEach(joiner::add);
+      sb.append(joiner.toString());
     }
 
-    StringBuilder sb = new StringBuilder("merge conflict(s):");
-    for (String c : conflicts) {
-      sb.append('\n').append(c);
-    }
     return sb.toString();
   }
 
@@ -1185,4 +1235,46 @@
               commit.getCommitter().getZoneId()));
     }
   }
+
+  @AutoOneOf(MergeBase.Kind.class)
+  public abstract static class MergeBase {
+    public enum Kind {
+      BASE_COMMIT,
+      NO_BASE_REASON
+    }
+
+    public abstract Kind getKind();
+
+    public abstract RevCommit baseCommit();
+
+    public abstract NoMergeBaseReason noBaseReason();
+
+    public static MergeBase create(
+        RevWalk rw, String mergeStrategy, @Nullable ObjectId baseCommitId) throws IOException {
+      if (baseCommitId != null) {
+        try {
+          RevCommit baseCommit = rw.parseCommit(baseCommitId);
+          return AutoOneOf_MergeUtil_MergeBase.baseCommit(baseCommit);
+        } catch (MissingObjectException e) {
+          // RecursiveMerger performs a content merge, if necessary across multiple bases, using
+          // recursion to compute a usable base and trying to parse this computed base fails with a
+          // MissingObjectException.
+          return AutoOneOf_MergeUtil_MergeBase.noBaseReason(NoMergeBaseReason.COMPUTED_BASE);
+        }
+      }
+
+      if ("ours".equals(mergeStrategy) || "theirs".equals(mergeStrategy)) {
+        return AutoOneOf_MergeUtil_MergeBase.noBaseReason(
+            NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
+      }
+
+      // baseCommitId is null if the merged commits do not have a common predecessor
+      // (e.g. if 2 initial commits or 2 commits with unrelated histories are merged)
+      return AutoOneOf_MergeUtil_MergeBase.noBaseReason(NoMergeBaseReason.NO_COMMON_ANCESTOR);
+    }
+
+    public static MergeBase create(RevCommit baseCommit) {
+      return AutoOneOf_MergeUtil_MergeBase.baseCommit(baseCommit);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 5f815b8..84f3eab 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -90,7 +90,8 @@
 
   public enum TaskKind {
     INDEXING,
-    RECEIVE_COMMITS;
+    RECEIVE_COMMITS,
+    MIGRATION;
   }
 
   /** Handle for a sub-task. */
diff --git a/java/com/google/gerrit/server/git/TransferConfig.java b/java/com/google/gerrit/server/git/TransferConfig.java
index 728e4ed..caf03bf 100644
--- a/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/java/com/google/gerrit/server/git/TransferConfig.java
@@ -39,7 +39,7 @@
                 "transfer",
                 null,
                 "timeout", //
-                0,
+                60,
                 TimeUnit.SECONDS);
     maxObjectSizeLimit = cfg.getLong("receive", "maxObjectSizeLimit", 0);
     maxObjectSizeLimitFormatted = cfg.getString("receive", null, "maxObjectSizeLimit");
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 49d0be0..1f87f1b 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -398,7 +398,7 @@
       }
     }
 
-    private class CancellableCountDownLatch extends CountDownLatch {
+    private static class CancellableCountDownLatch extends CountDownLatch {
       protected volatile boolean cancelled = false;
 
       public CancellableCountDownLatch(int count) {
@@ -451,7 +451,7 @@
             public Thread newThread(Runnable task) {
               final Thread t = parent.newThread(task);
               t.setName(queueName + "-" + tid.getAndIncrement());
-              t.setUncaughtExceptionHandler(WorkQueue::logUncaughtException);
+              t.setUncaughtExceptionHandler(WorkQueue::handleUncaughtException);
               return t;
             }
           });
@@ -764,8 +764,14 @@
     }
   }
 
-  private static void logUncaughtException(Thread t, Throwable e) {
+  private static void handleUncaughtException(Thread t, Throwable e) {
     logger.atSevere().withCause(e).log("WorkQueue thread %s threw exception", t.getName());
+
+    // Clear the logging context to prevent that it leaks into other threads.
+    if (!LoggingContext.getInstance().isEmpty()) {
+      logger.atInfo().log("Clearing LoggingContext after uncaught exception");
+      LoggingContext.getInstance().clear();
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/server/git/receive/PushOptionsValidator.java b/java/com/google/gerrit/server/git/receive/PushOptionsValidator.java
new file mode 100644
index 0000000..59caab5
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/PushOptionsValidator.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+
+/**
+ * Extension point to validate push options.
+ *
+ * <p>Possible usages are:
+ *
+ * <ul>
+ *   <li>Reject pushes that use a certain push option or a certain combination of push options.
+ *   <li>Print a warning if a certain push option is used (e.g because the push option is
+ *       deprecated).
+ * </ul>
+ */
+@ExtensionPoint
+public interface PushOptionsValidator {
+  /**
+   * Validate the push options that have been specified for a push to the given ref.
+   *
+   * @param projectName The name of the project to which the user is pushing.
+   * @param refName The target ref of the push.
+   * @param pushOptions The options that have been specified on push. This map includes the Git push
+   *     options (specified by {@code -o <key>=<value>}) and options which have been specified as
+   *     part of the target ref name (specified as {@code refs/for/master%<key>=<value>}).
+   * @return A list of validation messages. If any message with type {@link
+   *     com.google.gerrit.server.git.validators.ValidationMessage.Type#ERROR} or {@link
+   *     com.google.gerrit.server.git.validators.ValidationMessage.Type#FATAL} is returned the push
+   *     is rejected. Validation messages with other type are returned to the client, but let the
+   *     push proceed.
+   */
+  ImmutableList<ValidationMessage> validate(
+      Project.NameKey projectName, String refName, ListMultimap<String, String> pushOptions);
+}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index fb483f0..73b680b 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
 import static com.google.gerrit.entities.RefNames.isConfigRef;
 import static com.google.gerrit.entities.RefNames.isRefsUsersSelf;
@@ -30,7 +29,6 @@
 import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
 import static com.google.gerrit.server.git.receive.ReceiveConstants.SAME_CHANGE_ID_IN_MULTIPLE_CHANGES;
 import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -63,7 +61,6 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
-import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
@@ -124,7 +121,6 @@
 import com.google.gerrit.server.RequestInfo;
 import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.ServiceUserClassifier;
 import com.google.gerrit.server.approval.ApprovalsUtil;
@@ -167,7 +163,6 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceIdConsumer;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
-import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
@@ -256,7 +251,7 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -286,6 +281,8 @@
 
   public static final String DIRECT_PUSH_JUSTIFICATION_OPTION = "push-justification";
 
+  private static final String CUSTOM_KEYED_VALUE_OPTION = "custom-keyed-value";
+
   interface Factory {
     ReceiveCommits create(
         ProjectState projectState,
@@ -392,7 +389,6 @@
   // depend on previous ones.
 
   // Injected fields.
-  private final AccountResolver accountResolver;
   private final AclInfoController aclInfoController;
   private final AllProjectsName allProjectsName;
   private final BatchUpdate.Factory batchUpdateFactory;
@@ -415,6 +411,7 @@
   private final DeadlineChecker.Factory deadlineCheckerFactory;
   private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+  private final DynamicSet<PushOptionsValidator> pushOptionsValidators;
   private final DynamicSet<PluginPushOption> pluginPushOptions;
   private final PluginSetContext<ReceivePackInitializer> initializers;
   private final MergedByPushOp.Factory mergedByPushOpFactory;
@@ -480,6 +477,7 @@
   private boolean setChangeAsPrivate;
   private Optional<NoteDbPushOption> noteDbPushOption;
   private Optional<String> tracePushOption = Optional.empty();
+  private Map<String, String> customKeyedValues = new HashMap<>();
 
   private final TraceIdConsumer traceIdConsumer;
   private MessageSender messageSender;
@@ -492,7 +490,6 @@
 
   @Inject
   ReceiveCommits(
-      AccountResolver accountResolver,
       AclInfoController aclInfoController,
       AllProjectsName allProjectsName,
       BatchUpdate.Factory batchUpdateFactory,
@@ -515,6 +512,7 @@
       DeadlineChecker.Factory deadlineCheckerFactory,
       DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+      DynamicSet<PushOptionsValidator> pushOptionsValidators,
       DynamicSet<PluginPushOption> pluginPushOptions,
       PluginSetContext<ReceivePackInitializer> initializers,
       PluginSetContext<CommentValidator> commentValidators,
@@ -554,7 +552,6 @@
       @Assisted @Nullable RequestCounter requestCounter)
       throws IOException {
     // Injected fields.
-    this.accountResolver = accountResolver;
     this.aclInfoController = aclInfoController;
     this.allProjectsName = allProjectsName;
     this.batchUpdateFactory = batchUpdateFactory;
@@ -587,6 +584,7 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.permissionBackend = permissionBackend;
     this.pluginConfigEntries = pluginConfigEntries;
+    this.pushOptionsValidators = pushOptionsValidators;
     this.pluginPushOptions = pluginPushOptions;
     this.projectCache = projectCache;
     this.psUtil = psUtil;
@@ -752,6 +750,7 @@
       Throwable error = null;
       try (RequestStateContext requestStateContext =
           RequestStateContext.open()
+              .setPerformanceSummaryProvider(performanceLogContext)
               .addRequestStateProvider(progress)
               .addRequestStateProvider(
                   deadlineCheckerFactory.create(start, requestInfo, clientProvidedDeadlineValue))) {
@@ -1170,11 +1169,11 @@
       ObjectInserter ins,
       ImmutableList<CreateRequest> newChanges,
       Task replaceProgress) {
+    ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
     try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
       try (TraceTimer traceTimer =
           newTimer(
               "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
-        ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
         if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
           logger.atWarning().log(
               "Skipping change updates on %s because ref update failed: %s %s",
@@ -1218,8 +1217,20 @@
         } catch (RestApiException | IOException | UpdateException e) {
           throw new StorageException("Can't insert change/patch set for " + project.getName(), e);
         }
+      }
+    }
 
-        if (magicBranch != null && magicBranch.submit) {
+    if (magicBranch != null && magicBranch.submit) {
+      // Using the submit option submits the created change(s) immediately without checking labels
+      // nor submit rules. Since code review is bypassed, same as on direct push, use a direct push
+      // RefUpdateContext to do the direct submit.
+      Optional<String> justification =
+          pushOptions.get(DIRECT_PUSH_JUSTIFICATION_OPTION).stream().findFirst();
+      try (RefUpdateContext ctx = RefUpdateContext.openDirectPush(justification)) {
+        try (TraceTimer traceTimer =
+            newTimer(
+                "insertChangesAndPatchSets#submit",
+                Metadata.builder().resourceCount(newChanges.size()))) {
           try {
             submit(newChanges, replaceByChange.values());
           } catch (ResourceConflictException e) {
@@ -1682,6 +1693,12 @@
             RejectionReason.create(
                 MetricBucket.CONFLICT, "prohibited by Gerrit: " + denied.getMessage()));
         return;
+      } catch (UnprocessableEntityException denied) {
+        reject(
+            cmd,
+            RejectionReason.create(
+                MetricBucket.PROHIBITED, "prohibited by Gerrit: " + denied.getMessage()));
+        return;
       }
 
       if (validRefOperation(cmd)) {
@@ -1867,6 +1884,7 @@
     Map<String, Short> labels = new HashMap<>();
     String message;
     List<RevCommit> baseCommit;
+    ListMultimap<String, String> optionMap;
     CmdLineParser cmdLineParser;
     Set<String> hashtags = new HashSet<>();
 
@@ -1874,6 +1892,12 @@
     String trace;
 
     @Option(
+        name = "--push-justification",
+        metaVar = "NAME",
+        usage = "justification for the push if the 'submit' option is used to submit on push")
+    String pushJustification;
+
+    @Option(
         name = "--deadline",
         metaVar = "NAME",
         usage = "deadline after which the push should be aborted")
@@ -1882,6 +1906,16 @@
     @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
     List<ObjectId> base;
 
+    // Custom keyed values need to be specified as a push option ('-o
+    // custom-keyed-value=<key>:<value'). Setting the 'custom-keyed-value' option in the refname is
+    // not allowed. This option here is only defined, so that the 'custom-keyed-value' option gets
+    // mentioned in the help output.
+    @Option(
+        name = "--custom-keyed-value",
+        metaVar = "CUSTOM_KEYED_VALUES",
+        usage = "custom keyed value in the format '<key>:<value>'")
+    List<String> customKeyedValues;
+
     @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
     String topic;
 
@@ -2043,37 +2077,25 @@
     }
 
     /**
-     * Get reviewer strings from magic branch options, combined with additional recipients computed
-     * from some other place.
+     * Get reviewer strings from magic branch options
      *
-     * <p>The set of reviewers on a change includes strings passed explicitly via options as well as
-     * account IDs computed from the commit message itself.
+     * <p>The set of reviewers on a change includes strings passed explicitly via options.
      *
-     * @param additionalRecipients recipients parsed from the commit.
      * @return set of reviewer strings to pass to {@code ReviewerModifier}.
      */
-    ImmutableSet<String> getCombinedReviewers(MailRecipients additionalRecipients) {
-      return getCombinedReviewers(reviewer, additionalRecipients.getReviewers());
+    ImmutableSet<String> getReviewers() {
+      return ImmutableSet.copyOf(reviewer);
     }
 
     /**
-     * Get CC strings from magic branch options, combined with additional recipients computed from
-     * some other place.
+     * Get CC strings from magic branch options
      *
-     * <p>The set of CCs on a change includes strings passed explicitly via options as well as
-     * account IDs computed from the commit message itself.
+     * <p>The set of CCs on a change includes strings passed explicitly via options
      *
-     * @param additionalRecipients recipients parsed from the commit.
      * @return set of CC strings to pass to {@code ReviewerModifier}.
      */
-    ImmutableSet<String> getCombinedCcs(MailRecipients additionalRecipients) {
-      return getCombinedReviewers(cc, additionalRecipients.getCcOnly());
-    }
-
-    private static ImmutableSet<String> getCombinedReviewers(
-        Set<String> strings, Set<Account.Id> ids) {
-      return Streams.concat(strings.stream(), ids.stream().map(Account.Id::toString))
-          .collect(toImmutableSet());
+    ImmutableSet<String> getCcs() {
+      return ImmutableSet.copyOf(cc);
     }
 
     void setWithholdComments(boolean withholdComments) {
@@ -2100,7 +2122,7 @@
     String parse(ListMultimap<String, String> pushOptions) throws CmdLineException {
       String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName()));
 
-      ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
+      optionMap = LinkedListMultimap.create();
 
       // Process and lop off the "%OPTION" suffix.
       int optionStart = ref.indexOf('%');
@@ -2108,16 +2130,33 @@
         for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
           int e = s.indexOf('=');
           if (0 < e) {
-            options.put(s.substring(0, e), s.substring(e + 1));
+            optionMap.put(s.substring(0, e), s.substring(e + 1));
           } else {
-            options.put(s, "");
+            optionMap.put(s, "");
           }
         }
         ref = ref.substring(0, optionStart);
       }
 
-      if (!options.isEmpty()) {
-        cmdLineParser.parseOptionMap(options);
+      // We cannot accept the 'custom-key-value' option in the refname (deprecated format), since
+      // key and value are separated by ':' which is a character that is not allowed in the refname.
+      // If a user tries to specify '%custom-key-value=<key>:<value>' in the refname the push fails
+      // due to ':' not being allowed in refnames. In this case the push fails much earlier and we
+      // do not reach here. However if someone tries to specify the '%custom-key-value' option with
+      // allowed characters in the value, we return a message here telling the user that they should
+      // use the push option instead.
+      if (optionMap.containsKey(CUSTOM_KEYED_VALUE_OPTION)) {
+        throw cmdLineParser.reject(
+            String.format(
+                "option '%s' cannot be specified as an option in the ref name, use a push option"
+                    + " instead ('-o %s=<key>:<value>')",
+                CUSTOM_KEYED_VALUE_OPTION, CUSTOM_KEYED_VALUE_OPTION));
+      }
+
+      optionMap.putAll(pushOptions);
+
+      if (!optionMap.isEmpty()) {
+        cmdLineParser.parseOptionMap(optionMap);
       }
       return ref;
     }
@@ -2174,6 +2213,22 @@
       String ref;
       magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
 
+      // Parse custom keyed values.
+      for (String keyValue : pushOptions.get(CUSTOM_KEYED_VALUE_OPTION)) {
+        List<String> keyValueList = Splitter.on(":").trimResults().splitToList(keyValue);
+        if (keyValueList.size() != 2) {
+          reject(
+              cmd,
+              RejectionReason.create(
+                  MetricBucket.INVALID_OPTION,
+                  String.format(
+                      "the value for option '%s' must be given as '<key>:<format>'",
+                      CUSTOM_KEYED_VALUE_OPTION)));
+          return;
+        }
+        customKeyedValues.put(keyValueList.get(0), keyValueList.get(1));
+      }
+
       // Filter out plugin push options, as the parser would reject them as unknown.
       ImmutableListMultimap<String, String> pushOptionsToParse =
           pushOptions.entries().stream()
@@ -2190,6 +2245,16 @@
         ref = null; // never happens
       }
 
+      if (magicBranch.pushJustification != null && !magicBranch.submit) {
+        reject(
+            cmd,
+            RejectionReason.create(
+                MetricBucket.INVALID_OPTION,
+                "when pushing for a review a push justification can only be set when the"
+                    + " 'submit' option is used"));
+        return;
+      }
+
       if (magicBranch.skipValidation) {
         reject(
             cmd,
@@ -2342,7 +2407,7 @@
             return;
           }
           RevCommit branchTip = globalRevWalk.parseCommit(refTip.getObjectId());
-          if (!globalRevWalk.isMergedInto(tip, branchTip)) {
+          if (!isMergedInto(globalRevWalk, tip, branchTip)) {
             reject(
                 cmd,
                 RejectionReason.create(
@@ -2402,6 +2467,8 @@
             }
           }
         }
+
+        validatePushOptions(cmd, projectState.getNameKey(), ref, magicBranch.optionMap);
       } catch (IOException e) {
         throw new StorageException(
             String.format("Error walking to %s in project %s", destBranch, project.getName()), e);
@@ -2414,6 +2481,37 @@
     }
   }
 
+  private boolean isMergedInto(RevWalk globalRevWalk, RevCommit target, RevCommit start)
+      throws IOException {
+    ReachabilityChecker checker =
+        globalRevWalk.getObjectReader().createReachabilityChecker(globalRevWalk);
+    Optional<RevCommit> unreachableCommit =
+        checker.areAllReachable(ImmutableList.of(target), Stream.of(start));
+    return unreachableCommit.isEmpty();
+  }
+
+  private void validatePushOptions(
+      ReceiveCommand cmd,
+      Project.NameKey projectName,
+      String refName,
+      ListMultimap<String, String> pushOptions) {
+    ImmutableList<ValidationMessage> validationMessages =
+        StreamSupport.stream(pushOptionsValidators.entries().spliterator(), /* parallel= */ false)
+            .flatMap(
+                pluginPushValidators ->
+                    pluginPushValidators.get().validate(projectName, refName, pushOptions).stream())
+            .collect(toImmutableList());
+    for (ValidationMessage validationMessage : validationMessages) {
+      if (validationMessage.isError()) {
+        reject(
+            cmd,
+            RejectionReason.create(MetricBucket.INVALID_OPTION, validationMessage.getMessage()));
+      } else {
+        messages.add(validationMessage);
+      }
+    }
+  }
+
   private boolean isPluginPushOption(String pushOptionName) {
     if (transitionalPluginOptions.contains(pushOptionName)) {
       return true;
@@ -2902,9 +3000,16 @@
         Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
         if (targetRef != null) {
           RevCommit tip = globalRevWalk.parseCommit(targetRef.getObjectId());
+          ReachabilityChecker checker =
+              globalRevWalk.getObjectReader().createReachabilityChecker(globalRevWalk);
           boolean containsImplicitMerges = true;
           for (RevCommit p : mergedParents) {
-            containsImplicitMerges &= !globalRevWalk.isMergedInto(p, tip);
+            Optional<RevCommit> unreachableCommit =
+                checker.areAllReachable(ImmutableList.of(p), Stream.of(tip));
+            containsImplicitMerges &= unreachableCommit.isPresent();
+            if (!containsImplicitMerges) {
+              break;
+            }
           }
 
           if (containsImplicitMerges) {
@@ -3028,6 +3133,7 @@
                 .create(changeId, commit, refName)
                 .setTopic(magicBranch.topic)
                 .setPrivate(setChangeAsPrivate)
+                .setCustomKeyedValues(ImmutableMap.copyOf(customKeyedValues))
                 .setWorkInProgress(magicBranch.shouldSetWorkInProgressOnNewChanges())
                 // The commit has already been validated in
                 // selectNewAndReplacedChangesFromMagicBranch.
@@ -3050,17 +3156,8 @@
         try {
           globalRevWalk.parseBody(commit);
           final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId();
-          Account.Id me = user.getAccountId();
-          List<FooterLine> footerLines = commit.getFooterLines();
           requireNonNull(magicBranch);
 
-          // TODO(dborowitz): Support reviewers by email from footers? Maybe not: kernel developers
-          // with AOSP accounts already complain about these notifications, and that would make it
-          // worse. Might be better to get rid of the feature entirely:
-          // https://groups.google.com/d/topic/repo-discuss/tIFxY7L4DXk/discussion
-          MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, footerLines);
-          fromFooters.remove(me);
-
           Map<String, Short> approvals = magicBranch.labels;
           StringBuilder msg =
               new StringBuilder(
@@ -3073,9 +3170,7 @@
 
           bu.setNotify(magicBranch.getNotifyForNewChange());
           bu.insertChange(
-              ins.setReviewersAndCcsAsStrings(
-                      magicBranch.getCombinedReviewers(fromFooters),
-                      magicBranch.getCombinedCcs(fromFooters))
+              ins.setReviewersAndCcsAsStrings(magicBranch.getReviewers(), magicBranch.getCcs())
                   .setApprovals(approvals)
                   .setMessage(msg.toString())
                   .setRequestScopePropagator(requestScopePropagator)
@@ -3407,12 +3502,26 @@
           return false;
         }
 
-        try (TraceTimer traceTimer2 = newTimer("validateNewPatchSetNoteDb#isMergedInto")) {
+        try (TraceTimer traceTimer2 =
+            newTimer(
+                "validateNewPatchSetNoteDb#isMergedInto",
+                Metadata.builder().resourceCount(revisions.size()))) {
+          ReachabilityChecker checker =
+              globalRevWalk.getObjectReader().createReachabilityChecker(globalRevWalk);
           for (RevCommit prior : revisions.keySet()) {
-            // Don't allow a change to directly depend upon itself. This is a
-            // very common error due to users making a new commit rather than
-            // amending when trying to address review comments.
-            if (globalRevWalk.isMergedInto(prior, newCommit)) {
+            // Don't allow a change to directly depend upon itself. This is a very common error due
+            // to users making a new commit rather than amending when trying to address review
+            // comments.
+
+            // Since prior and newCommit have the same Change-Id, we must make sure that prior is
+            // not reachable from newCommit (otherwise there would be a dependency between patch
+            // sets of the same change).
+            Optional<RevCommit> unreachableCommit =
+                checker.areAllReachable(ImmutableList.of(prior), Stream.of(newCommit));
+
+            // If no unreachableCommit was returned, it means prior is reachable from newCommit and
+            // we should reject the newCommit.
+            if (unreachableCommit.isEmpty()) {
               reject(
                   inputCommand,
                   RejectionReason.create(
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 87ed6e7..966904c 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -17,7 +17,6 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.server.change.ReviewerModifier.newReviewerInputFromCommitIdentity;
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
@@ -49,7 +48,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.approval.ApprovalCopier;
 import com.google.gerrit.server.approval.ApprovalsUtil;
@@ -93,6 +91,7 @@
 import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -130,7 +129,6 @@
   private static final String CHANGE_IS_CLOSED = "change is closed";
 
   private final AccountCache accountCache;
-  private final AccountResolver accountResolver;
   private final String anonymousCowardName;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeData.Factory changeDataFactory;
@@ -180,7 +178,6 @@
   @Inject
   ReplaceOp(
       AccountCache accountCache,
-      AccountResolver accountResolver,
       @AnonymousCowardName String anonymousCowardName,
       ApprovalsUtil approvalsUtil,
       ChangeData.Factory changeDataFactory,
@@ -213,7 +210,6 @@
       @Assisted @Nullable PushCertificate pushCertificate,
       @Assisted RequestScopePropagator requestScopePropagator) {
     this.accountCache = accountCache;
-    this.accountResolver = accountResolver;
     this.anonymousCowardName = anonymousCowardName;
     this.approvalsUtil = approvalsUtil;
     this.changeDataFactory = changeDataFactory;
@@ -327,7 +323,7 @@
       reviewMessage = magicBranch.message;
       psDescription = magicBranch.message;
       approvals.putAll(magicBranch.labels);
-      Set<String> hashtags = magicBranch.hashtags;
+      Set<String> hashtags = new HashSet<>(magicBranch.hashtags);
       if (hashtags != null && !hashtags.isEmpty()) {
         hashtags.addAll(notes.getHashtags());
         update.setHashtags(hashtags);
@@ -370,7 +366,7 @@
             psDescription);
 
     update.setPsDescription(psDescription);
-    MailRecipients fromFooters = getRecipientsFromFooters(accountResolver, commit.getFooterLines());
+
     approvalsUtil.addApprovalsForNewPatchSet(
         update, projectState.getLabelTypes(), newPatchSet, ctx.getUser(), approvals);
 
@@ -378,7 +374,7 @@
         reviewerModifier.prepare(
             ctx.getNotes(),
             ctx.getUser(),
-            getReviewerInputs(magicBranch, fromFooters, ctx.getChange(), info),
+            getReviewerInputs(magicBranch, ctx.getChange(), info),
             true);
     Optional<ReviewerModification> reviewerError =
         reviewerAdditions.getFailures().stream().findFirst();
@@ -411,10 +407,7 @@
   }
 
   private ImmutableList<ReviewerInput> getReviewerInputs(
-      @Nullable MagicBranchInput magicBranch,
-      MailRecipients fromFooters,
-      Change change,
-      PatchSetInfo psInfo) {
+      @Nullable MagicBranchInput magicBranch, Change change, PatchSetInfo psInfo) {
     // Disable individual emails when adding reviewers, as all reviewers will receive the single
     // bulk new change email.
     Stream<ReviewerInput> inputs =
@@ -437,11 +430,11 @@
       inputs =
           Streams.concat(
               inputs,
-              magicBranch.getCombinedReviewers(fromFooters).stream()
+              magicBranch.getReviewers().stream()
                   .map(r -> newReviewerInput(r, ReviewerState.REVIEWER)),
-              magicBranch.getCombinedCcs(fromFooters).stream()
-                  .map(r -> newReviewerInput(r, ReviewerState.CC)));
+              magicBranch.getCcs().stream().map(r -> newReviewerInput(r, ReviewerState.CC)));
     }
+
     return inputs.collect(toImmutableList());
   }
 
diff --git a/java/com/google/gerrit/server/git/validators/CommentCountValidator.java b/java/com/google/gerrit/server/git/validators/CommentCountValidator.java
index a554f90..15b445e 100644
--- a/java/com/google/gerrit/server/git/validators/CommentCountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentCountValidator.java
@@ -45,9 +45,7 @@
     ChangeNotes notes =
         notesFactory.createChecked(Project.nameKey(ctx.getProject()), Change.id(ctx.getChangeId()));
     int numExistingCommentsAndChangeMessages =
-        notes.getHumanComments().size()
-            + notes.getRobotComments().size()
-            + notes.getChangeMessages().size();
+        notes.getHumanComments().size() + notes.getChangeMessages().size();
     if (!comments.isEmpty()
         && numExistingCommentsAndChangeMessages + comments.size() > maxComments) {
       // This warning really applies to the set of all comments, but we need to pick one to attach
diff --git a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
index b887323..67285ff 100644
--- a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.Inject;
-import java.util.stream.Stream;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -55,11 +54,7 @@
     ChangeNotes notes =
         notesFactory.createChecked(Project.nameKey(ctx.getProject()), Change.id(ctx.getChangeId()));
     int existingCumulativeSize =
-        Stream.concat(
-                    notes.getHumanComments().values().stream(),
-                    notes.getRobotComments().values().stream())
-                .mapToInt(Comment::getApproximateSize)
-                .sum()
+        notes.getHumanComments().values().stream().mapToInt(Comment::getApproximateSize).sum()
             + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
     int newCumulativeSize =
         comments.stream().mapToInt(CommentForValidation::getApproximateSize).sum();
@@ -85,11 +80,7 @@
    */
   public static boolean isEnoughSpace(ChangeNotes notes, int addedBytes, int maxCumulativeSize) {
     int existingCumulativeSize =
-        Stream.concat(
-                    notes.getHumanComments().values().stream(),
-                    notes.getRobotComments().values().stream())
-                .mapToInt(Comment::getApproximateSize)
-                .sum()
+        notes.getHumanComments().values().stream().mapToInt(Comment::getApproximateSize).sum()
             + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
     return existingCumulativeSize + addedBytes < maxCumulativeSize;
   }
diff --git a/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java b/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java
index 6c7eae6..eb1414e 100644
--- a/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentSizeValidator.java
@@ -26,12 +26,10 @@
 /** Limits the size of comments to prevent space/time complexity issues. */
 public class CommentSizeValidator implements CommentValidator {
   private final int commentSizeLimit;
-  private final int robotCommentSizeLimit;
 
   @Inject
   CommentSizeValidator(@GerritServerConfig Config serverConfig) {
     commentSizeLimit = serverConfig.getInt("change", "commentSizeLimit", 16 << 10);
-    robotCommentSizeLimit = serverConfig.getInt("change", "robotCommentSizeLimit", 1 << 20);
   }
 
   @Override
@@ -46,8 +44,6 @@
   private boolean exceedsSizeLimit(CommentForValidation comment) {
     return switch (comment.getSource()) {
       case HUMAN -> commentSizeLimit > 0 && comment.getApproximateSize() > commentSizeLimit;
-      case ROBOT ->
-          robotCommentSizeLimit > 0 && comment.getApproximateSize() > robotCommentSizeLimit;
       default ->
           throw new RuntimeException(
               "Unknown comment source (should not have compiled): " + comment.getSource());
@@ -60,10 +56,6 @@
           String.format(
               "Comment size exceeds limit (%d > %d)",
               comment.getApproximateSize(), commentSizeLimit);
-      case ROBOT ->
-          String.format(
-              "Size %d (bytes) of robot comment is greater than limit %d (bytes)",
-              comment.getApproximateSize(), robotCommentSizeLimit);
       default ->
           throw new RuntimeException(
               "Unknown comment source (should not have compiled): " + comment.getSource());
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 392f2ae..72d1d87 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -749,20 +749,20 @@
       PersonIdent committer = commit.getCommitterIdent();
       PersonIdent author = commit.getAuthorIdent();
 
-      boolean sboAuthor = false;
-      boolean sboCommitter = false;
-      boolean sboMe = false;
+      boolean signedOffByAuthor = false;
+      boolean signedOffByCommitter = false;
+      boolean signedOffByMe = false;
       for (FooterLine footer : commit.getFooterLines()) {
         if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
           String e = footer.getEmailAddress();
           if (e != null) {
-            sboAuthor |= author.getEmailAddress().equals(e);
-            sboCommitter |= committer.getEmailAddress().equals(e);
-            sboMe |= user.hasEmailAddress(e);
+            signedOffByAuthor |= author.getEmailAddress().equals(e);
+            signedOffByCommitter |= committer.getEmailAddress().equals(e);
+            signedOffByMe |= user.hasEmailAddress(e);
           }
         }
       }
-      if (!sboAuthor && !sboCommitter && !sboMe) {
+      if (!signedOffByAuthor && !signedOffByCommitter && !signedOffByMe) {
         try {
           if (!perm.test(RefPermission.FORGE_COMMITTER)) {
             throw new CommitValidationException(
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 654b5d0..73269ce 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -189,7 +190,7 @@
               if (!allowProjectOwnersToChangeParent) {
                 try {
                   if (!permissionBackend
-                      .user(caller.getRealUser())
+                      .user(caller, ImpersonationPermissionMode.REAL_USER)
                       .test(GlobalPermission.ADMINISTRATE_SERVER)) {
                     throw new MergeValidationException(SET_BY_ADMIN);
                   }
@@ -200,7 +201,7 @@
               } else {
                 try {
                   permissionBackend
-                      .user(caller.getRealUser())
+                      .user(caller, ImpersonationPermissionMode.REAL_USER)
                       .project(destProject.getNameKey())
                       .check(ProjectPermission.WRITE_CONFIG);
                 } catch (AuthException e) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 3777e8e..6c30479 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -1122,7 +1122,7 @@
           .build(
               cd ->
                   Stream.concat(
-                          cd.publishedComments().stream().map(c -> c.message),
+                          cd.getCommentsForIndex().stream(),
                           // Some endpoint allow passing user message in input, and we still want to
                           // search by that. Index on message template with placeholders for user
                           // data, so we don't
@@ -1133,7 +1133,7 @@
   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMENT_SPEC =
       COMMENT_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMENT);
 
-  /** Number of unresolved comment threads of the change, including robot comments. */
+  /** Number of unresolved comment threads of the change. */
   public static final IndexedField<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT_FIELD =
       IndexedField.<ChangeData>integerBuilder("UnresolvedCommentCount")
           .stored()
@@ -1145,7 +1145,7 @@
       UNRESOLVED_COMMENT_COUNT_FIELD.integerRange(
           ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT);
 
-  /** Total number of published inline comments of the change, including robot comments. */
+  /** Total number of published inline comments of the change. */
   public static final IndexedField<ChangeData, Integer> TOTAL_COMMENT_COUNT_FIELD =
       IndexedField.<ChangeData>integerBuilder("TotalCommentCount")
           .stored()
diff --git a/java/com/google/gerrit/server/logging/LoggingContext.java b/java/com/google/gerrit/server/logging/LoggingContext.java
index adc9c2b..fce4844 100644
--- a/java/com/google/gerrit/server/logging/LoggingContext.java
+++ b/java/com/google/gerrit/server/logging/LoggingContext.java
@@ -19,6 +19,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.flogger.context.Tags;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.inject.Provider;
@@ -97,12 +98,20 @@
   }
 
   public void clear() {
-    tags.remove();
-    forceLogging.remove();
-    performanceLogging.remove();
-    performanceLogRecords.remove();
-    aclLogging.remove();
-    aclLogRecords.remove();
+    try {
+      tags.remove();
+      forceLogging.remove();
+      performanceLogging.remove();
+      performanceLogRecords.remove();
+      aclLogging.remove();
+      aclLogRecords.remove();
+    } catch (RuntimeException e) {
+      FluentLogger.forEnclosingClass()
+          .atSevere()
+          .withCause(e)
+          .log("Clearing logging context failed: %s", this);
+      throw e;
+    }
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
index ab5db02..4a2701a 100644
--- a/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
@@ -76,21 +76,29 @@
     LoggingContext loggingCtx = LoggingContext.getInstance();
 
     if (!loggingCtx.isEmpty()) {
-      logger.atWarning().log("Logging context is not empty: %s", loggingCtx);
+      logger.atWarning().log("Logging context is not empty before call(): %s", loggingCtx);
     }
 
+    T returnValue;
+
     // propagate logging context
-    loggingCtx.setTags(tags);
-    loggingCtx.forceLogging(forceLogging);
-    loggingCtx.performanceLogging(performanceLogging);
-    loggingCtx.setMutablePerformanceLogRecords(mutablePerformanceLogRecords);
-    loggingCtx.aclLogging(aclLogging);
-    loggingCtx.setMutableAclLogRecords(mutableAclLogRecords);
     try {
-      return callable.call();
+      loggingCtx.setTags(tags);
+      loggingCtx.forceLogging(forceLogging);
+      loggingCtx.performanceLogging(performanceLogging);
+      loggingCtx.setMutablePerformanceLogRecords(mutablePerformanceLogRecords);
+      loggingCtx.aclLogging(aclLogging);
+      loggingCtx.setMutableAclLogRecords(mutableAclLogRecords);
+      returnValue = callable.call();
     } finally {
       // Cleanup logging context. This is important if the thread is pooled and reused.
       loggingCtx.clear();
     }
+
+    if (!loggingCtx.isEmpty()) {
+      logger.atWarning().log("Logging context is not empty after call(): %s", loggingCtx);
+    }
+
+    return returnValue;
   }
 }
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
index 1bba018..5b5b47a 100644
--- a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
@@ -99,21 +99,25 @@
     LoggingContext loggingCtx = LoggingContext.getInstance();
 
     if (!loggingCtx.isEmpty()) {
-      logger.atWarning().log("Logging context is not empty: %s", loggingCtx);
+      logger.atWarning().log("Logging context is not empty before run(): %s", loggingCtx);
     }
 
     // propagate logging context
-    loggingCtx.setTags(tags);
-    loggingCtx.forceLogging(forceLogging);
-    loggingCtx.performanceLogging(performanceLogging);
-    loggingCtx.setMutablePerformanceLogRecords(mutablePerformanceLogRecords);
-    loggingCtx.aclLogging(aclLogging);
-    loggingCtx.setMutableAclLogRecords(mutableAclLogRecords);
     try {
+      loggingCtx.setTags(tags);
+      loggingCtx.forceLogging(forceLogging);
+      loggingCtx.performanceLogging(performanceLogging);
+      loggingCtx.setMutablePerformanceLogRecords(mutablePerformanceLogRecords);
+      loggingCtx.aclLogging(aclLogging);
+      loggingCtx.setMutableAclLogRecords(mutableAclLogRecords);
       runnable.run();
     } finally {
       // Cleanup logging context. This is important if the thread is pooled and reused.
       loggingCtx.clear();
     }
+
+    if (!loggingCtx.isEmpty()) {
+      logger.atWarning().log("Logging context is not empty after run(): %s", loggingCtx);
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogContext.java b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
index 0edd344..e2fe752 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogContext.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogContext.java
@@ -14,11 +14,24 @@
 
 package com.google.gerrit.server.logging;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.flogger.LazyArgs.lazy;
+import static java.util.Comparator.comparing;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.Extension;
+import com.google.gerrit.server.cancellation.PerformanceSummaryProvider;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -33,9 +46,15 @@
  * leak into other requests that are executed by the same thread (if a thread pool is used to
  * process requests).
  */
-public class PerformanceLogContext implements AutoCloseable {
+public class PerformanceLogContext implements AutoCloseable, PerformanceSummaryProvider {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  // Keep in sync with PluginMetrics.PLUGIN_LATENCY_NAME.
+  private static final String PLUGIN_LATENCY_NAME = "plugin/latency";
+
+  /** Default for maximum number of operations for which the latency should be logged. */
+  private static final int DEFAULT_MAX_OPERATIONS_TO_LOG = 25;
+
   // Do not use PluginSetContext. PluginSetContext traces the plugin latency with a timer metric
   // which would result in a performance log and we don't want to log the performance of writing
   // a performance log in the performance log (endless loop).
@@ -44,6 +63,14 @@
   private final boolean oldPerformanceLogging;
   private final ImmutableList<PerformanceLogRecord> oldPerformanceLogRecords;
 
+  /** Maximum number of operations for which the latency should be logged. */
+  private final int maxOperationsToLog;
+
+  /** Threshold in nanos from which on a request is considered as "slow" if exceeded. */
+  private final long slowRequestThresholdNanos;
+
+  private long startNanos;
+
   public PerformanceLogContext(
       Config gerritConfig, DynamicSet<PerformanceLogger> performanceLoggers) {
     this.performanceLoggers = performanceLoggers;
@@ -53,13 +80,20 @@
     this.oldPerformanceLogRecords = LoggingContext.getInstance().getPerformanceLogRecords();
     LoggingContext.getInstance().clearPerformanceLogEntries();
 
-    // Do not create performance log entries if performance logging is disabled or if no
-    // PerformanceLogger is registered.
-    boolean enablePerformanceLogging =
-        gerritConfig.getBoolean("tracing", "performanceLogging", false);
     LoggingContext.getInstance()
-        .performanceLogging(
-            enablePerformanceLogging && !Iterables.isEmpty(performanceLoggers.entries()));
+        .performanceLogging(gerritConfig.getBoolean("tracing", "performanceLogging", false));
+
+    this.maxOperationsToLog =
+        gerritConfig.getInt("performance", "maxOperationsToLog", DEFAULT_MAX_OPERATIONS_TO_LOG);
+
+    this.slowRequestThresholdNanos =
+        gerritConfig.getTimeUnit(
+            "performance",
+            /* subsection= */ null,
+            "slowRequestThreshold",
+            SECONDS.toNanos(30),
+            NANOSECONDS);
+    this.startNanos = TimeUtil.nowNanos();
   }
 
   @Override
@@ -68,11 +102,87 @@
       runEach(performanceLoggers, LoggingContext.getInstance().getPerformanceLogRecords());
     }
 
+    long elapsedNanos = TimeUtil.nowNanos() - startNanos;
+    logger.at(elapsedNanos > slowRequestThresholdNanos ? Level.WARNING : Level.FINE).log(
+        "%s",
+        lazy(
+            () ->
+                getPerformanceSummary()
+                    .map(
+                        performanceSummary ->
+                            String.format(
+                                "%s\n\n%s",
+                                elapsedNanos > slowRequestThresholdNanos
+                                    ? String.format(
+                                        "Performance Summary for slow request (request took longer"
+                                            + " than %ss)",
+                                        NANOSECONDS.toSeconds(slowRequestThresholdNanos))
+                                    : "[Performance Summary]",
+                                performanceSummary))
+                    .orElse("No performance summary available")));
+
     // Restore old state. Required to support nesting of PerformanceLogContext's.
     LoggingContext.getInstance().performanceLogging(oldPerformanceLogging);
     LoggingContext.getInstance().setPerformanceLogRecords(oldPerformanceLogRecords);
   }
 
+  @Override
+  public Optional<String> getPerformanceSummary() {
+    ImmutableList<PerformanceLogRecord> performanceLogRecords =
+        LoggingContext.getInstance().getPerformanceLogRecords();
+    if (maxOperationsToLog <= 0 || performanceLogRecords.isEmpty()) {
+      return Optional.empty();
+    }
+
+    Map<String, PerformanceInfo> perRequestPerformanceInfo = new HashMap<>();
+    for (PerformanceLogRecord performanceLogRecord : performanceLogRecords) {
+      String pluginClass =
+          PLUGIN_LATENCY_NAME.equals(performanceLogRecord.operation())
+              ? performanceLogRecord
+                  .metadata()
+                  .map(Metadata::className)
+                  .map(clazz -> clazz.isPresent() ? " (" + clazz.get() + ")" : "")
+                  .orElse("")
+              : "";
+      PerformanceInfo info =
+          perRequestPerformanceInfo.computeIfAbsent(
+              performanceLogRecord.operation() + pluginClass,
+              operationName -> new PerformanceInfo(operationName));
+      info.add(performanceLogRecord.durationNanos());
+    }
+
+    ImmutableList<PerformanceInfo> performanceInfosWithLongestTotalDuration =
+        perRequestPerformanceInfo.values().stream()
+            .filter(performanceLogInfo -> performanceLogInfo.totalDurationMillis() > 0)
+            .sorted(comparing(PerformanceInfo::totalDurationNanos).reversed())
+            .limit(maxOperationsToLog)
+            .collect(toImmutableList());
+
+    ImmutableList<PerformanceInfo> performanceInfosForOperationsThatHaveBeenCalledMostOften =
+        perRequestPerformanceInfo.values().stream()
+            .filter(performanceLogInfo -> performanceLogInfo.count() > 1)
+            .sorted(
+                comparing(PerformanceInfo::count)
+                    .thenComparing(PerformanceInfo::totalDurationNanos)
+                    .reversed())
+            .limit(maxOperationsToLog)
+            .collect(toImmutableList());
+
+    return Optional.of(
+        String.format(
+            """
+            Operations with the highest latency (max %s):
+            %s
+
+            Operations which have been called most often (max %s):
+            %s
+            """,
+            maxOperationsToLog,
+            Joiner.on('\n').join(performanceInfosWithLongestTotalDuration),
+            maxOperationsToLog,
+            Joiner.on('\n').join(performanceInfosForOperationsThatHaveBeenCalledMostOften)));
+  }
+
   /**
    * Invokes all performance loggers.
    *
@@ -116,4 +226,47 @@
   private static TraceContext newPluginTrace(Extension<PerformanceLogger> extension) {
     return TraceContext.open().addPluginTag(extension.getPluginName());
   }
+
+  static class PerformanceInfo {
+    private final String operationName;
+
+    private int count;
+    private long totalDurationNanos;
+
+    PerformanceInfo(String operationName) {
+      this.operationName = operationName;
+      this.count = 0;
+      this.totalDurationNanos = 0;
+    }
+
+    void add(long durationNanos) {
+      this.count++;
+      this.totalDurationNanos += durationNanos;
+    }
+
+    int count() {
+      return count;
+    }
+
+    long totalDurationNanos() {
+      return totalDurationNanos;
+    }
+
+    long totalDurationMillis() {
+      return TimeUnit.NANOSECONDS.toMillis(totalDurationNanos);
+    }
+
+    @Override
+    public String toString() {
+      long totalDurationMillis = totalDurationMillis();
+      if (count == 1) {
+        return String.format("%sx %s (total: %sms)", count, operationName, totalDurationMillis);
+      }
+
+      long averageMillis = totalDurationMillis / count;
+      return String.format(
+          "%sx %s (total: %sms, avg: %sms)",
+          count, operationName, totalDurationMillis, averageMillis);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index a8b6ea3..22f1613 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -99,6 +99,8 @@
  * </pre>
  */
 public class TraceContext implements AutoCloseable {
+  private static final FluentLogger traceContextLogger = FluentLogger.forEnclosingClass();
+
   private static final String PLUGIN_TAG = "PLUGIN";
 
   public static TraceContext open() {
@@ -302,18 +304,23 @@
 
   @Override
   public void close() {
-    for (Table.Cell<String, String, Boolean> cell : tags.cellSet()) {
-      if (cell.getValue()) {
-        LoggingContext.getInstance().removeTag(cell.getRowKey(), cell.getColumnKey());
+    try {
+      for (Table.Cell<String, String, Boolean> cell : tags.cellSet()) {
+        if (cell.getValue()) {
+          LoggingContext.getInstance().removeTag(cell.getRowKey(), cell.getColumnKey());
+        }
       }
-    }
-    if (stopForceLoggingOnClose) {
-      LoggingContext.getInstance().forceLogging(false);
-    }
+      if (stopForceLoggingOnClose) {
+        LoggingContext.getInstance().forceLogging(false);
+      }
 
-    if (stopAclLoggingOnClose) {
-      LoggingContext.getInstance().aclLogging(oldAclLogging);
-      LoggingContext.getInstance().setAclLogRecords(oldAclLogRecords);
+      if (stopAclLoggingOnClose) {
+        LoggingContext.getInstance().aclLogging(oldAclLogging);
+        LoggingContext.getInstance().setAclLogRecords(oldAclLogRecords);
+      }
+    } catch (RuntimeException e) {
+      traceContextLogger.atSevere().withCause(e).log("Closing trace context failed");
+      throw e;
     }
   }
 }
diff --git a/java/com/google/gerrit/server/mail/EmailFactories.java b/java/com/google/gerrit/server/mail/EmailFactories.java
index cb9d541..0209f4f 100644
--- a/java/com/google/gerrit/server/mail/EmailFactories.java
+++ b/java/com/google/gerrit/server/mail/EmailFactories.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
@@ -23,6 +24,7 @@
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.server.account.AuthToken;
 import com.google.gerrit.server.mail.send.AttentionSetChangeEmailDecorator;
 import com.google.gerrit.server.mail.send.ChangeEmail;
 import com.google.gerrit.server.mail.send.ChangeEmail.ChangeEmailDecorator;
@@ -38,6 +40,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -63,6 +66,9 @@
   String REVIEW_REQUESTED = "newchange";
   String KEY_ADDED = "addkey";
   String KEY_DELETED = "deletekey";
+  String AUTH_TOKEN_UPDATED = "AuthTokenUpdate";
+  String AUTH_TOKEN_WILL_EXPIRE = "AuthTokenWillExpire";
+  String AUTH_TOKEN_EXPIRED = "AuthTokenExpired";
   String PASSWORD_UPDATED = "HttpPasswordUpdate";
   String INBOUND_EMAIL_REJECTED = "error";
   String NEW_EMAIL_REGISTERED = "registernewemail";
@@ -82,6 +88,9 @@
       case REVIEW_REQUESTED -> "Review Request";
       case KEY_ADDED -> "Key Added";
       case KEY_DELETED -> "Key Deleted";
+      case AUTH_TOKEN_UPDATED -> "Authentication Token Updated";
+      case AUTH_TOKEN_WILL_EXPIRE -> "Authentication Token Will Expire";
+      case AUTH_TOKEN_EXPIRED -> "Authentication Token Expired";
       case PASSWORD_UPDATED -> "Password Updated";
       case INBOUND_EMAIL_REJECTED -> "Error";
       case NEW_EMAIL_REGISTERED -> "Email Registered";
@@ -147,6 +156,29 @@
   /** Email decorator for adding gpg keys to the account. */
   EmailDecorator createDeleteKeyEmail(IdentifiedUser user, List<String> gpgKeys);
 
+  /** Email decorator for auth token modification operations. */
+  EmailDecorator createAuthTokenUpdateEmail(IdentifiedUser user, String operation, String tokenId);
+
+  /** Email decorator for auth token with close expiration date. */
+  EmailDecorator createAuthTokenWillExpireEmail(Account account, AuthToken authToken);
+
+  /** Email decorator for auth token with close expiration date. */
+  EmailDecorator createAuthTokenWillExpireEmail(
+      Account account,
+      AuthToken authToken,
+      Set<Account.Id> additionalReceivers,
+      String authTokenSettingsUrl);
+
+  /** Email decorator for expired auth tokens. */
+  EmailDecorator createAuthTokenExpiredEmail(Account account, AuthToken authToken);
+
+  /** Email decorator for expired auth tokens. */
+  EmailDecorator createAuthTokenExpiredEmail(
+      Account account,
+      AuthToken authToken,
+      Set<Account.Id> additionalReceivers,
+      String authTokenSettingsUrl);
+
   /** Email decorator for password modification operations. */
   EmailDecorator createHttpPasswordUpdateEmail(IdentifiedUser user, String operation);
 
diff --git a/java/com/google/gerrit/server/mail/MailUtil.java b/java/com/google/gerrit/server/mail/MailUtil.java
index aee8209..9357e4b 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -17,50 +17,14 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.account.AccountResolver;
-import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 import java.util.regex.Pattern;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.revwalk.FooterLine;
 
 public class MailUtil {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public static MailRecipients getRecipientsFromFooters(
-      AccountResolver accountResolver, List<FooterLine> footerLines)
-      throws IOException, ConfigInvalidException {
-    MailRecipients recipients = new MailRecipients();
-    for (FooterLine footerLine : footerLines) {
-      try {
-        if (isReviewer(footerLine)) {
-          Account.Id accountId = toAccountId(accountResolver, footerLine.getValue().trim());
-          recipients.reviewers.add(accountId);
-          logger.atFine().log(
-              "Added account %d from footer line \"%s\" as reviewer", accountId.get(), footerLine);
-        } else if (footerLine.matches(FooterKey.CC)) {
-          Account.Id accountId = toAccountId(accountResolver, footerLine.getValue().trim());
-          recipients.cc.add(accountId);
-          logger.atFine().log(
-              "Added account %d from footer line \"%s\" as cc", accountId.get(), footerLine);
-        }
-      } catch (UnprocessableEntityException e) {
-        logger.atFine().log(
-            "Skip adding reviewer/cc from footer line \"%s\": %s", footerLine, e.getMessage());
-        continue;
-      }
-    }
-    return recipients;
-  }
 
   public static MailRecipients getRecipientsFromReviewers(ReviewerSet reviewers) {
     MailRecipients recipients = new MailRecipients();
@@ -69,19 +33,6 @@
     return recipients;
   }
 
-  @SuppressWarnings("deprecation")
-  private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
-      throws UnprocessableEntityException, IOException, ConfigInvalidException {
-    return accountResolver.resolveByExactNameOrEmail(nameOrEmail).asUnique().account().id();
-  }
-
-  private static boolean isReviewer(FooterLine candidateFooterLine) {
-    return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY)
-        || candidateFooterLine.matches(FooterKey.ACKED_BY)
-        || candidateFooterLine.matches(FooterConstants.REVIEWED_BY)
-        || candidateFooterLine.matches(FooterConstants.TESTED_BY);
-  }
-
   public static class MailRecipients {
     private final Set<Account.Id> reviewers;
     private final Set<Account.Id> cc;
diff --git a/java/com/google/gerrit/server/mail/send/AuthTokenExpiredEmailDecorator.java b/java/com/google/gerrit/server/mail/send/AuthTokenExpiredEmailDecorator.java
new file mode 100644
index 0000000..618dc6a
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/AuthTokenExpiredEmailDecorator.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.send;
+
+import autovalue.shaded.com.google.common.base.Strings;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.mail.send.OutgoingEmail.EmailDecorator;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.util.Set;
+
+/** Sender that informs a user by email that an auth token will expire soon. */
+@AutoFactory
+public class AuthTokenExpiredEmailDecorator implements EmailDecorator {
+  private OutgoingEmail email;
+
+  private final Account account;
+  private final AuthToken token;
+  private final MessageIdGenerator messageIdGenerator;
+  private final Set<Account.Id> additionalReceivers;
+  @Nullable private String authTokenSettingsUrl;
+
+  public AuthTokenExpiredEmailDecorator(
+      @Provided MessageIdGenerator messageIdGenerator, Account account, AuthToken token) {
+    this(messageIdGenerator, account, token, Set.of(), null);
+  }
+
+  public AuthTokenExpiredEmailDecorator(
+      @Provided MessageIdGenerator messageIdGenerator,
+      Account account,
+      AuthToken token,
+      Set<Account.Id> additionalReceivers,
+      @Nullable String authTokenSettingsUrl) {
+    this.messageIdGenerator = messageIdGenerator;
+    this.account = account;
+    this.token = token;
+    this.additionalReceivers = additionalReceivers;
+    this.authTokenSettingsUrl = authTokenSettingsUrl;
+  }
+
+  @Override
+  public void init(OutgoingEmail email) {
+    this.email = email;
+
+    email.setHeader(
+        "Subject",
+        String.format("[Gerrit Code Review] Authentication token '%s' has expired.", token.id()));
+    email.setMessageId(
+        messageIdGenerator.fromReasonAccountIdAndTimestamp(
+            "Auth_token_expired", account.id(), TimeUtil.now()));
+    email.addByAccountId(RecipientType.TO, account.id());
+    for (Account.Id receiver : additionalReceivers) {
+      email.addByAccountId(RecipientType.TO, receiver);
+    }
+  }
+
+  @Override
+  public void populateEmailContent() {
+    email.addSoyEmailDataParam("email", account.preferredEmail());
+    email.addSoyEmailDataParam("userNameEmail", email.getUserNameEmailFor(account.id()));
+    email.addSoyEmailDataParam("expirationDate", token.expirationDate().get().toString());
+    email.addSoyEmailDataParam("tokenId", token.id());
+    if (Strings.isNullOrEmpty(authTokenSettingsUrl)) {
+      authTokenSettingsUrl = email.getSettingsUrl("HTTPCredentials");
+    }
+    email.addSoyEmailDataParam("authTokenSettingsUrl", authTokenSettingsUrl);
+
+    email.appendText(email.textTemplate("AuthTokenExpired"));
+    if (email.useHtml()) {
+      email.appendHtml(email.soyHtmlTemplate("AuthTokenExpiredHtml"));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/AuthTokenUpdateEmailDecorator.java b/java/com/google/gerrit/server/mail/send/AuthTokenUpdateEmailDecorator.java
new file mode 100644
index 0000000..a7be502
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/AuthTokenUpdateEmailDecorator.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.send;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.send.OutgoingEmail.EmailDecorator;
+import com.google.gerrit.server.util.time.TimeUtil;
+
+/** Sender that informs a user by email that an auth token of their account was updated. */
+@AutoFactory
+public class AuthTokenUpdateEmailDecorator implements EmailDecorator {
+  private OutgoingEmail email;
+
+  private final IdentifiedUser user;
+  private final String operation;
+  private final String tokenId;
+  private final MessageIdGenerator messageIdGenerator;
+
+  public AuthTokenUpdateEmailDecorator(
+      @Provided MessageIdGenerator messageIdGenerator,
+      IdentifiedUser user,
+      String operation,
+      String tokenId) {
+    this.messageIdGenerator = messageIdGenerator;
+    this.user = user;
+    this.operation = operation;
+    this.tokenId = tokenId;
+  }
+
+  @Override
+  public void init(OutgoingEmail email) {
+    this.email = email;
+
+    email.setHeader("Subject", "[Gerrit Code Review] Authentication token was " + operation);
+    email.setMessageId(
+        messageIdGenerator.fromReasonAccountIdAndTimestamp(
+            "Auth_token_change", user.getAccountId(), TimeUtil.now()));
+    email.addByAccountId(RecipientType.TO, user.getAccountId());
+  }
+
+  @Override
+  public void populateEmailContent() {
+    email.addSoyEmailDataParam("email", getEmail());
+    email.addSoyEmailDataParam("userNameEmail", email.getUserNameEmailFor(user.getAccountId()));
+    email.addSoyEmailDataParam("operation", operation);
+    email.addSoyEmailDataParam("tokenId", tokenId);
+    email.addSoyEmailDataParam("authTokenSettingsUrl", email.getSettingsUrl("HTTPCredentials"));
+
+    email.appendText(email.textTemplate("AuthTokenUpdate"));
+    if (email.useHtml()) {
+      email.appendHtml(email.soyHtmlTemplate("AuthTokenUpdateHtml"));
+    }
+  }
+
+  private String getEmail() {
+    return user.getAccount().preferredEmail();
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/AuthTokenWillExpireEmailDecorator.java b/java/com/google/gerrit/server/mail/send/AuthTokenWillExpireEmailDecorator.java
new file mode 100644
index 0000000..7bce00f
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/AuthTokenWillExpireEmailDecorator.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail.send;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.mail.send.OutgoingEmail.EmailDecorator;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.util.Set;
+
+/** Sender that informs a user by email that an auth token will expire soon. */
+@AutoFactory
+public class AuthTokenWillExpireEmailDecorator implements EmailDecorator {
+  private OutgoingEmail email;
+
+  private final Account account;
+  private final AuthToken token;
+  private final MessageIdGenerator messageIdGenerator;
+  private final Set<Account.Id> additionalReceivers;
+  @Nullable private String authTokenSettingsUrl;
+
+  public AuthTokenWillExpireEmailDecorator(
+      @Provided MessageIdGenerator messageIdGenerator, Account account, AuthToken token) {
+    this(messageIdGenerator, account, token, Set.of(), null);
+  }
+
+  public AuthTokenWillExpireEmailDecorator(
+      @Provided MessageIdGenerator messageIdGenerator,
+      Account account,
+      AuthToken token,
+      Set<Account.Id> additionalReceivers,
+      String authTokenSettingsUrl) {
+    this.messageIdGenerator = messageIdGenerator;
+    this.account = account;
+    this.token = token;
+    this.additionalReceivers = additionalReceivers;
+    this.authTokenSettingsUrl = authTokenSettingsUrl;
+  }
+
+  @Override
+  public void init(OutgoingEmail email) {
+    this.email = email;
+
+    email.setHeader(
+        "Subject",
+        String.format(
+            "[Gerrit Code Review] Authentication token '%s' will expire soon.", token.id()));
+    email.setMessageId(
+        messageIdGenerator.fromReasonAccountIdAndTimestamp(
+            "Auth_token_will_expire", account.id(), TimeUtil.now()));
+    email.addByAccountId(RecipientType.TO, account.id());
+    for (Account.Id receiver : additionalReceivers) {
+      email.addByAccountId(RecipientType.TO, receiver);
+    }
+  }
+
+  @Override
+  public void populateEmailContent() {
+    email.addSoyEmailDataParam("email", account.preferredEmail());
+    email.addSoyEmailDataParam("userNameEmail", email.getUserNameEmailFor(account.id()));
+    email.addSoyEmailDataParam("expirationDate", token.expirationDate().get().toString());
+    email.addSoyEmailDataParam("tokenId", token.id());
+    if (Strings.isNullOrEmpty(authTokenSettingsUrl)) {
+      authTokenSettingsUrl = email.getSettingsUrl("HTTPCredentials");
+    }
+    email.addSoyEmailDataParam("authTokenSettingsUrl", authTokenSettingsUrl);
+
+    email.appendText(email.textTemplate("AuthTokenWillExpire"));
+    if (email.useHtml()) {
+      email.appendHtml(email.soyHtmlTemplate("AuthTokenWillExpireHtml"));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java b/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
index 5c9106c..54c4061 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
@@ -187,8 +186,12 @@
     if (email.getFrom() != null) {
       // Is the from user in an email squelching group?
       try {
-        args.permissionBackend.absentUser(email.getFrom()).check(GlobalPermission.EMAIL_REVIEWERS);
-      } catch (AuthException | PermissionBackendException e) {
+        if (!args.permissionBackend
+            .absentUser(email.getFrom())
+            .test(GlobalPermission.EMAIL_REVIEWERS)) {
+          emailOnlyAuthors = true;
+        }
+      } catch (PermissionBackendException e) {
         emailOnlyAuthors = true;
       }
     }
@@ -569,24 +572,18 @@
     return this.authors = authors;
   }
 
-  @Override
-  public void populateEmailContent() throws EmailException {
-    BranchEmailUtils.addBranchData(email, args, branch);
-    setThreadHeaders();
-
-    email.addSoyParam("changeId", change.getKey().get());
-    email.addSoyParam("coverLetter", getCoverLetter());
-    email.addSoyParam("fromName", email.getNameFor(email.getFrom()));
-    email.addSoyParam("fromEmail", email.getNameEmailFor(email.getFrom()));
-    email.addSoyParam("diffLines", ChangeEmail.getDiffTemplateData(getUnifiedDiff()));
-
+  protected void addChangeRelatedSoyParams() {
+    // diffLines is the structured diff format for HTML emails.
+    // unifiedDiff is textual diff for plaintext emails.
+    email.addSoyParam("diffLines", ChangeEmail.getDiffTemplateData(getUnifiedDiff().trim()));
     email.addSoyEmailDataParam("unifiedDiff", getUnifiedDiff());
-    email.addSoyEmailDataParam("changeDetail", getChangeDetail());
-    email.addSoyEmailDataParam("changeUrl", getChangeUrl());
     email.addSoyEmailDataParam("includeDiff", getIncludeDiff());
 
-    Map<String, String> changeData = new HashMap<>();
+    // changeDetail - is a plaintext summary of the change (message, files, delta)
+    email.addSoyEmailDataParam("changeDetail", getChangeDetail());
+    email.addSoyEmailDataParam("changeUrl", getChangeUrl());
 
+    Map<String, String> changeData = new HashMap<>();
     String subject = change.getSubject();
     String originalSubject = change.getOriginalSubject();
     changeData.put("subject", subject);
@@ -611,7 +608,13 @@
     patchSetInfoData.put("authorName", patchSetInfo.getAuthor().getName());
     patchSetInfoData.put("authorEmail", patchSetInfo.getAuthor().getEmail());
     email.addSoyParam("patchSetInfo", patchSetInfoData);
+    // We need names rather than account ids / emails to make it user readable.
+    email.addSoyParam(
+        "attentionSet",
+        currentAttentionSet.stream().map(email::getNameFor).sorted().collect(toImmutableList()));
+  }
 
+  protected void addEmailFooters() {
     email.addFooter(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get());
     email.addFooter(MailHeader.CHANGE_NUMBER.withDelimiter() + change.getChangeId());
     email.addFooter(MailHeader.PATCH_SET.withDelimiter() + patchSet.number());
@@ -625,13 +628,24 @@
     for (Account.Id attentionUser : currentAttentionSet) {
       email.addFooter(MailHeader.ATTENTION.withDelimiter() + email.getNameEmailFor(attentionUser));
     }
-    // We need names rather than account ids / emails to make it user readable.
-    email.addSoyParam(
-        "attentionSet",
-        currentAttentionSet.stream().map(email::getNameFor).sorted().collect(toImmutableList()));
+  }
 
+  @Override
+  public void populateEmailContent() throws EmailException {
+    BranchEmailUtils.addBranchData(email, args, branch);
+    setThreadHeaders();
+
+    email.addSoyParam("coverLetter", getCoverLetter());
+    email.addSoyParam("fromName", email.getNameFor(email.getFrom()));
+    email.addSoyParam("fromEmail", email.getNameEmailFor(email.getFrom()));
+
+    addChangeRelatedSoyParams();
+    addEmailFooters();
+
+    // Set email subject.
     setChangeSubjectHeader();
 
+    // Set email content
     if (email.useHtml()) {
       email.appendHtml(email.soyHtmlTemplate("ChangeHeaderHtml"));
     }
diff --git a/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
index b9846f5..64e97ae 100644
--- a/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.exceptions.NoSuchEntityException;
@@ -98,16 +97,6 @@
           .orElse(null);
     }
 
-    /** Returns a web link to the findings tab view of a change. */
-    @Nullable
-    public String getFindingsTabLink() {
-      return args.urlFormatter
-          .get()
-          .getFindingsTabView(changeEmail.getChange())
-          .map(EmailArguments::addUspParam)
-          .orElse(null);
-    }
-
     /**
      * Returns a title for the group, i.e. "Commit Message", "Merge List", or "File [[filename]]".
      */
@@ -443,26 +432,11 @@
         // Set the comment link.
 
         if (comment.key.filename.equals(Patch.PATCHSET_LEVEL)) {
-          if (comment instanceof RobotComment) {
-            commentData.put("link", group.getFindingsTabLink());
-          } else {
-            commentData.put("link", group.getCommentsTabLink());
-          }
+          commentData.put("link", group.getCommentsTabLink());
         } else {
           commentData.put("link", group.getCommentLink(comment.key.uuid));
         }
 
-        // Set robot comment data.
-        if (comment instanceof RobotComment) {
-          RobotComment robotComment = (RobotComment) comment;
-          commentData.put("isRobotComment", true);
-          commentData.put("robotId", robotComment.robotId);
-          commentData.put("robotRunId", robotComment.robotRunId);
-          commentData.put("robotUrl", robotComment.url);
-        } else {
-          commentData.put("isRobotComment", false);
-        }
-
         // If the comment has a quote, don't bother loading the parent message.
         if (!hasQuote(blocks)) {
           // Set parent comment info.
diff --git a/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java b/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java
index 6a56e38..0657ec2 100644
--- a/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java
+++ b/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
@@ -22,6 +24,7 @@
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.server.account.AuthToken;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.ChangeEmail.ChangeEmailDecorator;
 import com.google.gerrit.server.mail.send.InboundEmailRejectionEmailDecorator.InboundEmailError;
@@ -30,6 +33,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import org.eclipse.jgit.lib.ObjectId;
@@ -43,6 +47,9 @@
   private final ChangeEmailImplFactory changeEmailFactory;
   private final AddKeyEmailDecoratorFactory addKeyEmailFactory;
   private final DeleteKeyEmailDecoratorFactory deleteKeyEmailFactory;
+  private final AuthTokenUpdateEmailDecoratorFactory authTokenUpdateEmailFactory;
+  private final AuthTokenWillExpireEmailDecoratorFactory authTokenWillExpireEmailFactory;
+  private final AuthTokenExpiredEmailDecoratorFactory authTokenExpiredEmailFactory;
   private final HttpPasswordUpdateEmailDecoratorFactory httpPasswordUpdateEmailFactory;
   private final RegisterNewEmailDecoratorImplFactory registerNewEmailFactory;
   private final OutgoingEmailFactory outgoingEmailFactory;
@@ -55,6 +62,9 @@
       ChangeEmailImplFactory changeEmailFactory,
       AddKeyEmailDecoratorFactory addKeyEmailFactory,
       DeleteKeyEmailDecoratorFactory deleteKeyEmailFactory,
+      AuthTokenUpdateEmailDecoratorFactory authTokenUpdateEmailFactory,
+      AuthTokenWillExpireEmailDecoratorFactory authTokenWillExpireEmailFactory,
+      AuthTokenExpiredEmailDecoratorFactory authTokenExpiredEmailFactory,
       HttpPasswordUpdateEmailDecoratorFactory httpPasswordUpdateEmailFactory,
       RegisterNewEmailDecoratorImplFactory registerNewEmailFactory,
       OutgoingEmailFactory outgoingEmailFactory) {
@@ -64,6 +74,9 @@
     this.changeEmailFactory = changeEmailFactory;
     this.addKeyEmailFactory = addKeyEmailFactory;
     this.deleteKeyEmailFactory = deleteKeyEmailFactory;
+    this.authTokenUpdateEmailFactory = authTokenUpdateEmailFactory;
+    this.authTokenWillExpireEmailFactory = authTokenWillExpireEmailFactory;
+    this.authTokenExpiredEmailFactory = authTokenExpiredEmailFactory;
     this.httpPasswordUpdateEmailFactory = httpPasswordUpdateEmailFactory;
     this.registerNewEmailFactory = registerNewEmailFactory;
     this.outgoingEmailFactory = outgoingEmailFactory;
@@ -158,6 +171,44 @@
   }
 
   @Override
+  public EmailDecorator createAuthTokenUpdateEmail(
+      IdentifiedUser user, String operation, String tokenId) {
+    return authTokenUpdateEmailFactory.create(user, operation, tokenId);
+  }
+
+  @Override
+  public EmailDecorator createAuthTokenWillExpireEmail(Account account, AuthToken authToken) {
+    return authTokenWillExpireEmailFactory.create(account, authToken);
+  }
+
+  @Override
+  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
+  public EmailDecorator createAuthTokenWillExpireEmail(
+      Account account,
+      AuthToken authToken,
+      Set<Account.Id> additionalReceivers,
+      String authTokenSettingsUrl) {
+    return authTokenWillExpireEmailFactory.create(
+        account, authToken, additionalReceivers, authTokenSettingsUrl);
+  }
+
+  @Override
+  public EmailDecorator createAuthTokenExpiredEmail(Account account, AuthToken authToken) {
+    return authTokenExpiredEmailFactory.create(account, authToken);
+  }
+
+  @Override
+  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
+  public EmailDecorator createAuthTokenExpiredEmail(
+      Account account,
+      AuthToken authToken,
+      Set<Account.Id> additionalReceivers,
+      String authTokenSettingsUrl) {
+    return authTokenExpiredEmailFactory.create(
+        account, authToken, additionalReceivers, authTokenSettingsUrl);
+  }
+
+  @Override
   public EmailDecorator createHttpPasswordUpdateEmail(IdentifiedUser user, String operation) {
     return httpPasswordUpdateEmailFactory.create(user, operation);
   }
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index 7bc319f..cae4128 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -50,6 +50,12 @@
     "AddKeyHtml.soy",
     "AddToAttentionSet.soy",
     "AddToAttentionSetHtml.soy",
+    "AuthTokenExpired.soy",
+    "AuthTokenExpiredHtml.soy",
+    "AuthTokenWillExpire.soy",
+    "AuthTokenWillExpireHtml.soy",
+    "AuthTokenUpdate.soy",
+    "AuthTokenUpdateHtml.soy",
     "ChangeFooter.soy",
     "ChangeFooterHtml.soy",
     "ChangeHeader.soy",
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 132fdc9..64c6657 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -230,8 +230,8 @@
         if (fromUser.isPresent()) {
           GeneralPreferencesInfo senderPrefs = fromUser.get().generalPreferences();
           CurrentUser user = args.currentUserProvider.get();
-          boolean isImpersonating = user.isIdentifiedUser() && user.isImpersonating();
-          if (isImpersonating && user.getAccountId() != fromId) {
+          boolean isImpersonated = user.isIdentifiedUser() && user.isImpersonated();
+          if (isImpersonated && !user.getAccountId().equals(fromId)) {
             // This should not be possible, if this is the case it means the RequestContext is not
             // set up correctly.
             throw new EmailException(
@@ -249,7 +249,7 @@
                 "CC email sender %s because the email strategy of this user is %s",
                 fromUser.get().account().id(), CC_ON_OWN_COMMENTS);
             addByAccountId(RecipientType.CC, fromId);
-          } else if (isImpersonating) {
+          } else if (isImpersonated) {
             // If we are impersonating a user, make sure they receive a CC of
             // this message regardless of email strategy, unless email notifications are explicitly
             // disabled for this user. This way they can always review and audit what we sent
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 456c1e7..7e5855d 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -61,10 +61,10 @@
 @Singleton
 public class SmtpEmailSender implements EmailSender {
   /** The socket's connect timeout (0 = infinite timeout) */
-  private static final int DEFAULT_CONNECT_TIMEOUT = 0;
+  private static final int DEFAULT_CONNECT_TIMEOUT = 10000;
 
   /** The socket's socket read timeout (0 = infinite timeout) */
-  private static final int DEFAULT_SOCKET_TIMEOUT = 0;
+  private static final int DEFAULT_SOCKET_TIMEOUT = 10000;
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
index ba10648..bdadb81 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
@@ -19,6 +19,7 @@
 /** Footers, that can be set in NoteDb commits. */
 public class ChangeNoteFooters {
   public static final FooterKey FOOTER_ATTENTION = new FooterKey("Attention");
+  public static final FooterKey FOOTER_BASE = new FooterKey("Base");
   public static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
   public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
   public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
@@ -29,6 +30,8 @@
   public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
   public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
   public static final FooterKey FOOTER_COPIED_LABEL = new FooterKey("Copied-Label");
+  public static final FooterKey FOOTER_MERGE_STRATEGY = new FooterKey("Merge-Strategy");
+  public static final FooterKey FOOTER_NO_BASE_REASON = new FooterKey("No-Base-Reason");
   public static final FooterKey FOOTER_OURS = new FooterKey("Ours");
   public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
   public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 233998d..6df8389 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -47,16 +47,12 @@
 import com.google.gerrit.entities.PatchSetApprovals;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.RefCache;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -390,7 +386,6 @@
   RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
 
   private DraftCommentNotes draftCommentNotes;
-  private RobotCommentNotes robotCommentNotes;
 
   // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the
   // ChangeNotesCache from handlers.
@@ -578,11 +573,6 @@
         .collect(toImmutableList());
   }
 
-  public ImmutableListMultimap<ObjectId, RobotComment> getRobotComments() {
-    loadRobotComments();
-    return robotCommentNotes.getComments();
-  }
-
   /**
    * If draft comments have already been loaded for this author, then they will not be reloaded.
    * However, this method will load the comments if no draft comments have been loaded or if the
@@ -596,27 +586,11 @@
     }
   }
 
-  private void loadRobotComments() {
-    if (robotCommentNotes == null) {
-      try (TraceTimer timer =
-          TraceContext.newTimer(
-              "Load Robot Comments", Metadata.builder().changeId(change.getId().get()).build())) {
-        robotCommentNotes = new RobotCommentNotes(args, change);
-        robotCommentNotes.load();
-      }
-    }
-  }
-
   @VisibleForTesting
   DraftCommentNotes getDraftCommentNotes() {
     return draftCommentNotes;
   }
 
-  public RobotCommentNotes getRobotCommentNotes() {
-    loadRobotComments();
-    return robotCommentNotes;
-  }
-
   public boolean containsCommentPublished(Comment c) {
     for (Comment l : getHumanComments().values()) {
       if (c.key.equals(l.key)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index b402d91..feb27a6 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BASE;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
@@ -28,6 +29,8 @@
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_MERGE_STRATEGY;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_NO_BASE_REASON;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_OURS;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
@@ -82,6 +85,7 @@
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRecord.Label.Status;
 import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
@@ -690,6 +694,10 @@
     return parseSha1(commit, FOOTER_COMMIT);
   }
 
+  private Optional<ObjectId> parseBase(ChangeNotesCommit commit) throws ConfigInvalidException {
+    return parseSha1(commit, FOOTER_BASE);
+  }
+
   private Optional<ObjectId> parseOurs(ChangeNotesCommit commit) throws ConfigInvalidException {
     return parseSha1(commit, FOOTER_OURS);
   }
@@ -698,6 +706,27 @@
     return parseSha1(commit, FOOTER_THEIRS);
   }
 
+  private Optional<String> parseMergeStrategy(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
+    return Optional.ofNullable(parseOneFooter(commit, FOOTER_MERGE_STRATEGY));
+  }
+
+  private Optional<NoMergeBaseReason> parseNoBaseReason(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
+    String noBaseReasonFooter = parseOneFooter(commit, FOOTER_NO_BASE_REASON);
+
+    if (noBaseReasonFooter == null) {
+      return Optional.empty();
+    }
+
+    NoMergeBaseReason noBaseReason =
+        Enums.getIfPresent(NoMergeBaseReason.class, noBaseReasonFooter).orNull();
+    if (noBaseReason == null) {
+      throw invalidFooter(FOOTER_NO_BASE_REASON, noBaseReasonFooter);
+    }
+    return Optional.of(noBaseReason);
+  }
+
   private Optional<ObjectId> parseSha1(ChangeNotesCommit commit, FooterKey footerKey)
       throws ConfigInvalidException {
     String sha = parseOneFooter(commit, footerKey);
@@ -720,6 +749,10 @@
       return Optional.empty();
     }
 
+    // base is missing if the patch sets has been created before Gerrit started to store the base
+    // for conflicts.
+    Optional<ObjectId> base = parseBase(commit);
+
     Optional<ObjectId> ours = parseOurs(commit);
     if (containsConflicts.get() && ours.isEmpty()) {
       throw parseException(
@@ -734,7 +767,20 @@
           FOOTER_THEIRS, FOOTER_CONTAINS_CONFLICTS.getName(), FOOTER_THEIRS);
     }
 
-    return Optional.of(PatchSet.Conflicts.create(ours, theirs, containsConflicts.get()));
+    // mergeStrategy is missing if the patch set has been created before Gerrit started to store the
+    // merge strategy for conflicts.
+    Optional<String> mergeStrategy = parseMergeStrategy(commit);
+
+    // noBaseReason is not set if a base is available or if the patch set has been created before
+    // Gerrit started to store the no base reasons for conflicts.
+    Optional<NoMergeBaseReason> noBaseReason = parseNoBaseReason(commit);
+    if (containsConflicts.get() && base.isEmpty() && noBaseReason.isEmpty()) {
+      noBaseReason = Optional.of(NoMergeBaseReason.HISTORIC_DATA_WITHOUT_BASE);
+    }
+
+    return Optional.of(
+        PatchSet.Conflicts.create(
+            base, ours, theirs, mergeStrategy, noBaseReason, containsConflicts.get()));
   }
 
   private Optional<Boolean> parseContainsConflicts(ChangeNotesCommit commit)
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index ae99363..c4878de 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -20,6 +20,7 @@
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BASE;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
@@ -31,6 +32,8 @@
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_MERGE_STRATEGY;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_NO_BASE_REASON;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_OURS;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
@@ -77,7 +80,6 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmissionId;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRequirementResult;
@@ -159,7 +161,6 @@
 
   private final NoteDbUpdateManager.Factory updateManagerFactory;
   private final ChangeDraftUpdate.ChangeDraftUpdateFactory draftUpdateFactory;
-  private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
   private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory;
   private final ServiceUserClassifier serviceUserClassifier;
   private final PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator;
@@ -201,7 +202,6 @@
   private Optional<String> cherryPickOf;
 
   private ChangeDraftUpdate draftUpdate;
-  private RobotCommentUpdate robotCommentUpdate;
   private DeleteCommentRewriter deleteCommentRewriter;
   private DeleteChangeMessageRewriter deleteChangeMessageRewriter;
   private List<SubmitRequirementResult> submitRequirementResults;
@@ -217,7 +217,6 @@
       @GerritPersonIdent PersonIdent serverIdent,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.ChangeDraftUpdateFactory draftUpdateFactory,
-      RobotCommentUpdate.Factory robotCommentUpdateFactory,
       DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
       ProjectCache projectCache,
       ServiceUserClassifier serviceUserClassifier,
@@ -231,7 +230,6 @@
         serverIdent,
         updateManagerFactory,
         draftUpdateFactory,
-        robotCommentUpdateFactory,
         deleteCommentRewriterFactory,
         serviceUserClassifier,
         patchSetApprovalUuidGenerator,
@@ -257,7 +255,6 @@
       @GerritPersonIdent PersonIdent serverIdent,
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.ChangeDraftUpdateFactory draftUpdateFactory,
-      RobotCommentUpdate.Factory robotCommentUpdateFactory,
       DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
       ServiceUserClassifier serviceUserClassifier,
       PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
@@ -270,7 +267,6 @@
     super(notes, user, serverIdent, noteUtil, when);
     this.updateManagerFactory = updateManagerFactory;
     this.draftUpdateFactory = draftUpdateFactory;
-    this.robotCommentUpdateFactory = robotCommentUpdateFactory;
     this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
     this.serviceUserClassifier = serviceUserClassifier;
     this.patchSetApprovalUuidGenerator = patchSetApprovalUuidGenerator;
@@ -419,12 +415,6 @@
     }
   }
 
-  public void putRobotComment(RobotComment c) {
-    verifyComment(c);
-    createRobotCommentUpdateIfNull();
-    robotCommentUpdate.putComment(c);
-  }
-
   public void deleteComment(HumanComment c) {
     verifyComment(c);
     createDraftUpdateIfNull().addDraftCommentForDeletion(c);
@@ -456,20 +446,6 @@
     return draftUpdate;
   }
 
-  private void createRobotCommentUpdateIfNull() {
-    if (robotCommentUpdate == null) {
-      ChangeNotes notes = getNotes();
-      if (notes != null) {
-        robotCommentUpdate =
-            robotCommentUpdateFactory.create(notes, accountId, realAccountId, authorIdent, when);
-      } else {
-        robotCommentUpdate =
-            robotCommentUpdateFactory.create(
-                getChange(), accountId, realAccountId, authorIdent, when);
-      }
-    }
-  }
-
   public void setTopic(String topic, TopicValidator validator) throws ValidationException {
     validator.validateSize(topic);
     if (isIllegalTopic(topic)) {
@@ -530,7 +506,7 @@
    */
   public void addToPlannedAttentionSetUpdates(Set<AttentionSetUpdate> updates) {
     if (updates == null || updates.isEmpty() || ignoreFurtherAttentionSetUpdates) {
-      // No updates to do. Robots don't change attention set.
+      // No updates to do. Service users don't change attention set.
       return;
     }
     checkArgument(
@@ -896,11 +872,18 @@
     }
 
     if (conflicts != null) {
+      conflicts.base().map(ObjectId::getName).ifPresent(base -> addFooter(msg, FOOTER_BASE, base));
       conflicts.ours().map(ObjectId::getName).ifPresent(ours -> addFooter(msg, FOOTER_OURS, ours));
       conflicts
           .theirs()
           .map(ObjectId::getName)
           .ifPresent(theirs -> addFooter(msg, FOOTER_THEIRS, theirs));
+      conflicts
+          .mergeStrategy()
+          .ifPresent(mergeStrategy -> addFooter(msg, FOOTER_MERGE_STRATEGY, mergeStrategy));
+      conflicts
+          .noBaseReason()
+          .ifPresent(noBaseReason -> addFooter(msg, FOOTER_NO_BASE_REASON, noBaseReason));
       addFooter(msg, FOOTER_CONTAINS_CONFLICTS, conflicts.containsConflicts());
     }
 
@@ -1023,7 +1006,7 @@
         || getNotes().getChange().isWorkInProgress()
         || status == Change.Status.MERGED) {
       // Attention set shouldn't change here for changes that are work in progress or are about to
-      // be submitted or when the caller is a robot.
+      // be submitted or when the caller is a service user.
       return;
     }
 
@@ -1122,7 +1105,7 @@
 
       if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
           && serviceUserClassifier.isServiceUser(attentionSetUpdate.account())) {
-        // Skip adding robots to the attention set.
+        // Skip adding service users to the attention set.
         continue;
       }
 
@@ -1247,10 +1230,6 @@
     return draftUpdate;
   }
 
-  RobotCommentUpdate getRobotCommentUpdate() {
-    return robotCommentUpdate;
-  }
-
   DeleteCommentRewriter getDeleteCommentRewriter() {
     return deleteCommentRewriter;
   }
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index ead2e15..895ae9d 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -1178,7 +1178,7 @@
       return Optional.of(AccountTemplateUtil.getAccountTemplate(account.get()));
     }
     // Retrieve reviewer accounts from cache and try to match by their name.
-    Map<Account.Id, AccountState> missingAccountStateReviewers =
+    ImmutableMap<Account.Id, AccountState> missingAccountStateReviewers =
         accountCache.get(
             changeFixProgress.parsedAccounts.entrySet().stream()
                 .filter(entry -> !entry.getValue().isPresent())
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 6a09f0f2..e0fc648 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -197,24 +197,34 @@
    * Git and not get deleted. These refs point to an empty tree. We delete such refs.
    */
   @Override
-  protected void deleteEmptyDraftsByKey(Collection<Ref> refs) throws IOException {
+  protected void deleteEmptyDraftsByKey(Collection<Ref> refs) {
     long zombieRefsCnt = refs.size();
     long deletedRefsCnt = 0;
     long startTime = System.currentTimeMillis();
 
     for (List<Ref> refsBatch : Iterables.partition(refs, CHUNK_SIZE)) {
-      deleteZombieDraftsBatch(refsBatch);
-      long elapsed = (System.currentTimeMillis() - startTime) / 1000;
-      deletedRefsCnt += refsBatch.size();
-      logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
+      try {
+        deleteZombieDraftsBatch(refsBatch);
+        long elapsed = (System.currentTimeMillis() - startTime) / 1000;
+        deletedRefsCnt += refsBatch.size();
+        logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
+      } catch (Exception error) {
+        logger.atWarning().withCause(error).log("Failed to delete drafts as a batch");
+      }
     }
   }
 
   @Override
-  protected void deleteZombieDrafts(ListMultimap<Ref, HumanComment> drafts) throws IOException {
+  protected void deleteZombieDrafts(ListMultimap<Ref, HumanComment> drafts) {
     for (Map.Entry<Ref, Collection<HumanComment>> e : drafts.asMap().entrySet()) {
-      deleteZombieDraftsForChange(
-          getAccountId(e.getKey()), getChangeNotes(getChangeId(e.getKey())), e.getValue());
+      try {
+        deleteZombieDraftsForChange(
+            getAccountId(e.getKey()), getChangeNotes(getChangeId(e.getKey())), e.getValue());
+      } catch (Exception error) {
+        logger.atWarning().withCause(error).log(
+            "Failed to delete draft for change %s account %s",
+            getChangeId(e.getKey()), getAccountId(e.getKey()));
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
index ec5a83a..b8ac233 100644
--- a/java/com/google/gerrit/server/notedb/IntBlob.java
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -93,8 +93,6 @@
           val, refName, projectName, oldId == null ? "null" : oldId.name());
       newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
       ins.flush();
-      logger.atFine().log(
-          "successfully stored %d on %s as %s in %s", val, refName, newId.name(), projectName);
     }
     RefUpdate ru = repo.updateRef(refName);
     if (oldId != null) {
@@ -105,6 +103,8 @@
     ru.setForceUpdate(true); // Required for non-commitish updates.
     RefUpdate.Result result = ru.update(rw);
     if (refUpdated(result)) {
+      logger.atFine().log(
+          "successfully stored %d on %s as %s in %s", val, refName, newId.name(), projectName);
       gitRefUpdated.fire(projectName, ru, null);
     }
     return ru;
diff --git a/java/com/google/gerrit/server/notedb/NoteDbModule.java b/java/com/google/gerrit/server/notedb/NoteDbModule.java
index cf3fad6..6062577 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -40,8 +40,6 @@
     factory(ChangeUpdate.Factory.class);
     factory(DeleteCommentRewriter.Factory.class);
     factory(NoteDbUpdateManager.Factory.class);
-    factory(RobotCommentNotes.Factory.class);
-    factory(RobotCommentUpdate.Factory.class);
 
     if (!useTestBindings) {
       install(ChangeNotesCache.module());
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index ec5c448..ef24a2f 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -95,7 +95,6 @@
   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
   private final NoteDbUpdateExecutor noteDbUpdateExecutor;
   private final ChangeDraftUpdateExecutor.AbstractFactory draftUpdatesExecutorFactory;
-  private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
   private final ListMultimap<String, NoteDbRewriter> rewriters;
   private final Set<Change.Id> changesToDelete;
 
@@ -128,7 +127,6 @@
     this.currentUser = currentUser;
     changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
-    robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     rewriters = MultimapBuilder.hashKeys().arrayListValues().build();
     changesToDelete = new HashSet<>();
     batchUpdateListeners = ImmutableList.of();
@@ -204,7 +202,6 @@
   private boolean isEmpty() {
     return changeUpdates.isEmpty()
         && draftUpdates.isEmpty()
-        && robotCommentUpdates.isEmpty()
         && rewriters.isEmpty()
         && changesToDelete.isEmpty()
         && !hasCommands(changeRepo)
@@ -239,10 +236,6 @@
     if (du != null) {
       draftUpdates.put(du.getStorageKey(), du);
     }
-    RobotCommentUpdate rcu = update.getRobotCommentUpdate();
-    if (rcu != null) {
-      robotCommentUpdates.put(rcu.getRefName(), rcu);
-    }
     DeleteCommentRewriter deleteCommentRewriter = update.getDeleteCommentRewriter();
     if (deleteCommentRewriter != null) {
       // Checks whether there is any ChangeUpdate or rewriter added earlier for the same ref.
@@ -380,9 +373,6 @@
     if (!draftUpdates.isEmpty()) {
       draftUpdatesExecutor.queueAllDraftUpdates(draftUpdates);
     }
-    if (!robotCommentUpdates.isEmpty()) {
-      changeRepo.addUpdatesNoLimits(robotCommentUpdates);
-    }
     if (!rewriters.isEmpty()) {
       addRewrites(rewriters, changeRepo);
     }
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 36a508d..5087b91 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -457,12 +457,13 @@
 
   private void checkIsIncremental(int value) {
     if (lastStoredSequence != null && value <= lastStoredSequence) {
-      String msg =
+      logger.atWarning().log(
+          "For %s, expected new value %d to be greater than last stored value %d",
+          refName, value, lastStoredSequence);
+      throw new NonIncrementingSequenceException(
           String.format(
               "For %s, expected new value %d to be greater than last stored value %d",
-              refName, value, lastStoredSequence);
-      logger.atWarning().log(msg);
-      throw new NonIncrementingSequenceException(msg);
+              refName, value, lastStoredSequence));
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteData.java b/java/com/google/gerrit/server/notedb/RevisionNoteData.java
index c8770f1..e3aabf0 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteData.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteData.java
@@ -21,8 +21,8 @@
 /**
  * Holds the raw data of a RevisionNote.
  *
- * <p>It is intended for serialization to JSON only. It is used for human comments and robot
- * comments, as well as for storing submit requirements.
+ * <p>It is intended for serialization to JSON only. It is used for human comments, as well as for
+ * storing submit requirements.
  */
 class RevisionNoteData {
   String pushCert;
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index 98c9873..9f808d3 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -53,19 +53,6 @@
     return new RevisionNoteMap<>(noteMap, result.build());
   }
 
-  static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments(
-      ChangeNoteJson changeNoteJson, ObjectReader reader, NoteMap noteMap)
-      throws ConfigInvalidException, IOException {
-    ImmutableMap.Builder<ObjectId, RobotCommentsRevisionNote> result = ImmutableMap.builder();
-    for (Note note : noteMap) {
-      RobotCommentsRevisionNote rn =
-          new RobotCommentsRevisionNote(changeNoteJson, reader, note.getData());
-      rn.parse();
-      result.put(note.copy(), rn);
-    }
-    return new RevisionNoteMap<>(noteMap, result.build());
-  }
-
   static <T extends RevisionNote<? extends Comment>> RevisionNoteMap<T> emptyMap() {
     return new RevisionNoteMap<>(NoteMap.newEmptyMap(), ImmutableMap.of());
   }
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
deleted file mode 100644
index 2ec68f1..0000000
--- a/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.entities.RobotComment;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevCommit;
-
-public class RobotCommentNotes extends AbstractChangeNotes<RobotCommentNotes> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public interface Factory {
-    RobotCommentNotes create(Change change);
-  }
-
-  private final Change change;
-
-  private ImmutableListMultimap<ObjectId, RobotComment> comments;
-  private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
-  private ObjectId metaId;
-
-  @Inject
-  RobotCommentNotes(Args args, @Assisted Change change) {
-    super(args, change.getId(), null);
-    this.change = change;
-  }
-
-  RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap() {
-    return revisionNoteMap;
-  }
-
-  public ImmutableListMultimap<ObjectId, RobotComment> getComments() {
-    return comments;
-  }
-
-  public boolean containsComment(RobotComment c) {
-    for (RobotComment existing : comments.values()) {
-      if (c.key.equals(existing.key)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public String getRefName() {
-    return RefNames.robotCommentsRef(getChangeId());
-  }
-
-  @Nullable
-  public ObjectId getMetaId() {
-    return metaId;
-  }
-
-  @Override
-  protected void onLoad(LoadHandle handle) throws IOException, ConfigInvalidException {
-    metaId = handle.id();
-    if (metaId == null) {
-      loadDefaults();
-      return;
-    }
-    metaId = metaId.copy();
-
-    logger.atFine().log(
-        "Load robot comment notes for change %s of project %s", getChangeId(), getProjectName());
-    RevCommit tipCommit = handle.walk().parseCommit(metaId);
-    ObjectReader reader = handle.walk().getObjectReader();
-    revisionNoteMap =
-        RevisionNoteMap.parseRobotComments(
-            args.changeNoteJson, reader, NoteMap.read(reader, tipCommit));
-    ImmutableListMultimap.Builder<ObjectId, RobotComment> cs = ImmutableListMultimap.builder();
-    for (RobotCommentsRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
-      for (RobotComment c : rn.getEntities()) {
-        cs.put(c.getCommitId(), c);
-      }
-    }
-    comments = cs.build();
-  }
-
-  @Override
-  protected void loadDefaults() {
-    comments = ImmutableListMultimap.of();
-  }
-
-  @Override
-  public Project.NameKey getProjectName() {
-    return change.getProject();
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
deleted file mode 100644
index e1e6305..0000000
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.gerrit.entities.RefNames.robotCommentsRef;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
-import java.io.IOException;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-/**
- * A single delta to apply atomically to a change.
- *
- * <p>This delta contains only robot comments on a single patch set of a change by a single author.
- * This delta will become a single commit in the repository.
- *
- * <p>This class is not thread safe.
- */
-public class RobotCommentUpdate extends AbstractChangeUpdate {
-  public interface Factory {
-    RobotCommentUpdate create(
-        ChangeNotes notes,
-        @Assisted("effective") Account.Id accountId,
-        @Assisted("real") Account.Id realAccountId,
-        PersonIdent authorIdent,
-        Instant when);
-
-    RobotCommentUpdate create(
-        Change change,
-        @Assisted("effective") Account.Id accountId,
-        @Assisted("real") Account.Id realAccountId,
-        PersonIdent authorIdent,
-        Instant when);
-  }
-
-  private List<RobotComment> put = new ArrayList<>();
-
-  @SuppressWarnings("UnusedMethod")
-  @AssistedInject
-  private RobotCommentUpdate(
-      @GerritPersonIdent PersonIdent serverIdent,
-      ChangeNoteUtil noteUtil,
-      @Assisted ChangeNotes notes,
-      @Assisted("effective") Account.Id accountId,
-      @Assisted("real") Account.Id realAccountId,
-      @Assisted PersonIdent authorIdent,
-      @Assisted Instant when) {
-    super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
-  }
-
-  @SuppressWarnings("UnusedMethod")
-  @AssistedInject
-  private RobotCommentUpdate(
-      @GerritPersonIdent PersonIdent serverIdent,
-      ChangeNoteUtil noteUtil,
-      @Assisted Change change,
-      @Assisted("effective") Account.Id accountId,
-      @Assisted("real") Account.Id realAccountId,
-      @Assisted PersonIdent authorIdent,
-      @Assisted Instant when) {
-    super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
-  }
-
-  public void putComment(RobotComment c) {
-    verifyComment(c);
-    put.add(c);
-  }
-
-  @Nullable
-  private CommitBuilder storeCommentsInNotes(
-      RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
-      throws ConfigInvalidException, IOException {
-    RevisionNoteMap<RobotCommentsRevisionNote> rnm = getRevisionNoteMap(rw, curr);
-    Set<ObjectId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
-    RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
-
-    for (RobotComment c : put) {
-      cache.get(c.getCommitId()).putComment(c);
-    }
-
-    Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
-    boolean touchedAnyRevs = false;
-    boolean hasComments = false;
-    for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
-      ObjectId id = e.getKey();
-      updatedRevs.add(id);
-      byte[] data = e.getValue().build(noteUtil);
-      if (!Arrays.equals(data, e.getValue().baseRaw)) {
-        touchedAnyRevs = true;
-      }
-      if (data.length == 0) {
-        rnm.noteMap.remove(id);
-      } else {
-        hasComments = true;
-        ObjectId dataBlob = ins.insert(OBJ_BLOB, data);
-        rnm.noteMap.set(id, dataBlob);
-      }
-    }
-
-    // If we didn't touch any notes, tell the caller this was a no-op update. We
-    // couldn't have done this in isEmpty() below because we hadn't read the old
-    // data yet.
-    if (!touchedAnyRevs) {
-      return NO_OP_UPDATE;
-    }
-
-    // If we touched every revision and there are no comments left, tell the
-    // caller to delete the entire ref.
-    boolean touchedAllRevs = updatedRevs.equals(rnm.revisionNotes.keySet());
-    if (touchedAllRevs && !hasComments) {
-      return null;
-    }
-
-    cb.setTreeId(rnm.noteMap.writeTree(ins));
-    return cb;
-  }
-
-  private RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
-      throws ConfigInvalidException, IOException {
-    if (curr.equals(ObjectId.zeroId())) {
-      return RevisionNoteMap.emptyMap();
-    }
-    // The old RobotCommentNotes already parsed the revision notes. We can reuse them as long as
-    // the ref hasn't advanced.
-    ChangeNotes changeNotes = getNotes();
-    if (changeNotes != null) {
-      RobotCommentNotes robotCommentNotes = changeNotes.load().getRobotCommentNotes();
-      if (robotCommentNotes != null) {
-        ObjectId idFromNotes = firstNonNull(robotCommentNotes.getRevision(), ObjectId.zeroId());
-        RevisionNoteMap<RobotCommentsRevisionNote> rnm = robotCommentNotes.getRevisionNoteMap();
-        if (idFromNotes.equals(curr) && rnm != null) {
-          return rnm;
-        }
-      }
-    }
-    NoteMap noteMap;
-    if (!curr.equals(ObjectId.zeroId())) {
-      noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
-    } else {
-      noteMap = NoteMap.newEmptyMap();
-    }
-    // Even though reading from changes might not be enabled, we need to
-    // parse any existing revision notes so we can merge them.
-    return RevisionNoteMap.parseRobotComments(
-        noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap);
-  }
-
-  @Override
-  protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
-      throws IOException {
-    CommitBuilder cb = new CommitBuilder();
-    cb.setMessage("Update robot comments");
-    try {
-      return storeCommentsInNotes(rw, ins, curr, cb);
-    } catch (ConfigInvalidException e) {
-      throw new StorageException(e);
-    }
-  }
-
-  @Override
-  protected Project.NameKey getProjectName() {
-    return getNotes().getProjectName();
-  }
-
-  @Override
-  protected String getRefName() {
-    return robotCommentsRef(getId());
-  }
-
-  @Override
-  public boolean isEmpty() {
-    return put.isEmpty();
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
deleted file mode 100644
index 010206c..0000000
--- a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.entities.RobotComment;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.util.List;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
-
-/**
- * Holds the raw data of a RevisionNote.
- *
- * <p>It is intended for deserialization from JSON only. It is used for robot comments only.
- */
-public class RobotCommentsRevisionNote extends RevisionNote<RobotComment> {
-  private final ChangeNoteJson noteUtil;
-
-  RobotCommentsRevisionNote(ChangeNoteJson noteUtil, ObjectReader reader, ObjectId noteId) {
-    super(reader, noteId);
-    this.noteUtil = noteUtil;
-  }
-
-  @Override
-  protected List<RobotComment> parse(byte[] raw, int offset) throws IOException {
-    try (InputStream is = new ByteArrayInputStream(raw, offset, raw.length - offset);
-        Reader r = new InputStreamReader(is, UTF_8)) {
-      return noteUtil.getGson().fromJson(r, RobotCommentsRevisionNoteData.class).comments;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java b/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
deleted file mode 100644
index 3619c43..0000000
--- a/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import com.google.gerrit.entities.RobotComment;
-import java.util.List;
-
-public class RobotCommentsRevisionNoteData {
-  List<RobotComment> comments;
-}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index b5f3bc4..d476bd7 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -19,6 +19,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.GerritNoMergeBaseException;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.InMemoryInserter;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.MergeUtil.MergeBase;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.update.RepoView;
 import com.google.inject.Inject;
@@ -36,6 +38,7 @@
 import java.io.IOException;
 import java.util.Optional;
 import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -44,6 +47,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.merge.StrategyRecursive;
 import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -242,11 +246,17 @@
         ins instanceof InMemoryInserter ? ins : new NonFlushingWrapper(ins);
 
     rw.parseHeaders(merge);
+    rw.parseBody(merge);
     ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(nonFlushingInserter, repoConfig);
     DirCache dc = DirCache.newInCore();
     m.setDirCache(dc);
 
-    boolean couldMerge = m.merge(merge.getParents());
+    boolean couldMerge;
+    try {
+      couldMerge = m.merge(merge.getParents());
+    } catch (NoMergeBaseException e) {
+      throw new GerritNoMergeBaseException(e);
+    }
 
     ObjectId treeId;
     if (couldMerge) {
@@ -281,6 +291,11 @@
               rw,
               nonFlushingInserter,
               dc,
+              "BASE",
+              MergeBase.create(
+                  rw,
+                  mergeStrategy instanceof StrategyRecursive ? "recursive" : "resolve",
+                  m.getBaseCommitId()),
               "HEAD",
               merge.getParent(0),
               "BRANCH",
@@ -291,7 +306,6 @@
           "AutoMerge treeId=%s (with conflicts, inserter: %s)", treeId.name(), nonFlushingInserter);
     }
 
-    rw.parseHeaders(merge);
     // For maximum stability, choose a single ident using the committer time of
     // the input commit, using the server name and timezone.
     PersonIdent ident =
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 21f3e7c..392bf9d 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -246,6 +246,8 @@
 
   private ImmutableMap<String, FileDiffOutput> getModifiedFiles(
       DiffParameters diffParams, DiffOptions diffOptions) throws DiffNotAvailableException {
+    logger.atFine().log(
+        "getModifiedFiles (diffParams: %s, diffOptions: %s)", diffParams, diffOptions);
     try {
       Project.NameKey project = diffParams.project();
       ObjectId newCommit = diffParams.newCommit();
@@ -431,6 +433,9 @@
       Config repoConfig,
       boolean enableRenameDetection)
       throws DiffNotAvailableException {
+    logger.atFine().log(
+        "loadModifiedFilesWithoutCacheIfNecessary (diffParams: %s, enableRenameDetection: %s)",
+        diffParams, enableRenameDetection);
     ModifiedFilesCacheKey.Builder cacheKeyBuilder =
         ModifiedFilesCacheKey.builder()
             .project(project)
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
index 022614a..8f06019 100644
--- a/java/com/google/gerrit/server/patch/DiffUtil.java
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -55,6 +55,9 @@
    */
   public static ImmutableList<ModifiedFile> mergeRewrittenModifiedFiles(
       List<ModifiedFile> modifiedFiles) {
+    if (modifiedFiles == null) {
+      return ImmutableList.of();
+    }
     ImmutableList.Builder<ModifiedFile> result = ImmutableList.builder();
     ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
     modifiedFiles.stream()
@@ -149,6 +152,25 @@
 
   public static void getFormattedDiff(
       Repository repo,
+      RevCommit baseCommit,
+      RevCommit childCommit,
+      @Nullable String path,
+      OutputStream out,
+      int context)
+      throws IOException {
+    try (DiffFormatter fmt = new DiffFormatter(out)) {
+      fmt.setRepository(repo);
+      fmt.setContext(context);
+      if (path != null) {
+        fmt.setPathFilter(PathFilter.create(path));
+      }
+      fmt.format(baseCommit, childCommit);
+      fmt.flush();
+    }
+  }
+
+  public static void getFormattedDiff(
+      Repository repo,
       @Nullable ObjectReader reader,
       RevTree baseTree,
       RevTree childTree,
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java b/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
index 0abb77b..fc5080f 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactoryForAutoFix.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.git.LargeObjectException;
@@ -96,6 +97,7 @@
   public PatchScript call()
       throws LargeObjectException,
           AuthException,
+          BadRequestException,
           InvalidChangeOperationException,
           IOException,
           PermissionBackendException,
@@ -115,15 +117,17 @@
     return createPatchScript();
   }
 
-  private PatchScript createPatchScript() throws LargeObjectException, ResourceNotFoundException {
+  private PatchScript createPatchScript()
+      throws LargeObjectException, BadRequestException, ResourceNotFoundException {
     checkState(patchSet.id().get() != 0, "edit not supported for left side");
     PatchScriptBuilder b = newBuilder();
     try {
       ObjectId baseId = patchSet.commitId();
       return b.toPatchScript(git, baseId, fileName, fixReplacements);
     } catch (ResourceConflictException e) {
-      logger.atSevere().withCause(e).log("AutoFix replacements is not valid");
-      throw new IllegalStateException("AutoFix replacements is not valid", e);
+      logger.atFine().log("replacement is not valid: %s", e.getMessage());
+      throw new BadRequestException(
+          String.format("replacement is not valid: %s", e.getMessage()), e);
     } catch (IOException e) {
       logger.atSevere().withCause(e).log("File content unavailable");
       throw new NoSuchChangeException(notes.getChangeId(), e);
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index 82d7e93..d417fe3 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -70,6 +70,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.sha1.SHA1;
 
 /**
  * Cache for the single file diff between two commits for a single file path. This cache adds extra
@@ -97,7 +98,7 @@
         persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
             .maximumWeight(10 << 20)
             .weigher(FileDiffWeigher.class)
-            .version(9)
+            .version(10)
             .keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
             .valueSerializer(FileDiffOutput.Serializer.INSTANCE)
             .loader(FileDiffLoader.class);
@@ -334,6 +335,12 @@
       byte[] rawHdr = getRawHeader(!comparisonType.isAgainstParentOrAutoMerge(), fileName);
       byte[] aContent = aText.getContent();
       byte[] bContent = bText.getContent();
+      SHA1 aContentDigest = SHA1.newInstance();
+      aContentDigest.update(aContent);
+      ObjectId aSha = ObjectId.fromRaw(aContentDigest.digest());
+      SHA1 bContentDigest = SHA1.newInstance();
+      bContentDigest.update(bContent);
+      ObjectId bSha = ObjectId.fromRaw(aContentDigest.digest());
       long size = bContent.length;
       long sizeDelta = size - aContent.length;
       RawText aRawText = new RawText(aContent);
@@ -348,6 +355,8 @@
           .comparisonType(comparisonType)
           .oldPath(FileHeaderUtil.getOldPath(fileHeader))
           .newPath(FileHeaderUtil.getNewPath(fileHeader))
+          .oldSha(Optional.of(aSha))
+          .newSha(Optional.of(bSha))
           .changeType(changeType)
           .patchType(Optional.of(FileHeaderUtil.getPatchType(fileHeader)))
           .headerLines(FileHeaderUtil.getHeaderLines(fileHeader))
@@ -415,22 +424,20 @@
         RevTree aTree = oldTreeId.equals(ObjectId.zeroId()) ? null : rw.parseTree(oldTreeId);
         RevTree bTree = rw.parseTree(allDiffs.mainDiff().gitKey().newTree());
 
-        Long oldSize =
+        FileSizeEvaluator aEvaluator = new FileSizeEvaluator(reader, aTree);
+        ObjectId oldSha =
             aTree != null && mainGitDiff.oldMode().isPresent() && mainGitDiff.oldPath().isPresent()
-                ? new FileSizeEvaluator(reader, aTree)
-                    .compute(
-                        mainGitDiff.oldId(),
-                        mainGitDiff.oldMode().get(),
-                        mainGitDiff.oldPath().get())
-                : 0;
-        Long newSize =
+                ? aEvaluator.getFileObjectId(
+                    mainGitDiff.oldId(), mainGitDiff.oldMode().get(), mainGitDiff.oldPath().get())
+                : ObjectId.zeroId();
+        Long oldSize = aEvaluator.compute(oldSha);
+        FileSizeEvaluator bEvaluator = new FileSizeEvaluator(reader, bTree);
+        ObjectId newSha =
             mainGitDiff.newMode().isPresent() && mainGitDiff.newPath().isPresent()
-                ? new FileSizeEvaluator(reader, bTree)
-                    .compute(
-                        mainGitDiff.newId(),
-                        mainGitDiff.newMode().get(),
-                        mainGitDiff.newPath().get())
-                : 0;
+                ? bEvaluator.getFileObjectId(
+                    mainGitDiff.newId(), mainGitDiff.newMode().get(), mainGitDiff.newPath().get())
+                : ObjectId.zeroId();
+        Long newSize = bEvaluator.compute(newSha);
 
         ObjectId oldCommit = augmentedKey.key().oldCommit();
         ObjectId newCommit = augmentedKey.key().newCommit();
@@ -445,6 +452,8 @@
                 .newPath(mainGitDiff.newPath())
                 .oldMode(mainGitDiff.oldMode())
                 .newMode(mainGitDiff.newMode())
+                .oldSha(!oldSha.equals(ObjectId.zeroId()) ? Optional.of(oldSha) : Optional.empty())
+                .newSha(!newSha.equals(ObjectId.zeroId()) ? Optional.of(newSha) : Optional.empty())
                 .headerLines(FileHeaderUtil.getHeaderLines(mainGitDiff.fileHeader()))
                 .edits(asTaggedEdits(mainGitDiff.edits(), rebaseEdits))
                 .size(newSize)
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
index b7cd5e4..dd6b8d9 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
@@ -38,7 +38,7 @@
 /** File diff for a single file path. Produced as output of the {@link FileDiffCache}. */
 @AutoValue
 public abstract class FileDiffOutput implements Serializable {
-  private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 2L;
 
   /**
    * The 20 bytes SHA-1 object ID of the old git commit used in the diff, or {@link
@@ -80,6 +80,20 @@
    */
   public abstract Optional<Patch.FileMode> newMode();
 
+  /**
+   * The SHA-1 of the old file at the old git tree diff identified by {@link #oldCommitId()} ()}.
+   *
+   * <p>Not populated if diffing was not performed (ie. error/timeout) or if the file is added.
+   */
+  public abstract Optional<ObjectId> oldSha();
+
+  /**
+   * The SHA-1 of the new file at the new git tree diff identified by {@link #newCommitId()} ()}.
+   *
+   * <p>Not populated if diffing was not performed (ie. error/timeout) or if the file is deleted.
+   */
+  public abstract Optional<ObjectId> newSha();
+
   /** The change type of the underlying file, e.g. added, deleted, renamed, etc... */
   public abstract Patch.ChangeType changeType();
 
@@ -223,6 +237,10 @@
 
     public abstract Builder newMode(Optional<Patch.FileMode> newMode);
 
+    public abstract Builder oldSha(Optional<ObjectId> oldSha);
+
+    public abstract Builder newSha(Optional<ObjectId> newSha);
+
     public abstract Builder changeType(ChangeType value);
 
     public abstract Builder patchType(Optional<PatchType> value);
@@ -264,6 +282,12 @@
     private static final FieldDescriptor NEW_MODE_DESCRIPTOR =
         FileDiffOutputProto.getDescriptor().findFieldByNumber(14);
 
+    private static final FieldDescriptor OLD_SHA_DESCRIPTOR =
+        FileDiffOutputProto.getDescriptor().findFieldByNumber(15);
+
+    private static final FieldDescriptor NEW_SHA_DESCRIPTOR =
+        FileDiffOutputProto.getDescriptor().findFieldByNumber(16);
+
     @Override
     public byte[] serialize(FileDiffOutput fileDiff) {
       ObjectIdConverter idConverter = ObjectIdConverter.create();
@@ -315,6 +339,14 @@
         builder.setNewMode(FILE_MODE_CONVERTER.reverse().convert(fileDiff.newMode().get()));
       }
 
+      if (fileDiff.oldSha().isPresent()) {
+        builder.setOldSha(idConverter.toByteString(fileDiff.oldSha().get()));
+      }
+
+      if (fileDiff.newSha().isPresent()) {
+        builder.setNewSha(idConverter.toByteString(fileDiff.newSha().get()));
+      }
+
       return Protos.toByteArray(builder.build());
     }
 
@@ -362,6 +394,12 @@
       if (proto.hasField(NEW_MODE_DESCRIPTOR)) {
         builder.newMode(Optional.of(FILE_MODE_CONVERTER.convert(proto.getNewMode())));
       }
+      if (proto.hasField(OLD_SHA_DESCRIPTOR)) {
+        builder.oldSha(Optional.of(idConverter.fromByteString(proto.getOldSha())));
+      }
+      if (proto.hasField(NEW_SHA_DESCRIPTOR)) {
+        builder.newSha(Optional.of(idConverter.fromByteString(proto.getNewSha())));
+      }
       return builder.build();
     }
   }
diff --git a/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java b/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java
index e2c1bc5..77c022e 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java
@@ -41,15 +41,22 @@
   }
 
   /**
+   * Computes the file ObjectId (SHA-1) identified by the {@code path} parameter at the given git
+   * tree identified by {@code gitTreeId}.
+   */
+  ObjectId getFileObjectId(AbbreviatedObjectId gitTreeId, Patch.FileMode mode, String path)
+      throws IOException {
+    if (!isBlob(mode)) {
+      return ObjectId.zeroId();
+    }
+    return toObjectId(reader, gitTreeId).orElseGet(() -> lookupObjectId(reader, path, tree));
+  }
+
+  /**
    * Computes the file size identified by the {@code path} parameter at the given git tree
    * identified by {@code gitTreeId}.
    */
-  long compute(AbbreviatedObjectId gitTreeId, Patch.FileMode mode, String path) throws IOException {
-    if (!isBlob(mode)) {
-      return 0;
-    }
-    ObjectId fileId =
-        toObjectId(reader, gitTreeId).orElseGet(() -> lookupObjectId(reader, path, tree));
+  long compute(ObjectId fileId) throws IOException {
     if (ObjectId.zeroId().equals(fileId)) {
       return 0;
     }
diff --git a/java/com/google/gerrit/server/permissions/AbstractChangeControl.java b/java/com/google/gerrit/server/permissions/AbstractChangeControl.java
new file mode 100644
index 0000000..bff1454
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/AbstractChangeControl.java
@@ -0,0 +1,312 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.PermissionRange;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Abstract access control management for a user accessing a single change.
+ *
+ * <p>Contains all logic that is common for checking permissions on an existing change and on a
+ * change that is not created yet.
+ */
+abstract class AbstractChangeControl {
+  protected final ProjectControl projectControl;
+  protected final RefControl refControl;
+  protected final boolean isNew;
+  protected final PermissionBackend permissionBackend;
+
+  /** Is this user the owner of the change? */
+  protected final boolean isOwner;
+
+  private Map<String, PermissionRange> labels;
+
+  AbstractChangeControl(
+      ProjectControl projectControl,
+      RefControl refControl,
+      PermissionBackend permissionBackend,
+      boolean isNew,
+      boolean isOwner) {
+    this.projectControl = projectControl;
+    this.refControl = refControl;
+    this.permissionBackend = permissionBackend;
+    this.isNew = isNew;
+    this.isOwner = isOwner;
+  }
+
+  protected abstract ForChange asForChange();
+
+  protected CurrentUser getUser() {
+    return refControl.getUser();
+  }
+
+  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");
+  }
+
+  protected boolean can(ChangePermission perm) throws PermissionBackendException {
+    try {
+      return switch (perm) {
+        case READ -> isVisible();
+        case ABANDON -> canAbandon();
+        case DELETE -> projectControl.isAdmin() || refControl.canDeleteChanges(isOwner);
+        case ADD_PATCH_SET -> canAddPatchSet();
+        case EDIT_DESCRIPTION -> canEditDescription();
+        case EDIT_HASHTAGS -> canEditHashtags();
+        case EDIT_CUSTOM_KEYED_VALUES -> canEditCustomKeyedValues();
+        case EDIT_TOPIC_NAME -> canEditTopicName();
+        case REBASE -> canRebase();
+        case REBASE_ON_BEHALF_OF_UPLOADER -> canRebaseOnBehalfOfUploader();
+        case RESTORE -> canRestore();
+        case REVERT -> canRevert();
+        case SUBMIT -> refControl.canSubmit(isOwner);
+        case TOGGLE_WORK_IN_PROGRESS_STATE -> canToggleWorkInProgressState();
+        case REMOVE_REVIEWER -> refControl.canPerform(changePermissionName(perm));
+        case SUBMIT_AS ->
+            permissionBackend.user(getUser()).test(GlobalPermission.RUN_AS)
+                || refControl.canPerform(changePermissionName(perm));
+      };
+    } catch (StorageException e) {
+      throw new PermissionBackendException("unavailable", e);
+    }
+  }
+
+  /** Can this user see this change? */
+  protected boolean isVisible() {
+    // Does the user have READ permission on the destination?
+    return refControl.asForRef().testOrFalse(RefPermission.READ);
+  }
+
+  /** Can this user abandon this change? */
+  private boolean canAbandon() {
+    return isOwner // owner (aka creator) of the change can abandon
+        || refControl.isOwner() // branch owner can abandon
+        || projectControl.isOwner() // project owner can abandon
+        || refControl.canPerform(Permission.ABANDON) // user can abandon a specific ref
+        || projectControl.isAdmin();
+  }
+
+  /** Can this user add a patch set to this change? */
+  private boolean canAddPatchSet() {
+    if (!refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)) {
+      return false;
+    }
+    if (isOwner) {
+      return true;
+    }
+    return refControl.canAddPatchSet();
+  }
+
+  /** Can this user edit the topic name? */
+  private boolean canEditTopicName() {
+    if (isNew) {
+      return isOwner // owner (aka creator) of the change can edit topic
+          || refControl.isOwner() // branch owner can edit topic
+          || projectControl.isOwner() // project owner can edit topic
+          || refControl.canPerform(
+              Permission.EDIT_TOPIC_NAME) // user can edit topic on a specific ref
+          || projectControl.isAdmin();
+    }
+    return refControl.canForceEditTopicName(isOwner);
+  }
+
+  /** Can this user edit the description? */
+  private boolean canEditDescription() {
+    if (isNew) {
+      return isOwner // owner (aka creator) of the change can edit desc
+          || refControl.isOwner() // branch owner can edit desc
+          || projectControl.isOwner() // project owner can edit desc
+          || projectControl.isAdmin();
+    }
+    return false;
+  }
+
+  /** Can this user edit the hashtag name? */
+  private boolean canEditHashtags() {
+    return isOwner // owner (aka creator) of the change can edit hashtags
+        || refControl.isOwner() // branch owner can edit hashtags
+        || projectControl.isOwner() // project owner can edit hashtags
+        || refControl.canPerform(
+            Permission.EDIT_HASHTAGS) // user can edit hashtag on a specific ref
+        || projectControl.isAdmin();
+  }
+
+  /** Can this user edit the custom keyed values? */
+  private boolean canEditCustomKeyedValues() {
+    return isOwner // owner (aka creator) of the change can edit custom keyed values
+        || projectControl.isAdmin();
+  }
+
+  /** Can this user rebase this change? */
+  private boolean canRebase() {
+    return (isOwner || refControl.canSubmit(isOwner) || refControl.canRebase())
+        && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
+  }
+
+  /**
+   * Can this user rebase this change on behalf of the uploader?
+   *
+   * <p>This only checks the permissions of the rebaser (aka the impersonating user).
+   *
+   * <p>In addition rebase on behalf of the uploader requires the uploader (aka the impersonated
+   * user) to have permissions to create the new patch set. These permissions need to be checked
+   * separately.
+   */
+  private boolean canRebaseOnBehalfOfUploader() {
+    return (isOwner || refControl.canSubmit(isOwner) || refControl.canRebase());
+  }
+
+  /** Can this user restore this change? */
+  private boolean canRestore() {
+    // Anyone who can abandon the change can restore it, as long as they can create changes.
+    return canAbandon() && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
+  }
+
+  /** Can this user revert this change? */
+  private boolean canRevert() {
+    return refControl.canRevert() && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
+  }
+
+  /** Can this user toggle WorkInProgress state? */
+  private boolean canToggleWorkInProgressState() {
+    return isOwner
+        || projectControl.isOwner()
+        || refControl.canPerform(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
+        || projectControl.isAdmin();
+  }
+
+  private boolean can(AbstractLabelPermission perm) {
+    return !label(labelPermissionName(perm)).isEmpty();
+  }
+
+  private boolean can(AbstractLabelPermission.WithValue perm) throws PermissionBackendException {
+    if (perm.forUser() == ON_BEHALF_OF
+        && permissionBackend.user(getUser()).test(GlobalPermission.RUN_AS)) {
+      return true;
+    }
+    PermissionRange r = label(labelPermissionName(perm));
+    if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
+      return false;
+    }
+    return r.contains(perm.value());
+  }
+
+  private PermissionRange label(String permission) {
+    if (labels == null) {
+      labels = Maps.newHashMapWithExpectedSize(4);
+    }
+    PermissionRange r = labels.get(permission);
+    if (r == null) {
+      r = getRange(permission);
+      labels.put(permission, r);
+    }
+    return r;
+  }
+
+  /** The range of permitted values associated with a label permission. */
+  private PermissionRange getRange(String permission) {
+    return refControl.getRange(permission, isOwner);
+  }
+
+  protected class ForChangeImpl extends ForChange {
+    @Nullable private final Change.Id changeId;
+
+    private String resourcePath;
+
+    protected ForChangeImpl(@Nullable Change.Id changeId) {
+      this.changeId = changeId;
+    }
+
+    @Override
+    public String resourcePath() {
+      if (resourcePath == null) {
+        resourcePath =
+            String.format(
+                "/projects/%s/+changes/%s",
+                projectControl.getProjectState().getName(), changeId != null ? changeId.get() : 0);
+      }
+      return resourcePath;
+    }
+
+    @Override
+    public void check(ChangePermissionOrLabel perm)
+        throws AuthException, PermissionBackendException {
+      if (!can(perm)) {
+        throw new AuthException(
+            perm.describeForException()
+                + " not permitted"
+                + perm.hintForException().map(hint -> " (" + hint + ")").orElse(""));
+      }
+    }
+
+    @Override
+    public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
+        throws PermissionBackendException {
+      Set<T> ok = newSet(permSet);
+      for (T perm : permSet) {
+        if (can(perm)) {
+          ok.add(perm);
+        }
+      }
+      return ok;
+    }
+
+    @Override
+    public BooleanCondition testCond(ChangePermissionOrLabel perm) {
+      return new PermissionBackendCondition.ForChange(this, perm, getUser());
+    }
+  }
+
+  protected static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
+    if (permSet instanceof EnumSet) {
+      @SuppressWarnings({"unchecked", "rawtypes"})
+      Set<T> s = ((EnumSet) permSet).clone();
+      s.clear();
+      return s;
+    }
+    return Sets.newHashSetWithExpectedSize(permSet.size());
+  }
+
+  protected static String changePermissionName(ChangePermission changePermission) {
+    // Within this class, it's programmer error to call this method on a
+    // ChangePermission that isn't associated with a permission name.
+    return DefaultPermissionMappings.changePermissionName(changePermission)
+        .orElseThrow(() -> new IllegalStateException("no name for " + changePermission));
+  }
+}
diff --git a/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
index e4a8160..ba7caed 100644
--- a/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
@@ -21,8 +21,7 @@
 import com.google.gerrit.server.util.LabelVote;
 
 /** Abstract permission representing a label. */
-public abstract class AbstractLabelPermission
-    implements ChangePermissionOrLabel, RefPermissionOrLabel {
+public abstract class AbstractLabelPermission implements ChangePermissionOrLabel {
   public enum ForUser {
     SELF,
     ON_BEHALF_OF
@@ -91,7 +90,7 @@
   }
 
   /** A {@link AbstractLabelPermission} at a specific value. */
-  public abstract static class WithValue implements ChangePermissionOrLabel, RefPermissionOrLabel {
+  public abstract static class WithValue implements ChangePermissionOrLabel {
     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 39a6310..2d2ec18 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -14,32 +14,18 @@
 
 package com.google.gerrit.server.permissions;
 
-import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
-import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
-
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Permission;
-import com.google.gerrit.entities.PermissionRange;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.conditions.BooleanCondition;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.assistedinject.Assisted;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.Map;
-import java.util.Set;
 import javax.inject.Inject;
 
 /** Access control management for a user accessing a single change. */
-public class ChangeControl {
+public class ChangeControl extends AbstractChangeControl {
   public interface Factory {
     ChangeControl create(
         ProjectControl projectControl, RefControl refControl, ChangeData changeData);
@@ -47,164 +33,44 @@
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final ProjectControl projectControl;
-  private final RefControl refControl;
   private final ChangeData changeData;
 
   @Inject
   protected ChangeControl(
+      PermissionBackend permissionBackend,
       @Assisted ProjectControl projectControl,
       @Assisted RefControl refControl,
       @Assisted ChangeData changeData) {
-    this.projectControl = projectControl;
-    this.refControl = refControl;
+    super(
+        projectControl,
+        refControl,
+        permissionBackend,
+        changeData.change().isNew(),
+        isOwner(refControl, changeData));
     this.changeData = changeData;
   }
 
-  ForChange asForChange() {
-    return new ForChangeImpl();
+  private static boolean isOwner(RefControl refControl, ChangeData changeData) {
+    CurrentUser user = refControl.getUser();
+    if (user.isIdentifiedUser()) {
+      Account.Id id = user.asIdentifiedUser().getAccountId();
+      return id.equals(changeData.change().getOwner());
+    }
+    return false;
   }
 
-  private CurrentUser getUser() {
-    return refControl.getUser();
-  }
-
-  private ProjectControl getProjectControl() {
-    return refControl.getProjectControl();
-  }
-
-  private Change getChange() {
-    return changeData.change();
+  @Override
+  public ForChange asForChange() {
+    return new ForChangeImpl(changeData.getId());
   }
 
   /** Can this user see this change? */
-  boolean isVisible() {
+  @Override
+  protected boolean isVisible() {
     if (changeData.isPrivateOrThrow() && !isPrivateVisible(changeData)) {
       return false;
     }
-    // Does the user have READ permission on the destination?
-    return refControl.asForRef().testOrFalse(RefPermission.READ);
-  }
-
-  /** Can this user abandon this change? */
-  private boolean canAbandon() {
-    return isOwner() // owner (aka creator) of the change can abandon
-        || refControl.isOwner() // branch owner can abandon
-        || getProjectControl().isOwner() // project owner can abandon
-        || refControl.canPerform(Permission.ABANDON) // user can abandon a specific ref
-        || getProjectControl().isAdmin();
-  }
-
-  /** Can this user rebase this change? */
-  private boolean canRebase() {
-    return (isOwner() || refControl.canSubmit(isOwner()) || refControl.canRebase())
-        && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
-  }
-
-  /**
-   * Can this user rebase this change on behalf of the uploader?
-   *
-   * <p>This only checks the permissions of the rebaser (aka the impersonating user).
-   *
-   * <p>In addition rebase on behalf of the uploader requires the uploader (aka the impersonated
-   * user) to have permissions to create the new patch set. These permissions need to be checked
-   * separately.
-   */
-  private boolean canRebaseOnBehalfOfUploader() {
-    return (isOwner() || refControl.canSubmit(isOwner()) || refControl.canRebase());
-  }
-
-  /** Can this user restore this change? */
-  private boolean canRestore() {
-    // Anyone who can abandon the change can restore it, as long as they can create changes.
-    return canAbandon() && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
-  }
-
-  /** Can this user revert this change? */
-  private boolean canRevert() {
-    return refControl.canRevert() && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
-  }
-
-  /** The range of permitted values associated with a label permission. */
-  private PermissionRange getRange(String permission) {
-    return refControl.getRange(permission, isOwner());
-  }
-
-  /** Can this user add a patch set to this change? */
-  private boolean canAddPatchSet() {
-    if (!refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE)) {
-      return false;
-    }
-    if (isOwner()) {
-      return true;
-    }
-    return refControl.canAddPatchSet();
-  }
-
-  /** Is this user the owner of the change? */
-  private boolean isOwner() {
-    if (getUser().isIdentifiedUser()) {
-      Account.Id id = getUser().asIdentifiedUser().getAccountId();
-      return id.equals(getChange().getOwner());
-    }
-    return false;
-  }
-
-  /** Is this user a reviewer for the change? */
-  private boolean isReviewer(ChangeData cd) {
-    if (getUser().isIdentifiedUser()) {
-      ImmutableSet<Account.Id> results = cd.reviewers().all();
-      return results.contains(getUser().getAccountId());
-    }
-    return false;
-  }
-
-  /** Can this user edit the topic name? */
-  private boolean canEditTopicName() {
-    if (getChange().isNew()) {
-      return isOwner() // owner (aka creator) of the change can edit topic
-          || refControl.isOwner() // branch owner can edit topic
-          || getProjectControl().isOwner() // project owner can edit topic
-          || refControl.canPerform(
-              Permission.EDIT_TOPIC_NAME) // user can edit topic on a specific ref
-          || getProjectControl().isAdmin();
-    }
-    return refControl.canForceEditTopicName(isOwner());
-  }
-
-  /** Can this user toggle WorkInProgress state? */
-  private boolean canToggleWorkInProgressState() {
-    return isOwner()
-        || getProjectControl().isOwner()
-        || refControl.canPerform(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
-        || getProjectControl().isAdmin();
-  }
-
-  /** Can this user edit the description? */
-  private boolean canEditDescription() {
-    if (getChange().isNew()) {
-      return isOwner() // owner (aka creator) of the change can edit desc
-          || refControl.isOwner() // branch owner can edit desc
-          || getProjectControl().isOwner() // project owner can edit desc
-          || getProjectControl().isAdmin();
-    }
-    return false;
-  }
-
-  /** Can this user edit the hashtag name? */
-  private boolean canEditHashtags() {
-    return isOwner() // owner (aka creator) of the change can edit hashtags
-        || refControl.isOwner() // branch owner can edit hashtags
-        || getProjectControl().isOwner() // project owner can edit hashtags
-        || refControl.canPerform(
-            Permission.EDIT_HASHTAGS) // user can edit hashtag on a specific ref
-        || getProjectControl().isAdmin();
-  }
-
-  /** Can this user edit the custom keyed values? */
-  private boolean canEditCustomKeyedValues() {
-    return isOwner() // owner (aka creator) of the change can edit custom keyed values
-        || getProjectControl().isAdmin();
+    return super.isVisible();
   }
 
   private boolean isPrivateVisible(ChangeData cd) {
@@ -215,7 +81,7 @@
       return true;
     }
 
-    if (isOwner()) {
+    if (isOwner) {
       logger.atFine().log(
           "%s can see private change %s because this user is the change owner",
           getUser().getLoggableName(), cd.getId());
@@ -247,125 +113,12 @@
     return false;
   }
 
-  private class ForChangeImpl extends ForChange {
-    private Map<String, PermissionRange> labels;
-    private String resourcePath;
-
-    private ForChangeImpl() {}
-
-    @Override
-    public String resourcePath() {
-      if (resourcePath == null) {
-        resourcePath =
-            String.format(
-                "/projects/%s/+changes/%s",
-                getProjectControl().getProjectState().getName(), changeData.getId().get());
-      }
-      return resourcePath;
+  /** Is this user a reviewer for the change? */
+  private boolean isReviewer(ChangeData cd) {
+    if (getUser().isIdentifiedUser()) {
+      ImmutableSet<Account.Id> results = cd.reviewers().all();
+      return results.contains(getUser().getAccountId());
     }
-
-    @Override
-    public void check(ChangePermissionOrLabel perm)
-        throws AuthException, PermissionBackendException {
-      if (!can(perm)) {
-        throw new AuthException(
-            perm.describeForException()
-                + " not permitted"
-                + perm.hintForException().map(hint -> " (" + hint + ")").orElse(""));
-      }
-    }
-
-    @Override
-    public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
-        throws PermissionBackendException {
-      Set<T> ok = newSet(permSet);
-      for (T perm : permSet) {
-        if (can(perm)) {
-          ok.add(perm);
-        }
-      }
-      return ok;
-    }
-
-    @Override
-    public BooleanCondition testCond(ChangePermissionOrLabel perm) {
-      return new PermissionBackendCondition.ForChange(this, perm, getUser());
-    }
-
-    private 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");
-    }
-
-    private boolean can(ChangePermission perm) throws PermissionBackendException {
-      try {
-        return switch (perm) {
-          case READ -> isVisible();
-          case ABANDON -> canAbandon();
-          case DELETE -> getProjectControl().isAdmin() || refControl.canDeleteChanges(isOwner());
-          case ADD_PATCH_SET -> canAddPatchSet();
-          case EDIT_DESCRIPTION -> canEditDescription();
-          case EDIT_HASHTAGS -> canEditHashtags();
-          case EDIT_CUSTOM_KEYED_VALUES -> canEditCustomKeyedValues();
-          case EDIT_TOPIC_NAME -> canEditTopicName();
-          case REBASE -> canRebase();
-          case REBASE_ON_BEHALF_OF_UPLOADER -> canRebaseOnBehalfOfUploader();
-          case RESTORE -> canRestore();
-          case REVERT -> canRevert();
-          case SUBMIT -> refControl.canSubmit(isOwner());
-          case TOGGLE_WORK_IN_PROGRESS_STATE -> canToggleWorkInProgressState();
-          case REMOVE_REVIEWER, SUBMIT_AS -> refControl.canPerform(changePermissionName(perm));
-        };
-      } catch (StorageException e) {
-        throw new PermissionBackendException("unavailable", e);
-      }
-    }
-
-    private boolean can(AbstractLabelPermission perm) {
-      return !label(labelPermissionName(perm)).isEmpty();
-    }
-
-    private boolean can(AbstractLabelPermission.WithValue perm) {
-      PermissionRange r = label(labelPermissionName(perm));
-      if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
-        return false;
-      }
-      return r.contains(perm.value());
-    }
-
-    private PermissionRange label(String permission) {
-      if (labels == null) {
-        labels = Maps.newHashMapWithExpectedSize(4);
-      }
-      PermissionRange r = labels.get(permission);
-      if (r == null) {
-        r = getRange(permission);
-        labels.put(permission, r);
-      }
-      return r;
-    }
-  }
-
-  private static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
-    if (permSet instanceof EnumSet) {
-      @SuppressWarnings({"unchecked", "rawtypes"})
-      Set<T> s = ((EnumSet) permSet).clone();
-      s.clear();
-      return s;
-    }
-    return Sets.newHashSetWithExpectedSize(permSet.size());
-  }
-
-  private static String changePermissionName(ChangePermission changePermission) {
-    // Within this class, it's programmer error to call this method on a
-    // ChangePermission that isn't associated with a permission name.
-    return DefaultPermissionMappings.changePermissionName(changePermission)
-        .orElseThrow(() -> new IllegalStateException("no name for " + changePermission));
+    return false;
   }
 }
diff --git a/java/com/google/gerrit/server/permissions/ChangeControlForChangeToBeCreated.java b/java/com/google/gerrit/server/permissions/ChangeControlForChangeToBeCreated.java
new file mode 100644
index 0000000..2cb9d32
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/ChangeControlForChangeToBeCreated.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
+
+/** Access control management for a user accessing a change that has not been created yet. */
+public class ChangeControlForChangeToBeCreated extends AbstractChangeControl {
+
+  public ChangeControlForChangeToBeCreated(
+      ProjectControl projectControl,
+      RefControl refControl,
+      PermissionBackend permissionBackend,
+      boolean isOwner) {
+    super(projectControl, refControl, permissionBackend, /* isNew= */ true, isOwner);
+  }
+
+  @Override
+  public ForChange asForChange() {
+    return new ForChangeImpl(/* changeId= */ null);
+  }
+}
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index a168ba1..02fa7c4 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.cache.PerThreadCache;
@@ -72,11 +73,28 @@
 
   @Override
   public WithUser currentUser() {
-    return new WithUserImpl(currentUser.get());
+    return user(currentUser.get());
   }
 
   @Override
   public WithUser user(CurrentUser user) {
+    return new WithUserImpl(requireNonNull(user.getUserForPermission(), "user"));
+  }
+
+  @Override
+  public WithUser user(CurrentUser user, ImpersonationPermissionMode permissionMode) {
+    switch (permissionMode) {
+      case REAL_USER -> {
+        return new WithUserImpl(requireNonNull(user.getRealUser(), "user"));
+      }
+      default -> { // THIS_USER
+        return new WithUserImpl(requireNonNull(user, "user"));
+      }
+    }
+  }
+
+  @Override
+  public WithUser exactUser(CurrentUser user) {
     return new WithUserImpl(requireNonNull(user, "user"));
   }
 
@@ -216,7 +234,7 @@
     }
 
     private Boolean computeAdmin() {
-      if (user.isImpersonating()) {
+      if (user.isImpersonated()) {
         return false;
       }
       if (user instanceof PeerDaemonUser) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 749470a..31be280 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -119,7 +119,7 @@
     this.user = projectControl.getUser();
     this.projectState = projectControl.getProjectState();
     this.permissionBackendForProject =
-        permissionBackend.user(user).project(projectState.getNameKey());
+        permissionBackend.exactUser(user).project(projectState.getNameKey());
     this.metrics = metrics;
   }
 
@@ -227,7 +227,7 @@
 
     boolean hasAccessDatabase =
         permissionBackend
-            .user(projectControl.getUser())
+            .exactUser(projectControl.getUser())
             .testOrFalse(GlobalPermission.ACCESS_DATABASE);
     ImmutableList.Builder<Ref> resultRefs = ImmutableList.builderWithExpectedSize(refs.size());
     ImmutableList.Builder<Ref> deferredTags = ImmutableList.builder();
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 6a8c92b..0fc9807 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -173,18 +173,23 @@
     }
 
     @Override
-    public void check(RefPermissionOrLabel perm) throws PermissionBackendException {
+    public ForChange changeToBeCreated(boolean isChangeOwner) {
+      return new FailedChange(message, cause);
+    }
+
+    @Override
+    public void check(RefPermission perm) throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
 
     @Override
-    public <T extends RefPermissionOrLabel> Set<T> test(Collection<T> permSet)
+    public Set<RefPermission> test(Collection<RefPermission> permSet)
         throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
 
     @Override
-    public BooleanCondition testCond(RefPermissionOrLabel perm) {
+    public BooleanCondition testCond(RefPermission perm) {
       throw new UnsupportedOperationException(
           "FailedPermissionBackend does not support conditions");
     }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 55cdc3d..93be379 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.ImplementedBy;
@@ -98,14 +99,31 @@
 
   /**
    * Returns an instance scoped to the specified user. Should be used in cases where the user could
-   * either be the issuer of the current request or an impersonated user. PermissionBackends that do
-   * not support impersonation can fail with an {@code IllegalStateException}.
+   * either be the issuer of the current request or an impersonated user. Uses {@link
+   * CurrentUser#getUserForPermission()} to determine permissions of which user should be used in
+   * case of impersonation. PermissionBackends that do not support impersonation can fail with an
+   * {@code IllegalStateException}.
    *
    * <p>If an instance scoped to the current user is desired, use {@code currentUser()} instead.
    */
   public abstract WithUser user(CurrentUser user);
 
   /**
+   * Returns an instance scoped to the specified user. Should be used in cases where the user could
+   * either be the issuer of the current request or an impersonated user. Uses {@code
+   * permissionMode} argument to determine permissions of which user should be used in case of
+   * impersonation. PermissionBackends that do not support impersonation can fail with an {@code
+   * IllegalStateException}.
+   */
+  public abstract WithUser user(CurrentUser user, ImpersonationPermissionMode permissionMode);
+
+  /**
+   * For calls internal to permission evaluation logic, uses specified user without attempting to
+   * resolve impersonation.
+   */
+  public abstract WithUser exactUser(CurrentUser user);
+
+  /**
    * Returns an instance scoped to the provided user. Should be used in cases where the caller wants
    * to check the permissions of a user who is not the issuer of the current request and not the
    * target of impersonation.
@@ -394,33 +412,24 @@
     /** Returns an instance scoped to change. */
     public abstract ForChange change(ChangeNotes notes);
 
+    /** Returns an instance scoped to change that has not been created yet. */
+    public abstract ForChange changeToBeCreated(boolean isOwner);
+
     /**
      * Verify scoped user can {@code perm}, throwing if denied.
      *
      * <p>Should be used in REST API handlers where the thrown {@link AuthException} can be
      * propagated. In business logic, where the exception would have to be caught, prefer using
-     * {@link #test(RefPermissionOrLabel)}.
+     * {@link #test(RefPermission)}.
      */
-    public abstract void check(RefPermissionOrLabel perm)
-        throws AuthException, PermissionBackendException;
+    public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
 
     /** Filter {@code permSet} to permissions scoped user might be able to perform. */
-    public abstract <T extends RefPermissionOrLabel> Set<T> test(Collection<T> permSet)
+    public abstract Set<RefPermission> test(Collection<RefPermission> permSet)
         throws PermissionBackendException;
 
-    public boolean test(RefPermissionOrLabel perm) throws PermissionBackendException {
-      return test(Collections.singleton(perm)).contains(perm);
-    }
-
-    /**
-     * Test which values of a label the user may be able to set.
-     *
-     * @param label definition of the label to test values of.
-     * @return set containing values the user may be able to use; may be empty if none.
-     * @throws PermissionBackendException if failure consulting backend configuration.
-     */
-    public Set<LabelPermission.WithValue> test(LabelType label) throws PermissionBackendException {
-      return test(valuesOf(requireNonNull(label, "LabelType")));
+    public boolean test(RefPermission perm) throws PermissionBackendException {
+      return test(EnumSet.of(perm)).contains(perm);
     }
 
     /**
@@ -433,7 +442,7 @@
      * @return true if the user might be able to perform the permission; false if the user may be
      *     missing the necessary grants or state, or if the backend threw an exception.
      */
-    public boolean testOrFalse(RefPermissionOrLabel perm) {
+    public boolean testOrFalse(RefPermission perm) {
       try {
         return test(perm);
       } catch (PermissionBackendException e) {
@@ -442,13 +451,7 @@
       }
     }
 
-    public abstract BooleanCondition testCond(RefPermissionOrLabel perm);
-
-    private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
-      return label.getValues().stream()
-          .map(v -> new LabelPermission.WithValue(label, v))
-          .collect(toSet());
-    }
+    public abstract BooleanCondition testCond(RefPermission perm);
   }
 
   /** PermissionBackend scoped to a user, project, reference and change. */
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java b/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
index 7018fa0..a92e504 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
@@ -148,10 +148,10 @@
 
   public static class ForRef extends PermissionBackendCondition {
     private final PermissionBackend.ForRef impl;
-    private final RefPermissionOrLabel perm;
+    private final RefPermission perm;
     private final CurrentUser user;
 
-    public ForRef(PermissionBackend.ForRef impl, RefPermissionOrLabel perm, CurrentUser user) {
+    public ForRef(PermissionBackend.ForRef impl, RefPermission perm, CurrentUser user) {
       this.impl = impl;
       this.perm = perm;
       this.user = user;
@@ -161,7 +161,7 @@
       return impl;
     }
 
-    public RefPermissionOrLabel permission() {
+    public RefPermission permission() {
       return perm;
     }
 
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 194179e..328d01e 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -123,6 +123,11 @@
     return changeControlFactory.create(this, controlForRef(cd.branchOrThrow()), cd);
   }
 
+  ChangeControlForChangeToBeCreated controlForChangeToBeCreated(
+      RefControl refControl, boolean isOwner) {
+    return new ChangeControlForChangeToBeCreated(this, refControl, permissionBackend, isOwner);
+  }
+
   RefControl controlForRef(BranchNameKey ref) {
     return controlForRef(ref.branch());
   }
@@ -169,7 +174,7 @@
 
   boolean isAdmin() {
     try {
-      return permissionBackend.user(user).test(GlobalPermission.ADMINISTRATE_SERVER);
+      return permissionBackend.exactUser(user).test(GlobalPermission.ADMINISTRATE_SERVER);
     } catch (PermissionBackendException e) {
       return false;
     }
@@ -181,8 +186,7 @@
 
   boolean allRefsAreVisible(Set<String> ignore) {
     return user.isInternalUser()
-        || (!getProject().getNameKey().equals(allUsersName)
-            && canPerformOnAllRefs(Permission.READ, ignore));
+        || (!getProject().getNameKey().equals(allUsersName) && canPerformReadOnAllRefs(ignore));
   }
 
   /** Can the user run upload pack? */
@@ -292,20 +296,25 @@
     return null;
   }
 
-  private boolean canPerformOnAllRefs(String permission, Set<String> ignore) {
+  private boolean canPerformReadOnAllRefs(Set<String> ignore) {
     try (TraceTimer timer =
         TraceContext.newTimer(
-            "ProjectControl#canPerformOnAllRefs",
+            "ProjectControl#canPerformReadOnAllRefs",
             Metadata.builder().projectName(getProject().getName()).build())) {
+      // Admins should be able to read all refs.
+      if (isAdmin()) {
+        return true;
+      }
+
       boolean canPerform = false;
-      Set<String> patterns = allRefPatterns(permission);
+      Set<String> patterns = allRefPatterns(Permission.READ);
       if (patterns.contains(ALL)) {
         // Only possible if granted on the pattern that
         // matches every possible reference.  Check all
         // patterns also have the permission.
         //
         for (String pattern : patterns) {
-          if (controlForRef(pattern).canPerform(permission)) {
+          if (controlForRef(pattern).canPerform(Permission.READ)) {
             canPerform = true;
           } else if (ignore.contains(pattern)) {
             continue;
@@ -453,7 +462,7 @@
       throw new PermissionBackendException(perm.describeForException() + " unsupported");
     }
 
-    private boolean can(ProjectPermission perm) throws PermissionBackendException {
+    private boolean can(ProjectPermission perm) {
       return switch (perm) {
         case ACCESS -> user.isInternalUser() || isOwner() || canPerformOnAnyRef(Permission.READ);
         case READ -> allRefsAreVisible(Collections.emptySet());
@@ -463,7 +472,8 @@
         case RUN_RECEIVE_PACK -> canRunReceivePack();
         case RUN_UPLOAD_PACK -> canRunUploadPack();
         case PUSH_AT_LEAST_ONE_REF -> canPushToAtLeastOneRef();
-        case READ_CONFIG -> controlForRef(RefNames.REFS_CONFIG).hasReadPermissionOnRef(false);
+        case READ_CONFIG ->
+            isOwner() || controlForRef(RefNames.REFS_CONFIG).hasReadPermissionOnRef(false);
         case BAN_COMMIT, READ_REFLOG, WRITE_CONFIG -> isOwner();
         case UPDATE_CONFIG_WITHOUT_CREATING_CHANGE -> canUpdateConfigWithoutCreatingChange();
       };
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 88e53f7..8165cab 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -15,12 +15,8 @@
 package com.google.gerrit.server.permissions;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
-import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
@@ -47,7 +43,6 @@
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Constants;
@@ -397,6 +392,9 @@
         }
         if (overrideFound) {
           // Found an exclusive override, nothing further to do in this project.
+          logger.atFine().log(
+              "Exclusive override found for permission %s and user %s",
+              permissionName, getUser().getLoggableName());
           continue projectLoop;
         }
 
@@ -408,6 +406,9 @@
           }
 
           if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+            logger.atFine().log(
+                "Block rule found for permission %s and user %s",
+                permissionName, getUser().getLoggableName());
             blocked = true;
             break;
           }
@@ -417,6 +418,10 @@
           // ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK.
           for (PermissionRule pr : p.getRules()) {
             if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+              logger.atFine().log(
+                  "Allow rule found in the same access section which overrides the BLOCK for"
+                      + " permission %s and user %s",
+                  permissionName, getUser().getLoggableName());
               blocked = false;
               break;
             }
@@ -424,6 +429,8 @@
         }
 
         if (blocked) {
+          logger.atFine().log(
+              "Permission %s is blocked for user %s", permissionName, getUser().getLoggableName());
           return true;
         }
       }
@@ -487,7 +494,6 @@
   }
 
   private class ForRefImpl extends ForRef {
-    private Map<String, PermissionRange> labels;
     private String resourcePath;
 
     @Override
@@ -524,17 +530,14 @@
     }
 
     @Override
-    public void check(RefPermissionOrLabel perm) throws AuthException, PermissionBackendException {
-      if (perm instanceof RefPermission) {
-        check((RefPermission) perm);
-      } else {
-        if (!can(perm)) {
-          throw new AuthException(perm.describeForException() + " not permitted");
-        }
-      }
+    public ForChange changeToBeCreated(boolean isOwner) {
+      return getProjectControl()
+          .controlForChangeToBeCreated(RefControl.this, isOwner)
+          .asForChange();
     }
 
-    private void check(RefPermission perm) throws AuthException, PermissionBackendException {
+    @Override
+    public void check(RefPermission perm) throws AuthException, PermissionBackendException {
       if (!can(perm)) {
         PermissionDeniedException pde = new PermissionDeniedException(perm, refName);
         switch (perm) {
@@ -601,10 +604,10 @@
     }
 
     @Override
-    public <T extends RefPermissionOrLabel> Set<T> test(Collection<T> permSet)
+    public Set<RefPermission> test(Collection<RefPermission> permSet)
         throws PermissionBackendException {
-      Set<T> ok = newSet(permSet);
-      for (T perm : permSet) {
+      EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class);
+      for (RefPermission perm : permSet) {
         if (can(perm)) {
           ok.add(perm);
         }
@@ -613,44 +616,9 @@
     }
 
     @Override
-    public BooleanCondition testCond(RefPermissionOrLabel perm) {
+    public BooleanCondition testCond(RefPermission perm) {
       return new PermissionBackendCondition.ForRef(this, perm, getUser());
     }
-
-    private boolean can(RefPermissionOrLabel perm) throws PermissionBackendException {
-      if (perm instanceof RefPermission) {
-        return RefControl.this.can((RefPermission) 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");
-    }
-
-    private boolean can(AbstractLabelPermission perm) {
-      return !label(labelPermissionName(perm)).isEmpty();
-    }
-
-    private boolean can(AbstractLabelPermission.WithValue perm) {
-      PermissionRange r = label(labelPermissionName(perm));
-      if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
-        return false;
-      }
-      return r.contains(perm.value());
-    }
-
-    private PermissionRange label(String permission) {
-      if (labels == null) {
-        labels = Maps.newHashMapWithExpectedSize(4);
-      }
-      PermissionRange r = labels.get(permission);
-      if (r == null) {
-        r = getRange(permission);
-        labels.put(permission, r);
-      }
-      return r;
-    }
   }
 
   protected boolean can(RefPermission perm) throws PermissionBackendException {
@@ -773,15 +741,4 @@
     return DefaultPermissionMappings.refPermissionName(refPermission)
         .orElseThrow(() -> new IllegalStateException("no name for " + refPermission));
   }
-
-  // Same as in ChangeControl
-  private static <T extends RefPermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
-    if (permSet instanceof EnumSet) {
-      @SuppressWarnings({"unchecked", "rawtypes"})
-      Set<T> s = ((EnumSet) permSet).clone();
-      s.clear();
-      return s;
-    }
-    return Sets.newHashSetWithExpectedSize(permSet.size());
-  }
 }
diff --git a/java/com/google/gerrit/server/permissions/RefPermission.java b/java/com/google/gerrit/server/permissions/RefPermission.java
index da5b745..34c46af 100644
--- a/java/com/google/gerrit/server/permissions/RefPermission.java
+++ b/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.extensions.api.access.GerritPermission;
 
-public enum RefPermission implements RefPermissionOrLabel {
+public enum RefPermission implements GerritPermission {
   READ,
   CREATE,
 
diff --git a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
index bae1fef..756e922 100644
--- a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
+++ b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
@@ -68,6 +68,11 @@
     if (!RefNames.isGerritRef(refName)) {
       // This is not a special Gerrit ref and not a NoteDb ref. Likely, it's just a ref under
       // refs/heads or another ref the user created. Apply the regular permissions with inheritance.
+
+      // Admins should be able to read all branches (including 'refs/meta/config').
+      if (projectControl.isAdmin()) {
+        return true;
+      }
       return projectControl.controlForRef(refName).hasReadPermissionOnRef(false);
     }
 
@@ -78,7 +83,7 @@
 
     boolean hasAccessDatabase =
         permissionBackend
-            .user(projectControl.getUser())
+            .exactUser(projectControl.getUser())
             .testOrFalse(GlobalPermission.ACCESS_DATABASE);
     if (hasAccessDatabase) {
       return true;
diff --git a/java/com/google/gerrit/server/plugincontext/PluginContext.java b/java/com/google/gerrit/server/plugincontext/PluginContext.java
index a5fad56..dbb772a 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginContext.java
@@ -112,6 +112,9 @@
 
   @Singleton
   public static class PluginMetrics {
+    // Keep in sync with PerformanceLogContext.PLUGIN_LATENCY_NAME.
+    private static final String PLUGIN_LATENCY_NAME = "plugin/latency";
+
     public static final PluginMetrics DISABLED_INSTANCE =
         new PluginMetrics(new DisabledMetricMaker());
 
@@ -135,7 +138,7 @@
 
       this.latency =
           metricMaker.newTimer(
-              "plugin/latency",
+              PLUGIN_LATENCY_NAME,
               new Description("Latency for plugin invocation")
                   .setCumulative()
                   .setUnit(Units.MILLISECONDS),
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 8da0510..dbfbb87 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
 import java.util.List;
@@ -40,6 +41,7 @@
   public InheritableBoolean requireSignedPush;
   public boolean createEmptyCommit;
   public String maxObjectSizeLimit;
+  public boolean initOnly;
 
   public CreateProjectArgs() {
     contributorAgreements = InheritableBoolean.INHERIT;
@@ -69,4 +71,8 @@
   public void setProjectName(Project.NameKey n) {
     projectName = n;
   }
+
+  public String getHead() {
+    return permissionsOnly ? RefNames.REFS_CONFIG : branch.get(0);
+  }
 }
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 7a6926e..955ff95 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -16,11 +16,13 @@
 
 import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -31,10 +33,13 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -73,6 +78,8 @@
    * @param object the object the user will start the reference with
    * @param sourceBranches the source ref from which the new ref is created from
    * @throws AuthException if creation is denied; the message explains the denial.
+   * @throws UnprocessableEntityException if creation depends on an object or ref that is not
+   *     visible
    * @throws PermissionBackendException on failure of permission checks.
    * @throws ResourceConflictException if the project state does not permit the operation
    */
@@ -87,7 +94,8 @@
           PermissionBackendException,
           NoSuchProjectException,
           IOException,
-          ResourceConflictException {
+          ResourceConflictException,
+          UnprocessableEntityException {
     ProjectState ps =
         projectCache.get(destBranch.project()).orElseThrow(noSuchProject(destBranch.project()));
     ps.checkStatePermitsWrite();
@@ -98,22 +106,25 @@
       if (sourceBranches.length == 0) {
         checkCreateCommit(user, repo, (RevCommit) object, ps.getNameKey(), perm, forPush);
       } else {
+        List<Ref> sourceRefs = new ArrayList<>();
         for (BranchNameKey src : sourceBranches) {
-          PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(src);
-          if (forRef.testOrFalse(RefPermission.READ)) {
-            return;
-          }
+          sourceRefs.add(repo.exactRef(src.branch()));
         }
-        AuthException e =
-            new AuthException(
-                String.format(
-                    "must have %s on existing ref to create new ref from it",
-                    RefPermission.READ.describeForException()));
-        e.setAdvice(
+        if (reachable.fromRefs(
+            destBranch.project(),
+            repo,
+            (RevCommit) object,
+            ImmutableList.copyOf(sourceRefs),
+            Optional.of(user.get()))) {
+          return;
+        }
+        // Don't expose existence of source refs or the object to the caller
+        throw new UnprocessableEntityException(
             String.format(
-                "use an existing ref visible to you, or get %s permission on the ref",
-                RefPermission.READ.describeForException()));
-        throw e;
+                "Unable to resolve object '%s'. Check that the provided source branch(es) exist"
+                    + " (%s), that you are permitted to read them, and that the object is reachable"
+                    + " from the source(s).",
+                object, Arrays.toString(sourceBranches)));
       }
       return;
     }
@@ -168,32 +179,29 @@
       Project.NameKey project,
       PermissionBackend.ForRef forRef,
       boolean forPush)
-      throws AuthException, PermissionBackendException, IOException {
-    try {
-      // If the user has UPDATE (push) permission, they can set the ref to an arbitrary commit:
-      //
-      //  * if they don't have access, we don't advertise the data, and a conforming git client
-      //  would send the object along with the push as outcome of the negotation.
-      //  * a malicious client could try to send the update without sending the object. This
-      //  is prevented by JGit's ConnectivityChecker (see receive.checkReferencedObjectsAreReachable
-      //  to switch off this costly check).
-      //
-      // Thus, when using the git command-line client, we don't need to do extra checks for users
-      // with push access.
-      //
-      // When using the REST API, there is no negotiation, and the target commit must already be on
-      // the server, so we must check that the user can see that commit.
-      if (forPush) {
-        // We can only shortcut for UPDATE permission. Pushing a tag (CREATE_TAG, CREATE_SIGNED_TAG)
-        // can also introduce new objects. While there may not be a confidentiality problem
-        // (the caller supplies the data as documented above), the permission is for creating
-        // tags to existing commits.
-        forRef.check(RefPermission.UPDATE);
-        return;
-      }
-    } catch (AuthException denied) {
-      // Fall through to check reachability.
+      throws PermissionBackendException, IOException, UnprocessableEntityException {
+    // If the user has UPDATE (push) permission, they can set the ref to an arbitrary commit:
+    //
+    //  * if they don't have access, we don't advertise the data, and a conforming git client
+    //  would send the object along with the push as outcome of the negotation.
+    //  * a malicious client could try to send the update without sending the object. This
+    //  is prevented by JGit's ConnectivityChecker (see receive.checkReferencedObjectsAreReachable
+    //  to switch off this costly check).
+    //
+    // When using the REST API, there is no negotiation, and the target commit must already be on
+    // the server, so we must check that the user can see that commit.
+    //
+    // Thus, when using the git command-line client, we don't need to do extra checks for users
+    // with push access.
+    //
+    // We can only shortcut for UPDATE permission. Pushing a tag (CREATE_TAG, CREATE_SIGNED_TAG)
+    // can also introduce new objects. While there may not be a confidentiality problem
+    // (the caller supplies the data as documented above), the permission is for creating
+    // tags to existing commits.
+    if (forPush && forRef.test(RefPermission.UPDATE)) {
+      return;
     }
+    // Fall through to check reachability.
     if (reachable.fromRefs(
         project,
         repo,
@@ -219,15 +227,18 @@
       return;
     }
 
-    AuthException e =
-        new AuthException(
-            String.format(
-                "%s for creating new commit object not permitted",
-                RefPermission.UPDATE.describeForException()));
-    e.setAdvice(
+    // Don't expose existence of the commit to the caller
+    String msg =
         String.format(
-            "use a SHA1 visible to you, or get %s permission on the ref",
-            RefPermission.UPDATE.describeForException()));
-    throw e;
+            "Unable to resolve object '%s'. Check that the object exists on the server ", commit);
+    if (forPush) {
+      msg +=
+          String.format(
+              "or get %s permission to create new commit objects.",
+              RefPermission.UPDATE.describeForException());
+    } else {
+      msg += "and is visible to you.";
+    }
+    throw new UnprocessableEntityException(msg);
   }
 }
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
index f46c2b1..a673458 100644
--- a/java/com/google/gerrit/server/project/LabelDefinitionJson.java
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -23,11 +23,19 @@
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 
 public class LabelDefinitionJson {
-  public static LabelDefinitionInfo format(Project.NameKey projectName, LabelType labelType) {
+  /**
+   * Formats the given {@link LabelType} as a {@link LabelDefinitionInfo}.
+   *
+   * @param projectName the name of the project that defines the label, {@code null} if the label is
+   *     globally defined (by implementing the {@link LabelType} extension point)
+   * @param labelType the label type that should be formatted
+   */
+  public static LabelDefinitionInfo format(
+      @Nullable Project.NameKey projectName, LabelType labelType) {
     LabelDefinitionInfo label = new LabelDefinitionInfo();
     label.name = labelType.getName();
     label.description = labelType.getDescription().orElse(null);
-    label.projectName = projectName.get();
+    label.projectName = projectName != null ? projectName.get() : null;
     label.function = labelType.getFunction().getFunctionName();
     label.values =
         labelType.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText));
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index e774d0b..7f6634c 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.CachedProjectConfig;
 import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Permission;
@@ -34,7 +39,10 @@
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
@@ -47,11 +55,14 @@
 import com.google.gerrit.server.git.RepositoryExistsException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryableAction.ActionType;
 import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -59,6 +70,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
@@ -76,11 +88,13 @@
 
   private final GitRepositoryManager repoManager;
   private final PluginSetContext<NewProjectCreatedListener> createdListeners;
+  private final PluginSetContext<ExceptionHook> exceptionHooks;
   private final ProjectCache projectCache;
   private final GroupBackend groupBackend;
   private final MetaDataUpdate.User metaDataUpdateFactory;
   private final GitReferenceUpdated referenceUpdated;
   private final RepositoryConfig repositoryCfg;
+  private final RetryHelper retryHelper;
   private final Provider<PersonIdent> serverIdent;
   private final Provider<IdentifiedUser> identifiedUser;
   private final ProjectConfig.Factory projectConfigFactory;
@@ -90,22 +104,26 @@
   ProjectCreator(
       GitRepositoryManager repoManager,
       PluginSetContext<NewProjectCreatedListener> createdListeners,
+      PluginSetContext<ExceptionHook> exceptionHooks,
       ProjectCache projectCache,
       GroupBackend groupBackend,
       MetaDataUpdate.User metaDataUpdateFactory,
       GitReferenceUpdated referenceUpdated,
       RepositoryConfig repositoryCfg,
+      RetryHelper retryHelper,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       @Nullable @GerritInstanceId String gerritInstanceId,
       Provider<IdentifiedUser> identifiedUser,
       ProjectConfig.Factory projectConfigFactory) {
     this.repoManager = repoManager;
     this.createdListeners = createdListeners;
+    this.exceptionHooks = exceptionHooks;
     this.projectCache = projectCache;
     this.groupBackend = groupBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.referenceUpdated = referenceUpdated;
     this.repositoryCfg = repositoryCfg;
+    this.retryHelper = retryHelper;
     this.serverIdent = serverIdent;
     this.gerritInstanceId = gerritInstanceId;
     this.identifiedUser = identifiedUser;
@@ -114,44 +132,117 @@
 
   @CanIgnoreReturnValue
   public ProjectState createProject(CreateProjectArgs args)
-      throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
-    try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO)) {
-      final Project.NameKey nameKey = args.getProject();
-      try {
-        final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
-        Status status = repoManager.getRepositoryStatus(nameKey);
-        if (!status.equals(Status.NON_EXISTENT)) {
-          throw new RepositoryExistsException(nameKey, "Repository status: " + status);
-        }
-        try (Repository repo = repoManager.createRepository(nameKey)) {
-          projectCache.evict(nameKey);
+      throws RestApiException, IOException, ConfigInvalidException {
+    try {
+      return retryHelper
+          .action(
+              ActionType.REPO_CREATION,
+              "createProject",
+              () -> {
+                try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO)) {
+                  Project.NameKey nameKey = args.getProject();
 
-          RefUpdate u = repo.updateRef(Constants.HEAD);
-          u.disableRefLog();
-          u.link(head);
+                  if (args.initOnly) {
+                    try (Repository repo = repoManager.openRepository(nameKey)) {
+                      initProject(nameKey, repo, args);
+                      return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
+                    } catch (RepositoryNotFoundException notFound) {
+                      throw new ResourceNotFoundException(
+                          String.format("repository %s not found", nameKey), notFound);
+                    }
+                  }
 
-          createProjectConfig(args, head);
-
-          if (!args.permissionsOnly && args.createEmptyCommit) {
-            createEmptyCommits(repo, nameKey, args.branch);
-          }
-
-          return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
-        }
-      } catch (RepositoryExistsException e) {
-        throw new ResourceConflictException(
-            "Cannot create "
-                + nameKey.get()
-                + " because the name is already occupied by another project.",
-            e);
-      } catch (RepositoryNotFoundException badName) {
-        throw new BadRequestException("invalid project name: " + nameKey, badName);
-      }
+                  try {
+                    Status status = repoManager.getRepositoryStatus(nameKey);
+                    if (!status.equals(Status.NON_EXISTENT)) {
+                      throw new RepositoryExistsException(nameKey, "Repository status: " + status);
+                    }
+                    try (Repository repo = repoManager.createRepository(nameKey)) {
+                      initProject(nameKey, repo, args);
+                      return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
+                    }
+                  } catch (RepositoryExistsException e) {
+                    throw new ResourceConflictException(
+                        "Cannot create "
+                            + nameKey.get()
+                            + " because the name is already occupied by another project.",
+                        e);
+                  } catch (RepositoryNotFoundException badName) {
+                    throw new BadRequestException("invalid project name: " + nameKey, badName);
+                  } catch (RuntimeException e) {
+                    if (exceptionHooks.stream()
+                        .anyMatch(
+                            exceptionHook ->
+                                exceptionHook.tryProjectInitializationOnRepoCreationFailure(
+                                    nameKey, args, e))) {
+                      logger.atFine().withCause(e).log(
+                          "try initializing project %s after repo creation failure", nameKey);
+                      try (Repository repo = repoManager.openRepository(nameKey)) {
+                        initProject(nameKey, repo, args);
+                      } catch (
+                          @SuppressWarnings("UnusedException")
+                          RuntimeException e2) {
+                        logger.atWarning().withCause(e).log(
+                            "initializing project %s after repo creation failure has failed",
+                            nameKey);
+                        throw e;
+                      }
+                    }
+                    throw e;
+                  }
+                }
+              })
+          .call();
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      Throwables.throwIfInstanceOf(e, IOException.class);
+      Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
+      Throwables.throwIfInstanceOf(e, RestApiException.class);
+      throw new IllegalStateException(e);
     }
   }
 
-  private void createProjectConfig(CreateProjectArgs args, String head)
-      throws IOException, ConfigInvalidException {
+  private void initProject(Project.NameKey nameKey, Repository repo, CreateProjectArgs args)
+      throws IOException, ConfigInvalidException, ResourceConflictException {
+    projectCache.evict(nameKey);
+    createHead(repo, args);
+    createProjectConfig(repo, args);
+
+    if (!args.permissionsOnly && args.createEmptyCommit) {
+      createEmptyCommits(repo, args, nameKey);
+    }
+  }
+
+  private void createHead(Repository repo, CreateProjectArgs args)
+      throws IOException, ResourceConflictException {
+    if (args.initOnly) {
+      Ref head = repo.exactRef(Constants.HEAD);
+      if (head != null) {
+        if (head.getTarget().getName().equals(args.getHead())) {
+          logger.atFine().log("Skip creation of HEAD because it already exists");
+          return;
+        }
+
+        throw new ResourceConflictException("conflicting HEAD already exists");
+      }
+    }
+
+    RefUpdate u = repo.updateRef(Constants.HEAD);
+    u.disableRefLog();
+    u.link(args.getHead());
+  }
+
+  private void createProjectConfig(Repository repo, CreateProjectArgs args)
+      throws IOException, ConfigInvalidException, ResourceConflictException {
+    Optional<CachedProjectConfig> currentConfig =
+        args.initOnly && repo.exactRef("refs/meta/config") != null
+            ? Optional.of(
+                projectCache
+                    .get(args.getProject())
+                    .orElseThrow(illegalState(args.getProject()))
+                    .getConfig())
+            : Optional.empty();
+
     RevCommit configRevCommit = null;
     try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) {
       ProjectConfig config = projectConfigFactory.read(md);
@@ -197,18 +288,40 @@
             });
       }
 
+      if (currentConfig.isPresent()) {
+        if (currentConfig.get().equals(config.getCacheable())) {
+          logger.atFine().log("Skip creation of project config because it already exists");
+          return;
+        }
+        throw new ResourceConflictException("conflicting project config already exists");
+      }
+
       configRevCommit = config.commit(md, false);
       md.getRepository().setGitwebDescription(args.projectDescription);
     } finally {
       if (configRevCommit != null) {
-        fireEvents(args.getProject(), head, configRevCommit);
+        fireEvents(args.getProject(), args.getHead(), configRevCommit);
       }
     }
     projectCache.onCreateProject(args.getProject());
   }
 
-  private void createEmptyCommits(Repository repo, Project.NameKey project, List<String> refs)
-      throws IOException {
+  private void createEmptyCommits(Repository repo, CreateProjectArgs args, Project.NameKey project)
+      throws IOException, ResourceConflictException {
+    if (args.initOnly) {
+      List<Ref> existingRefs = repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS);
+      if (!existingRefs.isEmpty()) {
+        if (Sets.symmetricDifference(
+                existingRefs.stream().map(Ref::getName).collect(toImmutableSet()),
+                ImmutableSet.copyOf(args.branch))
+            .isEmpty()) {
+          logger.atFine().log("Skip creation of branches since they already exist");
+          return;
+        }
+        throw new ResourceConflictException(String.format("conflicting branches already exists"));
+      }
+    }
+
     try (ObjectInserter oi = repo.newObjectInserter()) {
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
@@ -219,7 +332,12 @@
       ObjectId id = oi.insert(cb);
       oi.flush();
 
-      for (String ref : refs) {
+      for (String ref : args.branch) {
+        if (repo.exactRef(ref) != null) {
+          logger.atFine().log("Skip creation of branch %s because it already exists", ref);
+          continue;
+        }
+
         RefUpdate ru = repo.updateRef(ref);
         ru.setNewObjectId(id);
         Result result = ru.update();
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index dd5e72f..db5f8ee 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -45,9 +45,12 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.DefaultBooleanProjectConfig;
 import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -76,6 +79,7 @@
 
   private final boolean isAllProjects;
   private final boolean isAllUsers;
+  private final RepositoryConfig repositoryConfig;
   private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final List<CommentLinkInfo> commentLinks;
@@ -85,6 +89,8 @@
   private final long globalMaxObjectSizeLimit;
   private final boolean inheritProjectMaxObjectSizeLimit;
 
+  private final PluginSetContext<LabelType> globalLabelTypes;
+
   /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
   private volatile List<SectionMatcher> localAccessSections;
 
@@ -93,13 +99,16 @@
 
   @Inject
   public ProjectState(
+      RepositoryConfig repositoryConfig,
       ProjectCache projectCache,
       AllProjectsName allProjectsName,
       AllUsersName allUsersName,
       List<CommentLinkInfo> commentLinks,
       CapabilityCollection.Factory limitsFactory,
       TransferConfig transferConfig,
+      PluginSetContext<LabelType> globalLabelTypes,
       @Assisted CachedProjectConfig cachedProjectConfig) {
+    this.repositoryConfig = repositoryConfig;
     this.projectCache = projectCache;
     this.isAllProjects = cachedProjectConfig.getProject().getNameKey().equals(allProjectsName);
     this.isAllUsers = cachedProjectConfig.getProject().getNameKey().equals(allUsersName);
@@ -115,6 +124,7 @@
             : null;
     this.globalMaxObjectSizeLimit = transferConfig.getMaxObjectSizeLimit();
     this.inheritProjectMaxObjectSizeLimit = transferConfig.inheritProjectMaxObjectSizeLimit();
+    this.globalLabelTypes = globalLabelTypes;
 
     if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
       localOwners = Collections.emptySet();
@@ -370,6 +380,12 @@
   }
 
   public boolean is(BooleanProjectConfig config) {
+    DefaultBooleanProjectConfig.Value defaultValue =
+        repositoryConfig.getDefault(getNameKey(), config);
+    if (DefaultBooleanProjectConfig.Value.FORCED.equals(defaultValue)) {
+      return true;
+    }
+
     for (ProjectState s : tree()) {
       switch (s.getProject().getBooleanConfig(config)) {
         case TRUE:
@@ -381,7 +397,11 @@
           continue;
       }
     }
-    return false;
+
+    return switch (defaultValue) {
+      case TRUE, FORCED -> true;
+      case FALSE -> false;
+    };
   }
 
   /** Get all submit requirements for a project, including those from parent projects. */
@@ -402,6 +422,7 @@
   /** All available label types. */
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = new LinkedHashMap<>();
+    globalLabelTypes.forEach(e -> types.put(e.get().getName().toLowerCase(Locale.US), e.get()));
     for (ProjectState s : treeInOrder()) {
       for (LabelType type : s.getConfig().getLabelSections().values()) {
         String lower = type.getName().toLowerCase(Locale.US);
diff --git a/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java b/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java
index e32100e..fa75ba7 100644
--- a/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java
+++ b/java/com/google/gerrit/server/project/PrologRulesWarningValidator.java
@@ -47,7 +47,7 @@
 
   @Inject
   public PrologRulesWarningValidator(@GerritServerConfig Config cfg) {
-    this.allowNewRules = cfg.getBoolean("rules", "allowNewRules", true);
+    this.allowNewRules = cfg.getBoolean("rules", "allowNewRules", false);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index ea62d7d..ee8cf05 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSetApproval;
@@ -32,6 +34,8 @@
 
 @Singleton
 public class RemoveReviewerControl {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
 
@@ -49,7 +53,7 @@
    * @throws ResourceConflictException if the approval cannot be removed because the change is
    *     merged
    */
-  public void checkRemoveReviewer(
+  public void checkRemoveReviewerApproval(
       ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
       throws PermissionBackendException, AuthException, ResourceConflictException {
     if (notes.getChange().isMerged() && approval.value() != 0) {
@@ -63,10 +67,15 @@
    * Checks if removing the given reviewer is OK. Does not check if removing any approvals the
    * reviewer might have given is OK.
    *
+   * <p>The `reviewer` parameter is only used for logging purposes and for checking whether the
+   * current user is equal to the reviewer. So it can be set to null. Then the permission check is
+   * generic.
+   *
    * @throws AuthException if this user is not allowed to remove this approval.
    * @throws PermissionBackendException on failure of permission checks.
    */
-  public void checkRemoveReviewer(ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer)
+  public void checkRemoveReviewer(
+      ChangeNotes notes, CurrentUser currentUser, @Nullable Account.Id reviewer)
       throws PermissionBackendException, AuthException {
     checkRemoveReviewer(notes, currentUser, reviewer, 0);
   }
@@ -80,14 +89,14 @@
 
   /** Returns true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
-      ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+      ChangeNotes notes, CurrentUser currentUser, @Nullable Account.Id reviewer, int value)
       throws PermissionBackendException {
     return testRemoveReviewer(changeDataFactory.create(notes), currentUser, reviewer, value);
   }
 
   /** Returns true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
-      ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
+      ChangeData cd, CurrentUser currentUser, @Nullable Account.Id reviewer, int value)
       throws PermissionBackendException {
     if (cd.change().isMerged() && value != 0) {
       return false;
@@ -107,34 +116,76 @@
   }
 
   private void checkRemoveReviewer(
-      ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+      ChangeNotes notes, CurrentUser currentUser, @Nullable Account.Id reviewer, int value)
       throws PermissionBackendException, AuthException {
     if (canRemoveReviewerWithoutPermissionCheck(notes.getChange(), currentUser, reviewer, value)) {
       return;
     }
+    String reviewerStr = reviewer != null ? reviewer.toString() : "by email";
 
     // Users with the remove reviewer permission, the branch owner, project
     // owner and site admin can remove anyone
     PermissionBackend.WithUser withUser = permissionBackend.user(currentUser);
     PermissionBackend.ForProject forProject = withUser.project(notes.getProjectName());
-    if (forProject.ref(notes.getChange().getDest().branch()).test(RefPermission.WRITE_CONFIG)
-        || withUser.test(GlobalPermission.ADMINISTRATE_SERVER)) {
+    if (forProject.ref(notes.getChange().getDest().branch()).test(RefPermission.WRITE_CONFIG)) {
+      logger.atFine().log(
+          "%s can remove reviewer %s from change %s since they are an owner of the destination"
+              + " branch %s",
+          currentUser.getLoggableName(),
+          reviewerStr,
+          notes.getChangeId(),
+          notes.getChange().getDest().branch());
       return;
     }
-    permissionBackend.user(currentUser).change(notes).check(ChangePermission.REMOVE_REVIEWER);
+    if (withUser.test(GlobalPermission.ADMINISTRATE_SERVER)) {
+      logger.atFine().log(
+          "%s can remove reviewer %s from change %s since they are an administrator",
+          currentUser.getLoggableName(), reviewer, notes.getChangeId());
+      return;
+    }
+    try {
+      permissionBackend.user(currentUser).change(notes).check(ChangePermission.REMOVE_REVIEWER);
+      logger.atFine().log(
+          "%s can remove reviewer %s from change %s since they have the %s permission",
+          currentUser.getLoggableName(),
+          reviewerStr,
+          notes.getChangeId(),
+          ChangePermission.REMOVE_REVIEWER.name());
+    } catch (AuthException e) {
+      logger.atFine().log(
+          "%s cannot remove reviewer %s from change %s since they don't have the %s permission",
+          currentUser.getLoggableName(),
+          reviewerStr,
+          notes.getChangeId(),
+          ChangePermission.REMOVE_REVIEWER.name());
+      throw e;
+    }
   }
 
   public static boolean canRemoveReviewerWithoutPermissionCheck(
-      Change change, CurrentUser currentUser, Account.Id reviewer, int value) {
+      Change change, CurrentUser currentUser, @Nullable Account.Id reviewer, int value) {
+    String reviewerStr = reviewer != null ? reviewer.toString() : "by email";
     if (currentUser.isIdentifiedUser()) {
       Account.Id aId = currentUser.getAccountId();
       if (aId.equals(reviewer)) {
-        return true; // A user can always remove themselves.
+        // A user can always remove themselves.
+        logger.atFine().log(
+            "%s can remove reviewer %s from change %s since they can always remove themselves",
+            currentUser.getLoggableName(), reviewerStr, change.getId());
+        return true;
       } else if (aId.equals(change.getOwner()) && 0 <= value) {
-        return true; // The change owner may remove any zero or positive score.
+        // The change owner may remove any zero or positive score.
+        logger.atFine().log(
+            "%s can remove reviewer %s from change %s since they own the change and the reviewer"
+                + " scored with zero or a positive score",
+            currentUser.getLoggableName(), reviewerStr, change.getId());
+        return true;
       }
     }
 
+    logger.atFine().log(
+        "%s cannot remove reviewer %s from change %s without permission check",
+        currentUser.getLoggableName(), reviewerStr, change.getId());
     return false;
   }
 }
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementJson.java b/java/com/google/gerrit/server/project/SubmitRequirementJson.java
index 5593ff4..4468b30 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementJson.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementJson.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.inject.Singleton;
@@ -21,9 +23,19 @@
 /** Converts a {@link SubmitRequirement} to a {@link SubmitRequirementInfo}. */
 @Singleton
 public class SubmitRequirementJson {
-  public static SubmitRequirementInfo format(SubmitRequirement sr) {
+  /**
+   * Formats the given {@link SubmitRequirement} as a {@link SubmitRequirementInfo}.
+   *
+   * @param projectName the name of the project that defines the submit requirement, {@code null} if
+   *     the submit requirement is globally defined (by implementing the {@link SubmitRequirement}
+   *     extension point)
+   * @param sr the submit requirement that should be formatted
+   */
+  public static SubmitRequirementInfo format(
+      @Nullable Project.NameKey projectName, SubmitRequirement sr) {
     SubmitRequirementInfo info = new SubmitRequirementInfo();
     info.name = sr.name();
+    info.projectName = projectName != null ? projectName.get() : null;
     info.description = sr.description().orElse(null);
     if (sr.applicabilityExpression().isPresent()) {
       info.applicabilityExpression = sr.applicabilityExpression().get().expressionString();
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 6fb56fe..16cdc26 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -20,12 +20,11 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PredicateResult;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
-import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
 import com.google.gerrit.entities.SubmitRequirementResult;
-import com.google.gerrit.index.query.MatchResult;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.logging.Metadata;
@@ -113,7 +112,7 @@
       SubmitRequirementExpression expression, ChangeData changeData) {
     try {
       Predicate<ChangeData> predicate = queryBuilder.get().parse(expression.expressionString());
-      PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData);
+      PredicateResult predicateResult = changeData.evaluatePredicateTree(predicate);
       return SubmitRequirementExpressionResult.create(expression, predicateResult);
     } catch (QueryParseException | SubmitRequirementEvaluationException e) {
       logger.atWarning().withCause(e).log(
@@ -123,56 +122,63 @@
   }
 
   private SubmitRequirementResult evaluateRequirementInternal(SubmitRequirement sr, ChangeData cd) {
-    Optional<SubmitRequirementExpressionResult> applicabilityResult =
-        sr.applicabilityExpression().isPresent()
-            ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
-            : Optional.empty();
-    Optional<SubmitRequirementExpressionResult> submittabilityResult =
-        Optional.of(SubmitRequirementExpressionResult.notEvaluated(sr.submittabilityExpression()));
-    Optional<SubmitRequirementExpressionResult> overrideResult =
-        sr.overrideExpression().isPresent()
-            ? Optional.of(
-                SubmitRequirementExpressionResult.notEvaluated(sr.overrideExpression().get()))
-            : Optional.empty();
-    if (!sr.applicabilityExpression().isPresent()
-        || SubmitRequirementResult.assertPass(applicabilityResult)) {
-      submittabilityResult = Optional.of(evaluateExpression(sr.submittabilityExpression(), cd));
-      overrideResult =
-          sr.overrideExpression().isPresent()
-              ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Evaluate submit requirement " + sr.name(),
+            Metadata.builder().changeId(cd.change().getId().get()).build())) {
+      Optional<SubmitRequirementExpressionResult> applicabilityResult =
+          sr.applicabilityExpression().isPresent()
+              ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
               : Optional.empty();
-    }
+      Optional<SubmitRequirementExpressionResult> submittabilityResult =
+          Optional.of(
+              SubmitRequirementExpressionResult.notEvaluated(sr.submittabilityExpression()));
+      Optional<SubmitRequirementExpressionResult> overrideResult =
+          sr.overrideExpression().isPresent()
+              ? Optional.of(
+                  SubmitRequirementExpressionResult.notEvaluated(sr.overrideExpression().get()))
+              : Optional.empty();
+      if (!sr.applicabilityExpression().isPresent()
+          || SubmitRequirementResult.assertPass(applicabilityResult)) {
+        submittabilityResult = Optional.of(evaluateExpression(sr.submittabilityExpression(), cd));
+        overrideResult =
+            sr.overrideExpression().isPresent()
+                ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
+                : Optional.empty();
+      }
 
-    if (applicabilityResult.isPresent()) {
-      logger.atFine().log(
-          "Applicability expression result for SR name '%s':"
-              + " passing atoms: %s, failing atoms: %s",
-          sr.name(),
-          applicabilityResult.get().passingAtoms(),
-          applicabilityResult.get().failingAtoms());
-    }
-    if (submittabilityResult.isPresent()) {
-      logger.atFine().log(
-          "Submittability expression result for SR name '%s':"
-              + " passing atoms: %s, failing atoms: %s",
-          sr.name(),
-          submittabilityResult.get().passingAtoms(),
-          submittabilityResult.get().failingAtoms());
-    }
-    if (overrideResult.isPresent()) {
-      logger.atFine().log(
-          "Override expression result for SR name '%s':" + " passing atoms: %s, failing atoms: %s",
-          sr.name(), overrideResult.get().passingAtoms(), overrideResult.get().failingAtoms());
-    }
+      if (applicabilityResult.isPresent()) {
+        logger.atFine().log(
+            "Applicability expression result for SR name '%s':"
+                + " passing atoms: %s, failing atoms: %s",
+            sr.name(),
+            applicabilityResult.get().passingAtoms(),
+            applicabilityResult.get().failingAtoms());
+      }
+      if (submittabilityResult.isPresent()) {
+        logger.atFine().log(
+            "Submittability expression result for SR name '%s':"
+                + " passing atoms: %s, failing atoms: %s",
+            sr.name(),
+            submittabilityResult.get().passingAtoms(),
+            submittabilityResult.get().failingAtoms());
+      }
+      if (overrideResult.isPresent()) {
+        logger.atFine().log(
+            "Override expression result for SR name '%s':"
+                + " passing atoms: %s, failing atoms: %s",
+            sr.name(), overrideResult.get().passingAtoms(), overrideResult.get().failingAtoms());
+      }
 
-    return SubmitRequirementResult.builder()
-        .legacy(Optional.of(false))
-        .submitRequirement(sr)
-        .patchSetCommitId(cd.currentPatchSet().commitId())
-        .submittabilityExpressionResult(submittabilityResult)
-        .applicabilityExpressionResult(applicabilityResult)
-        .overrideExpressionResult(overrideResult)
-        .build();
+      return SubmitRequirementResult.builder()
+          .legacy(Optional.of(false))
+          .submitRequirement(sr)
+          .patchSetCommitId(cd.currentPatchSet().commitId())
+          .submittabilityExpressionResult(submittabilityResult)
+          .applicabilityExpressionResult(applicabilityResult)
+          .overrideExpressionResult(overrideResult)
+          .build();
+    }
   }
 
   /**
@@ -189,10 +195,16 @@
         TraceContext.newTimer(
             "Get submit requirements",
             Metadata.builder().changeId(cd.change().getId().get()).build())) {
-      ImmutableMap<String, SubmitRequirement> globalRequirements = getGlobalRequirements();
-
-      ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
-      Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements();
+      ImmutableMap<String, SubmitRequirement> globalRequirements;
+      Map<String, SubmitRequirement> projectConfigRequirements;
+      try (TraceTimer timer2 =
+          TraceContext.newTimer(
+              "Read submit requirement definitions",
+              Metadata.builder().changeId(cd.change().getId().get()).build())) {
+        globalRequirements = getGlobalRequirements();
+        ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
+        projectConfigRequirements = state.getSubmitRequirements();
+      }
 
       ImmutableMap<String, SubmitRequirement> requirements =
           Stream.concat(
@@ -229,20 +241,4 @@
                 globalRequirement -> globalRequirement.name().toLowerCase(Locale.US),
                 Function.identity()));
   }
-
-  /** Evaluate the predicate recursively using change data. */
-  private PredicateResult evaluatePredicateTree(
-      Predicate<ChangeData> predicate, ChangeData changeData) {
-    MatchResult match = predicate.asMatchable().matchResult(changeData);
-    PredicateResult.Builder predicateResult =
-        PredicateResult.builder()
-            .predicateString(predicate.isLeaf() ? predicate.getPredicateString() : "")
-            .explanation(predicate.isLeaf() ? match.explanation : "")
-            .status(match.status);
-    predicate
-        .getChildren()
-        .forEach(
-            c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c, changeData)));
-    return predicateResult.build();
-  }
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 265e25d..07c3299 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
@@ -49,9 +50,9 @@
 import com.google.gerrit.entities.LabelTypes;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PredicateResult;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementResult;
@@ -61,6 +62,8 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.index.RefState;
+import com.google.gerrit.index.query.MatchResult;
+import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -82,7 +85,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtilFactory;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.RobotCommentNotes;
 import com.google.gerrit.server.patch.DiffSummary;
 import com.google.gerrit.server.patch.DiffSummaryKey;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -90,12 +92,14 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRequirementsAdapter;
 import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
 import com.google.gerrit.server.project.SubmitRequirementsUtil;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.util.MarkdownImagesUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -113,7 +117,6 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -329,13 +332,40 @@
       ObjectId commitId,
       ChangeNumberVirtualIdAlgorithm virtualIdAlgo,
       @Nullable ChangeNotes changeNotes) {
+    return createForTest(
+        project, id, currentPatchSetId, commitId, virtualIdAlgo, null, changeNotes);
+  }
+
+  /**
+   * Create an instance for testing only.
+   *
+   * <p>Attempting to lazy load data will fail with NPEs. Callers may consider manually setting
+   * fields that can be set.
+   *
+   * @param project project name
+   * @param id change ID
+   * @param currentPatchSetId current patchset number
+   * @param commitId commit SHA1 of the current patchset
+   * @param virtualIdAlgo algorithm for virtualising the Change number
+   * @param changeNotesFactory factory to create change notes
+   * @param changeNotes notes associated with the Change
+   * @return instance for testing.
+   */
+  public static ChangeData createForTest(
+      Project.NameKey project,
+      Change.Id id,
+      int currentPatchSetId,
+      ObjectId commitId,
+      ChangeNumberVirtualIdAlgorithm virtualIdAlgo,
+      ChangeNotes.Factory changeNotesFactory,
+      @Nullable ChangeNotes changeNotes) {
     ChangeData cd =
         new ChangeData(
             null,
             null,
             null,
             null,
-            null,
+            changeNotesFactory,
             null,
             null,
             null,
@@ -352,6 +382,7 @@
             null,
             virtualIdAlgo,
             false,
+            null,
             project,
             id,
             null,
@@ -391,6 +422,7 @@
   private final SubmitRequirementsUtil submitRequirementsUtil;
   private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
   private final boolean skipCurrentRulesEvaluationOnClosedChanges;
+  private final MarkdownImagesUtil markdownImagesUtil;
 
   // Required assisted injected fields.
   private final Project.NameKey project;
@@ -417,7 +449,6 @@
   private List<String> currentFiles;
   private Optional<DiffSummary> diffSummary;
   private List<HumanComment> publishedComments;
-  private List<RobotComment> robotComments;
   private CurrentUser visibleTo;
   private List<ChangeMessage> messages;
   private Optional<ChangedLines> changedLines;
@@ -489,6 +520,7 @@
       SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
       ChangeNumberVirtualIdAlgorithm virtualIdFunc,
       @SkipCurrentRulesEvaluationOnClosedChanges Boolean skipCurrentRulesEvaluationOnClosedChange,
+      MarkdownImagesUtil markdownImagesUtil,
       @Assisted Project.NameKey project,
       @Assisted("changeId") Change.Id id,
       @Assisted("virtualId") @Nullable Change.Id virtualId,
@@ -517,6 +549,7 @@
     this.submitRequirementsUtil = submitRequirementsUtil;
     this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
     this.skipCurrentRulesEvaluationOnClosedChanges = skipCurrentRulesEvaluationOnClosedChange;
+    this.markdownImagesUtil = markdownImagesUtil;
 
     this.project = project;
     this.legacyId = id;
@@ -1131,14 +1164,11 @@
     return publishedComments;
   }
 
-  public Collection<RobotComment> robotComments() {
-    if (robotComments == null) {
-      if (!lazyload()) {
-        return Collections.emptyList();
-      }
-      robotComments = commentsUtil.robotCommentsByChange(notes());
-    }
-    return robotComments;
+  public ImmutableSet<String> getCommentsForIndex() {
+    return publishedComments().stream()
+        .map(c -> c.message)
+        .map(markdownImagesUtil::replaceImagesWithPlaceholder)
+        .collect(ImmutableSet.toImmutableSet());
   }
 
   @Nullable
@@ -1148,11 +1178,26 @@
         return null;
       }
 
-      List<Comment> comments =
-          Stream.concat(publishedComments().stream(), robotComments().stream()).collect(toList());
+      List<Comment> comments = publishedComments().stream().collect(toList());
+      logger.atFine().log(
+          "published comments: %s",
+          comments.stream()
+              .map(
+                  c ->
+                      String.format(
+                          "%s -> {parentUuid = %s, unresolved = %s}",
+                          c.key,
+                          c.parentUuid,
+                          c instanceof HumanComment ? ((HumanComment) c).unresolved : "n/a"))
+              .collect(toImmutableList()));
 
       ImmutableSet<CommentThread<Comment>> commentThreads =
           CommentThreads.forComments(comments).getThreads();
+      logger.atFine().log(
+          "comment threads: %s",
+          commentThreads.stream()
+              .map(t -> t.comments().stream().map(c -> c.key).collect(toImmutableList()))
+              .collect(toImmutableList()));
       unresolvedCommentCount =
           (int) commentThreads.stream().filter(CommentThread::unresolved).count();
     }
@@ -1172,8 +1217,7 @@
       }
 
       // Fail on overflow.
-      totalCommentCount =
-          Ints.checkedCast((long) publishedComments().size() + robotComments().size());
+      totalCommentCount = Ints.checkedCast((long) publishedComments().size());
     }
     return totalCommentCount;
   }
@@ -1565,12 +1609,6 @@
       // refs.
       result.put(project, RefState.create(notes().getRefName(), notes().getMetaId()));
 
-      @SuppressWarnings("unused")
-      var unused = notes().getRobotComments(); // Force loading robot comments.
-
-      RobotCommentNotes robotNotes = notes().getRobotCommentNotes();
-      result.put(project, RefState.create(robotNotes.getRefName(), robotNotes.getMetaId()));
-
       refStates = result.build();
     }
 
@@ -1601,6 +1639,50 @@
     this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
   }
 
+  public PredicateResult evaluatePredicateTree(Predicate<ChangeData> predicate) {
+    MatchResult match = predicate.asMatchable().matchResult(this);
+    PredicateResult.Builder predicateResult =
+        PredicateResult.builder()
+            .predicateString(predicate.isLeaf() ? predicate.getPredicateString() : "")
+            .explanation(predicate.isLeaf() ? match.explanation : "")
+            .status(match.status);
+    predicate
+        .getChildren()
+        .forEach(c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c)));
+    return predicateResult.build();
+  }
+
+  public boolean projectStatePermitsWrite() {
+    return isProjectConfigOnlyChange()
+        || projectCache.get(project()).orElseThrow(illegalState(project())).statePermitsWrite();
+  }
+
+  public void checkStatePermitsWrite() throws ResourceConflictException {
+    if (isProjectConfigOnlyChange()) {
+      return;
+    }
+    projectCache.get(project()).orElseThrow(illegalState(project())).checkStatePermitsWrite();
+  }
+
+  public boolean projectStatePermitsRead() {
+    return isProjectConfigOnlyChange()
+        || projectCache.get(project()).orElseThrow(illegalState(project())).statePermitsRead();
+  }
+
+  public void checkProjectStatePermitsRead() throws ResourceConflictException {
+    if (isProjectConfigOnlyChange()) {
+      return;
+    }
+    projectCache.get(project()).orElseThrow(illegalState(project())).checkStatePermitsRead();
+  }
+
+  private boolean isProjectConfigOnlyChange() {
+    return change().isNew()
+        && change().getDest().branch().equals(RefNames.REFS_CONFIG)
+        && currentFilePaths().size() == 1
+        && currentFilePaths().contains(ProjectConfig.PROJECT_CONFIG);
+  }
+
   private void throwIfNotLazyLoad(String field) {
     if (!lazyload()) {
       // We are not allowed to load values from NoteDb. 'field' was not populated, however,
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index dac0e97..3c535df 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -239,11 +239,15 @@
   public static final String ARG_ID_OWNER = "owner";
   public static final String ARG_ID_NON_UPLOADER = "non_uploader";
   public static final String ARG_ID_NON_CONTRIBUTOR = "non_contributor";
+  public static final String ARG_ID_NON_AUTHOR = "non_author";
+  public static final String ARG_ID_NON_COMMITTER = "non_committer";
   public static final String ARG_COUNT = "count";
   public static final String ARG_USERS = "users";
   public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
   public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
   public static final Account.Id NON_CONTRIBUTOR_ACCOUNT_ID = Account.id(-2);
+  public static final Account.Id NON_AUTHOR_ACCOUNT_ID = Account.id(-3);
+  public static final Account.Id NON_COMMITTER_ACCOUNT_ID = Account.id(-4);
   public static final Account.Id NON_EXISTING_ACCOUNT_ID = Account.id(-1000);
 
   public static final String OPERATOR_MERGED_BEFORE = "mergedbefore";
@@ -805,6 +809,10 @@
       return new IsSubmittablePredicate();
     }
 
+    if ("consistent".equalsIgnoreCase(value)) {
+      return new IsConsistentPredicate();
+    }
+
     if ("started".equalsIgnoreCase(value)) {
       checkOperatorAvailable(ChangeField.STARTED_SPEC, "is:started");
       return new BooleanPredicate(ChangeField.STARTED_SPEC);
@@ -1099,6 +1107,9 @@
       assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
       assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
 
+      // Using "user=" or "group=" args is not supported.
+      assertDisjunctive(lblArgs, ARG_ID_USER, ARG_ID_GROUP);
+
       for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
         String key = pair.getKey();
         String value = pair.getValue().value();
@@ -1110,6 +1121,10 @@
             accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
           } else if (value.equals(ARG_ID_NON_CONTRIBUTOR)) {
             accounts = Collections.singleton(NON_CONTRIBUTOR_ACCOUNT_ID);
+          } else if (value.equals(ARG_ID_NON_AUTHOR)) {
+            accounts = Collections.singleton(NON_AUTHOR_ACCOUNT_ID);
+          } else if (value.equals(ARG_ID_NON_COMMITTER)) {
+            accounts = Collections.singleton(NON_COMMITTER_ACCOUNT_ID);
           } else {
             accounts = parseAccountIgnoreVisibility(value);
           }
@@ -1148,6 +1163,10 @@
           accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
         } else if (value.equals(ARG_ID_NON_CONTRIBUTOR)) {
           accounts = Collections.singleton(NON_CONTRIBUTOR_ACCOUNT_ID);
+        } else if (value.equals(ARG_ID_NON_AUTHOR)) {
+          accounts = Collections.singleton(NON_AUTHOR_ACCOUNT_ID);
+        } else if (value.equals(ARG_ID_NON_COMMITTER)) {
+          accounts = Collections.singleton(NON_COMMITTER_ACCOUNT_ID);
         } else {
           accounts = parseAccountIgnoreVisibility(value);
         }
@@ -1188,8 +1207,16 @@
   }
 
   protected void validateLabelArgs(Set<Account.Id> accounts) throws QueryParseException {
-    if (accounts != null && accounts.contains(NON_CONTRIBUTOR_ACCOUNT_ID)) {
-      throw new QueryParseException("non_contributor arg is not allowed in change queries");
+    if (accounts != null) {
+      if (accounts.contains(NON_CONTRIBUTOR_ACCOUNT_ID)) {
+        throw new QueryParseException("non_contributor arg is not allowed in change queries");
+      }
+      if (accounts.contains(NON_AUTHOR_ACCOUNT_ID)) {
+        throw new QueryParseException("non_author arg is not allowed in change queries");
+      }
+      if (accounts.contains(NON_COMMITTER_ACCOUNT_ID)) {
+        throw new QueryParseException("non_committer arg is not allowed in change queries");
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 3ada9d7..7660b93 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -145,7 +145,7 @@
   }
 
   public PluginDefinedInfosFactory getInfosFactory() {
-    return this::createPluginDefinedInfos;
+    return this;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index d949ea8..2303e96 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.common.Nullable;
@@ -24,6 +25,7 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -45,58 +47,62 @@
   private static final String INVALID_STATUS = "__invalid__";
   static final Predicate<ChangeData> NONE = new ChangeStatusPredicate(null);
 
-  private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
-  private static final Predicate<ChangeData> CLOSED;
-  private static final Predicate<ChangeData> OPEN;
+  private static final TreeMap<String, Provider<Predicate<ChangeData>>> PREDICATES;
+  private static final Provider<Predicate<ChangeData>> CLOSED;
+  private static final Provider<Predicate<ChangeData>> OPEN;
 
   static {
     PREDICATES = new TreeMap<>();
-    List<Predicate<ChangeData>> open = new ArrayList<>();
-    List<Predicate<ChangeData>> closed = new ArrayList<>();
+    List<Change.Status> openStatuses = new ArrayList<>();
+    List<Change.Status> closedStatuses = new ArrayList<>();
 
     for (Change.Status s : Change.Status.values()) {
-      ChangeStatusPredicate p = forStatus(s);
       String str = canonicalize(s);
       checkState(
           !INVALID_STATUS.equals(str),
           "invalid status sentinel %s cannot match canonicalized status string %s",
           INVALID_STATUS,
           str);
-      PREDICATES.put(str, p);
-      (s.isOpen() ? open : closed).add(p);
+      PREDICATES.put(str, () -> forStatus(s));
+      (s.isOpen() ? openStatuses : closedStatuses).add(s);
     }
 
-    CLOSED = Predicate.or(closed);
-    OPEN = Predicate.or(open);
+    CLOSED = () -> statusesToPrecidate(closedStatuses);
+    OPEN = () -> statusesToPrecidate(openStatuses);
 
     PREDICATES.put("closed", CLOSED);
     PREDICATES.put("open", OPEN);
     PREDICATES.put("pending", OPEN);
   }
 
+  private static Predicate<ChangeData> statusesToPrecidate(List<Change.Status> statuses) {
+    return Predicate.or(
+        statuses.stream().map(ChangeStatusPredicate::forStatus).collect(toImmutableList()));
+  }
+
   public static String canonicalize(Change.Status status) {
     return status.name().toLowerCase(Locale.US);
   }
 
   public static Predicate<ChangeData> parse(String value) throws QueryParseException {
     String lower = value.toLowerCase(Locale.US);
-    NavigableMap<String, Predicate<ChangeData>> head = PREDICATES.tailMap(lower, true);
+    NavigableMap<String, Provider<Predicate<ChangeData>>> head = PREDICATES.tailMap(lower, true);
     if (!head.isEmpty()) {
       // Assume no statuses share a common prefix so we can only walk one entry.
-      Map.Entry<String, Predicate<ChangeData>> e = head.entrySet().iterator().next();
+      Map.Entry<String, Provider<Predicate<ChangeData>>> e = head.entrySet().iterator().next();
       if (e.getKey().startsWith(lower)) {
-        return e.getValue();
+        return e.getValue().get();
       }
     }
     throw new QueryParseException("Unrecognized value: " + value);
   }
 
   public static Predicate<ChangeData> open() {
-    return OPEN;
+    return OPEN.get();
   }
 
   public static Predicate<ChangeData> closed() {
-    return CLOSED;
+    return CLOSED.get();
   }
 
   public static ChangeStatusPredicate forStatus(Change.Status status) {
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
index 889c237..369ddd2 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelTypes;
 import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.index.query.PostFilterPredicate;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -302,17 +301,17 @@
         if (account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
             && cd.currentPatchSet().uploader().equals(approver)) {
           logger.atFine().log(
-              "vote %s on change %s doesn't match since it is not from the uploader %s",
+              "vote %s on change %s doesn't match since it is from the uploader %s",
               psa, cd.change().getChangeId(), cd.currentPatchSet().uploader());
           return false;
         }
 
         if (account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID)) {
-          if ((cd.currentPatchSet().uploader().equals(approver)
+          if (cd.currentPatchSet().uploader().equals(approver)
               || matchAccount(cd.getCommitter().getEmailAddress(), approver)
-              || matchAccount(cd.getAuthor().getEmailAddress(), approver))) {
+              || matchAccount(cd.getAuthor().getEmailAddress(), approver)) {
             logger.atFine().log(
-                "vote %s on change %s doesn't match since it is not from a contributor"
+                "vote %s on change %s doesn't match since it is from a contributor"
                     + " (uploader: %s, committer: %s, author: %s)",
                 psa,
                 cd.change().getChangeId(),
@@ -322,6 +321,24 @@
             return false;
           }
         }
+
+        if (account.equals(ChangeQueryBuilder.NON_AUTHOR_ACCOUNT_ID)) {
+          if (matchAccount(cd.getAuthor().getEmailAddress(), approver)) {
+            logger.atFine().log(
+                "vote %s on change %s doesn't match since it is from the author %s",
+                psa, cd.change().getChangeId(), cd.getAuthor().getEmailAddress());
+            return false;
+          }
+        }
+
+        if (account.equals(ChangeQueryBuilder.NON_COMMITTER_ACCOUNT_ID)) {
+          if (matchAccount(cd.getCommitter().getEmailAddress(), approver)) {
+            logger.atFine().log(
+                "vote %s on change %s doesn't match since it is from the committer %s",
+                psa, cd.change().getChangeId(), cd.getCommitter().getEmailAddress());
+            return false;
+          }
+        }
       }
 
       if (group != null) {
@@ -336,23 +353,28 @@
       }
 
       // Check the user has 'READ' permission.
+      PermissionBackend.ForChange perm = permissionBackend.absentUser(approver).change(cd);
+      if (!cd.projectStatePermitsRead()) {
+        logger.atFine().log(
+            "vote %s on change %s doesn't match since the project %s doesn't permit read",
+            psa, cd.change().getChangeId(), cd.project().get());
+        return false;
+      }
       try {
-        PermissionBackend.ForChange perm = permissionBackend.absentUser(approver).change(cd);
-        if (!projectCache.get(cd.project()).map(ProjectState::statePermitsRead).orElse(false)) {
-          logger.atFine().log(
-              "vote %s on change %s doesn't match since the project %s doesn't permit read",
-              psa, cd.change().getChangeId(), cd.project().get());
-          return false;
+        if (perm.test(ChangePermission.READ)) {
+          logger.atFine().log("vote %s on change %s matches", psa, cd.change().getChangeId());
+          return true;
         }
-
-        perm.check(ChangePermission.READ);
-        logger.atFine().log("vote %s on change %s matches", psa, cd.change().getChangeId());
-        return true;
-      } catch (PermissionBackendException | AuthException e) {
         logger.atFine().log(
             "vote %s on change %s doesn't match because the approver %s has no read access",
             psa, cd.change().getChangeId(), approver);
         return false;
+      } catch (PermissionBackendException e) {
+        logger.atFine().log(
+            "vote %s on change %s doesn't match because we failed to compute if the approver %s has"
+                + " read access",
+            psa, cd.change().getChangeId(), approver);
+        return false;
       }
     }
 
@@ -374,7 +396,9 @@
       return account != null
           && (account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
               || account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
-              || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID));
+              || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID)
+              || account.equals(ChangeQueryBuilder.NON_AUTHOR_ACCOUNT_ID)
+              || account.equals(ChangeQueryBuilder.NON_COMMITTER_ACCOUNT_ID));
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 951c69a..dff7cdd 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -122,6 +122,11 @@
     return query(ChangePredicates.changeNumber(id, queryBuilderArgsProvider.get()));
   }
 
+  public List<ChangeData> byProjectChangeNumber(Project.NameKey project, Change.Id id) {
+    return query(
+        and(project(project), ChangePredicates.changeNumber(id, queryBuilderArgsProvider.get())));
+  }
+
   @UsedAt(UsedAt.Project.GOOGLE)
   public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
     List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
diff --git a/java/com/google/gerrit/server/query/change/IsConsistentPredicate.java b/java/com/google/gerrit/server/query/change/IsConsistentPredicate.java
new file mode 100644
index 0000000..d6ca5f5
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/IsConsistentPredicate.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.index.query.PostFilterPredicate;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.NoSuchChangeException;
+
+public class IsConsistentPredicate extends PostFilterPredicate<ChangeData> {
+
+  public IsConsistentPredicate() {
+    super("is", "consistent");
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    try {
+      ChangeNotes ignored = cd.notes();
+      return true;
+    } catch (NoSuchChangeException nsce) {
+      return false;
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 3;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 5a6f75f..70ba6c5 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -12,6 +12,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:converters",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
@@ -23,6 +24,7 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/flow",
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/util/time",
diff --git a/java/com/google/gerrit/server/restapi/RestApiModule.java b/java/com/google/gerrit/server/restapi/RestApiModule.java
index 3bc5bb8..6982028 100644
--- a/java/com/google/gerrit/server/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/RestApiModule.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.restapi.change.ChangeRestApiModule;
 import com.google.gerrit.server.restapi.config.ConfigRestApiModule;
 import com.google.gerrit.server.restapi.config.RestCacheAdminModule;
+import com.google.gerrit.server.restapi.flow.FlowRestApiModule;
 import com.google.gerrit.server.restapi.group.GroupRestApiModule;
 import com.google.gerrit.server.restapi.project.ProjectRestApiModule;
 import com.google.inject.AbstractModule;
@@ -40,6 +41,7 @@
     install(new ChangeRestApiModule());
     install(new ConfigRestApiModule());
     install(new RestCacheAdminModule());
+    install(new FlowRestApiModule());
     install(new GroupRestApiModule());
     install(new PluginRestApiModule());
     install(new ProjectRestApiModule());
diff --git a/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java b/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java
index adf4bdd..36fb452 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.account.AccountResource.SSH_KEY_KIND;
 import static com.google.gerrit.server.account.AccountResource.STARRED_CHANGE_KIND;
 import static com.google.gerrit.server.account.AccountResource.Star.STAR_KIND;
+import static com.google.gerrit.server.account.AccountResource.TOKEN_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -34,6 +35,7 @@
     DynamicMap.mapOf(binder(), ACCOUNT_KIND);
     DynamicMap.mapOf(binder(), CAPABILITY_KIND);
     DynamicMap.mapOf(binder(), EMAIL_KIND);
+    DynamicMap.mapOf(binder(), TOKEN_KIND);
     DynamicMap.mapOf(binder(), SSH_KEY_KIND);
     DynamicMap.mapOf(binder(), STAR_KIND);
     DynamicMap.mapOf(binder(), STARRED_CHANGE_KIND);
@@ -71,8 +73,6 @@
     get(ACCOUNT_KIND, "name").to(GetName.class);
     put(ACCOUNT_KIND, "name").to(PutName.class);
     delete(ACCOUNT_KIND, "name").to(PutName.class);
-    put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
-    delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
     get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
     put(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
     get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
@@ -80,6 +80,13 @@
     get(ACCOUNT_KIND, "preferences.edit").to(GetEditPreferences.class);
     put(ACCOUNT_KIND, "preferences.edit").to(SetEditPreferences.class);
 
+    put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+    delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+
+    child(ACCOUNT_KIND, "tokens").to(TokensCollection.class);
+    create(TOKEN_KIND).to(CreateToken.class);
+    delete(TOKEN_KIND).to(DeleteToken.class);
+
     child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
     postOnCollection(SSH_KEY_KIND).to(AddSshKey.class);
     get(SSH_KEY_KIND).to(GetSshKey.class);
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 74caf74..bccc12d 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -42,6 +43,9 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -58,10 +62,13 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -79,6 +86,7 @@
   private final Sequences seq;
   private final GroupResolver groupResolver;
   private final VersionedAuthorizedKeys.Accessor authorizedKeys;
+  private final AuthTokenAccessor tokensAccessor;
   private final SshKeyCache sshKeyCache;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final AccountLoader.Factory infoLoader;
@@ -87,12 +95,14 @@
   private final OutgoingEmailValidator validator;
   private final AuthConfig authConfig;
   private final ExternalIdFactory externalIdFactory;
+  private final Optional<Duration> maxAuthTokenLifetime;
 
   @Inject
   CreateAccount(
       Sequences seq,
       GroupResolver groupResolver,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
+      AuthTokenAccessor tokensAccessor,
       SshKeyCache sshKeyCache,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       AccountLoader.Factory infoLoader,
@@ -104,6 +114,7 @@
     this.seq = seq;
     this.groupResolver = groupResolver;
     this.authorizedKeys = authorizedKeys;
+    this.tokensAccessor = tokensAccessor;
     this.sshKeyCache = sshKeyCache;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.infoLoader = infoLoader;
@@ -112,6 +123,7 @@
     this.validator = validator;
     this.authConfig = authConfig;
     this.externalIdFactory = externalIdFactory;
+    this.maxAuthTokenLifetime = authConfig.getMaxAuthTokenLifetime();
   }
 
   @Override
@@ -122,7 +134,8 @@
           UnprocessableEntityException,
           IOException,
           ConfigInvalidException,
-          PermissionBackendException {
+          PermissionBackendException,
+          InvalidAuthTokenException {
     return apply(id, input != null ? input : new AccountInput());
   }
 
@@ -132,7 +145,8 @@
           UnprocessableEntityException,
           IOException,
           ConfigInvalidException,
-          PermissionBackendException {
+          PermissionBackendException,
+          InvalidAuthTokenException {
     String username = applyCaseOfUsername(id.get());
     if (input.username != null && !username.equals(applyCaseOfUsername(input.username))) {
       throw new BadRequestException("username must match URL");
@@ -157,7 +171,7 @@
       extIds.add(externalIdFactory.createEmail(accountId, input.email));
     }
 
-    extIds.add(externalIdFactory.createUsername(username, accountId, input.httpPassword));
+    extIds.add(externalIdFactory.createUsername(username, accountId));
     externalIdCreators.runEach(c -> extIds.addAll(c.create(accountId, username, input.email)));
 
     try {
@@ -197,6 +211,28 @@
       }
     }
 
+    Optional<Instant> defaultExpiration = Optional.empty();
+    if (maxAuthTokenLifetime.isPresent()) {
+      defaultExpiration = Optional.of(Instant.now().plus(maxAuthTokenLifetime.get()));
+    }
+
+    List<AuthToken> tokens = new ArrayList<>();
+    if (input.tokens != null) {
+      for (AuthTokenInput token : input.tokens) {
+        tokens.add(
+            AuthToken.createWithPlainToken(
+                token.id, token.token, CreateToken.getExpirationInstant(token, defaultExpiration)));
+      }
+    }
+
+    if (input.httpPassword != null) {
+      tokens.add(AuthToken.createWithPlainToken("legacy", input.httpPassword));
+    }
+
+    if (!tokens.isEmpty()) {
+      tokensAccessor.addTokens(accountId, tokens);
+    }
+
     AccountLoader loader = infoLoader.create(true);
     AccountInfo info = loader.get(accountId);
     loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/account/CreateToken.java b/java/com/google/gerrit/server/restapi/account/CreateToken.java
new file mode 100644
index 0000000..da83a03
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/CreateToken.java
@@ -0,0 +1,214 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.account;
+
+import static com.google.gerrit.server.mail.EmailFactories.AUTH_TOKEN_UPDATED;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.io.BaseEncoding;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.mail.EmailFactories;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.sql.Timestamp;
+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;
+
+/**
+ * REST endpoint to set an authentication token of an account.
+ *
+ * <p>This REST endpoint handles {@code PUT
+ * /accounts/<account-identifier>/tokens/<token-identifier>} requests.
+ *
+ * <p>Gerrit only stores the hash of the token, hence if a token was set it's not possible to get it
+ * back from Gerrit.
+ */
+@Singleton
+public class CreateToken
+    implements RestCollectionCreateView<AccountResource, AccountResource.Token, AuthTokenInput> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final int LEN = 31;
+  private static final SecureRandom rng;
+
+  static {
+    try {
+      rng = SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalStateException("Cannot create RNG for password generator", e);
+    }
+  }
+
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final EmailFactories emailFactories;
+  private final AuthTokenAccessor tokensAccessor;
+  private final Optional<Duration> maxAuthTokenLifetime;
+
+  @Inject
+  CreateToken(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      EmailFactories emailFactories,
+      AuthTokenAccessor tokensAccessor,
+      AuthConfig authConfig) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.emailFactories = emailFactories;
+    this.tokensAccessor = tokensAccessor;
+
+    this.maxAuthTokenLifetime = authConfig.getMaxAuthTokenLifetime();
+  }
+
+  @Override
+  @CanIgnoreReturnValue
+  public Response<AuthTokenInfo> apply(AccountResource rsrc, IdString id, AuthTokenInput input)
+      throws IOException,
+          ConfigInvalidException,
+          PermissionBackendException,
+          BadRequestException,
+          InvalidAuthTokenException,
+          RestApiException {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+    }
+
+    if (rsrc.getUser().getUserName().isEmpty()) {
+      throw new ResourceConflictException("A username is required to use basic authentication.");
+    }
+
+    if (input == null) {
+      input = new AuthTokenInput();
+    }
+
+    if (input.id != null && !input.id.equals(id.get())) {
+      throw new ResourceConflictException("Token ID must match in URL and input");
+    }
+
+    return apply(rsrc.getUser(), id.get(), input);
+  }
+
+  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
+  public Response<AuthTokenInfo> apply(IdentifiedUser user, String id, AuthTokenInput input)
+      throws IOException, ConfigInvalidException, RestApiException, PermissionBackendException {
+    String newToken;
+    if (Strings.isNullOrEmpty(input.token)) {
+      newToken = generate();
+    } else {
+      // Only administrators can explicitly set a token.
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+      newToken = input.token;
+    }
+
+    Optional<Instant> defaultExpiration = Optional.empty();
+    if (maxAuthTokenLifetime.isPresent()) {
+      defaultExpiration = Optional.of(Instant.now().plus(maxAuthTokenLifetime.get()));
+    }
+
+    Optional<Instant> expiration = getExpirationInstant(input, defaultExpiration);
+    AuthToken token;
+    try {
+      token = tokensAccessor.addPlainToken(user.getAccountId(), id, newToken, expiration);
+    } catch (InvalidAuthTokenException e) {
+      throw new BadRequestException(e.getMessage(), e);
+    }
+    try {
+      emailFactories
+          .createOutgoingEmail(
+              AUTH_TOKEN_UPDATED, emailFactories.createAuthTokenUpdateEmail(user, "added", id))
+          .send();
+    } catch (EmailException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot send HttpPassword update message to %s", user.getAccount().preferredEmail());
+    }
+
+    AuthTokenInfo info = new AuthTokenInfo();
+    info.id = token.id();
+    info.token = newToken;
+    if (token.expirationDate().isPresent()) {
+      info.expiration = Timestamp.from(token.expirationDate().get());
+    }
+    return Response.created(info);
+  }
+
+  public static Optional<Instant> getExpirationInstant(
+      AuthTokenInput input, Optional<Instant> defaultExpiration) throws BadRequestException {
+    return getExpirationInstant(input.lifetime, defaultExpiration);
+  }
+
+  public static Optional<Instant> getExpirationInstant(
+      String lifetime, Optional<Instant> defaultExpiration) throws BadRequestException {
+    if (Strings.isNullOrEmpty(lifetime)) {
+      return defaultExpiration;
+    }
+    long lifetimeMinutes;
+    try {
+      lifetimeMinutes = ConfigUtil.getTimeUnit(lifetime, 0, TimeUnit.MINUTES);
+    } catch (IllegalArgumentException e) {
+      throw new BadRequestException("Invalid lifetime: " + lifetime, e);
+    }
+    if (lifetimeMinutes <= 0) {
+      throw new BadRequestException("Lifetime must be larger than 0");
+    }
+    return Optional.of(Instant.now().plus(lifetimeMinutes, ChronoUnit.MINUTES));
+  }
+
+  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
+  public static String generate() {
+    byte[] rand = new byte[LEN];
+    rng.nextBytes(rand);
+
+    byte[] enc = BaseEncoding.base64().encode(rand).getBytes(UTF_8);
+    StringBuilder r = new StringBuilder(enc.length);
+    for (int i = 0; i < enc.length; i++) {
+      if (enc[i] == '=') {
+        break;
+      }
+      r.append((char) enc[i]);
+    }
+    return r.toString();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteAccount.java b/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
index 47a7c8b..7239075 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.server.restapi.account;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
@@ -38,12 +41,15 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.AuthTokenAccessor;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.config.AccountConfig;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.db.GroupDelta;
+import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -53,6 +59,8 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -69,6 +77,8 @@
  */
 @Singleton
 public class DeleteAccount implements RestModifyView<AccountResource, Input> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final Provider<CurrentUser> self;
   private final Provider<PersonIdent> serverIdent;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -83,12 +93,15 @@
   private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
   private final PublicKeyStoreUtil publicKeyStoreUtil;
   private final AccountConfig accountConfig;
+  private final Provider<GroupsUpdate> groupsUpdateProvider;
+  private final AuthTokenAccessor tokenAccessor;
 
   @Inject
   public DeleteAccount(
       Provider<CurrentUser> self,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+      @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
       VersionedAuthorizedKeys.Accessor authorizedKeys,
       SshKeyCache sshKeyCache,
       StarredChangesReader starredChangesReader,
@@ -99,7 +112,8 @@
       ChangeEditUtil changeEditUtil,
       PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
       PublicKeyStoreUtil publicKeyStoreUtil,
-      AccountConfig accountConfig) {
+      AccountConfig accountConfig,
+      AuthTokenAccessor tokenAccessor) {
     this.self = self;
     this.serverIdent = serverIdent;
     this.accountsUpdateProvider = accountsUpdateProvider;
@@ -114,6 +128,8 @@
     this.accountPatchReviewStore = accountPatchReviewStore;
     this.publicKeyStoreUtil = publicKeyStoreUtil;
     this.accountConfig = accountConfig;
+    this.groupsUpdateProvider = groupsUpdateProvider;
+    this.tokenAccessor = tokenAccessor;
   }
 
   @Override
@@ -134,8 +150,10 @@
       deleteSshKeys(user);
       deleteStarredChanges(userId);
       deleteChangeEdits(userId);
+      tokenAccessor.deleteAllTokens(user.getAccountId());
       deleteDraftCommentsUtil.deleteDraftComments(user, null);
       accountPatchReviewStore.run(a -> a.clearReviewedBy(userId));
+      removeUserFromGroups(user);
       accountsUpdateProvider
           .get()
           .delete("Deleting user through `DELETE /accounts/{ID}`", user.getAccountId());
@@ -145,6 +163,31 @@
     return Response.none();
   }
 
+  private void removeUserFromGroups(IdentifiedUser user) {
+    Account.Id accountId = user.getAccountId();
+
+    GroupDelta groupDelta =
+        GroupDelta.builder()
+            .setMemberModification(memberIds -> Sets.difference(memberIds, Set.of(accountId)))
+            .build();
+
+    Set<AccountGroup.UUID> internalGroups =
+        user.getEffectiveGroups().getKnownGroups().stream()
+            .filter(g -> g.isInternalGroup())
+            .collect(Collectors.toSet());
+
+    internalGroups.forEach(
+        groupUuid -> {
+          try {
+            groupsUpdateProvider.get().updateGroup(groupUuid, groupDelta);
+            logger.atInfo().log("User: %s removed from Group: %s", accountId, groupUuid);
+          } catch (Exception e) {
+            logger.atWarning().withCause(e).log(
+                "User: %s could not be removed from Group: %s ", accountId, groupUuid);
+          }
+        });
+  }
+
   private void deletePgpKeys(IdentifiedUser user) {
     if (!publicKeyStoreUtil.hasInitializedPublicKeyStore()) {
       return;
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index e099a70..8b19f59 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -91,7 +91,7 @@
             .collect(toMap(ExternalId::key, Function.identity()));
 
     List<ExternalId> toDelete = new ArrayList<>();
-    Optional<ExternalId.Key> last = resource.getUser().getLastLoginExternalIdKey();
+    Optional<ExternalId.Key> loginExternalIdKey = resource.getUser().getLastLoginExternalIdKey();
     for (String externalIdStr : extIds) {
       ExternalId id = externalIdMap.get(externalIdKeyFactory.parse(externalIdStr));
 
@@ -100,22 +100,25 @@
             String.format("External id %s does not exist", externalIdStr));
       }
 
-      if (!last.isPresent() || !last.get().equals(id.key())) {
-        if (id.isScheme(SCHEME_USERNAME)) {
-          if (self.get().hasSameAccountId(resource.getUser())) {
-            throw new AuthException("User cannot delete its own externalId in 'username:' scheme");
-          }
-          permissionBackend
-              .currentUser()
-              .checkAny(
-                  ImmutableSet.of(
-                      GlobalPermission.ADMINISTRATE_SERVER, GlobalPermission.MAINTAIN_SERVER));
-        }
-        toDelete.add(id);
-      } else {
+      if (loginExternalIdKey.isPresent() && loginExternalIdKey.get().equals(id.key())) {
         throw new ResourceConflictException(
             String.format("External id %s cannot be deleted", externalIdStr));
       }
+
+      if (id.isScheme(SCHEME_USERNAME)) {
+        if (self.get().hasSameAccountId(resource.getUser())) {
+          throw new AuthException("User cannot delete its own externalId in 'username:' scheme");
+        }
+        // TODO: Define a consistent threat model around deleting external ids and remove
+        // this special case
+        permissionBackend
+            .currentUser()
+            .checkAny(
+                ImmutableSet.of(
+                    GlobalPermission.ADMINISTRATE_SERVER, GlobalPermission.MAINTAIN_SERVER));
+      }
+
+      toDelete.add(id);
     }
 
     try {
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteToken.java b/java/com/google/gerrit/server/restapi/account/DeleteToken.java
new file mode 100644
index 0000000..2efbc5c
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/DeleteToken.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.account;
+
+import static com.google.gerrit.server.mail.EmailFactories.AUTH_TOKEN_UPDATED;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
+import com.google.gerrit.server.mail.EmailFactories;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+/**
+ * REST endpoint to delete a token of an account.
+ *
+ * <p>This REST endpoint handles {@code DELETE /accounts/<account-identifier>/tokens/<token-id>}
+ * requests.
+ */
+@Singleton
+public class DeleteToken implements RestModifyView<AccountResource.Token, Input> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final AuthTokenAccessor tokenAccessor;
+  private final EmailFactories emailFactories;
+
+  @Inject
+  DeleteToken(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      AuthTokenAccessor tokenAccessor,
+      EmailFactories emailFactories) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.tokenAccessor = tokenAccessor;
+    this.emailFactories = emailFactories;
+  }
+
+  @Override
+  public Response<String> apply(AccountResource.Token rsrc, Input input)
+      throws AuthException,
+          RepositoryNotFoundException,
+          IOException,
+          ConfigInvalidException,
+          PermissionBackendException,
+          InvalidAuthTokenException {
+    return apply(rsrc.getUser(), rsrc.getId(), true);
+  }
+
+  @CanIgnoreReturnValue
+  public Response<String> apply(IdentifiedUser user, String id, boolean notify)
+      throws RepositoryNotFoundException,
+          IOException,
+          ConfigInvalidException,
+          AuthException,
+          PermissionBackendException,
+          InvalidAuthTokenException {
+    if (!self.get().hasSameAccountId(user)) {
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+    }
+    Account.Id accountId = user.getAccountId();
+    tokenAccessor.deleteToken(accountId, id);
+    if (notify) {
+      try {
+        emailFactories
+            .createOutgoingEmail(
+                AUTH_TOKEN_UPDATED, emailFactories.createAuthTokenUpdateEmail(user, "deleted", id))
+            .send();
+      } catch (EmailException e) {
+        logger.atSevere().withCause(e).log(
+            "Cannot send token deletion message to %s", user.getAccount().preferredEmail());
+      }
+    }
+
+    return Response.none();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/account/GetTokens.java b/java/com/google/gerrit/server/restapi/account/GetTokens.java
new file mode 100644
index 0000000..bc97b8c
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/GetTokens.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.account;
+
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+/**
+ * REST endpoint to list the tokens of an account.
+ *
+ * <p>This REST endpoint handles {@code GET /accounts/<account-identifier>/tokens/} requests.
+ */
+@Singleton
+public class GetTokens implements RestReadView<AccountResource> {
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final AuthTokenAccessor tokenAccessor;
+
+  @Inject
+  GetTokens(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      AuthTokenAccessor tokenAccessor) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.tokenAccessor = tokenAccessor;
+  }
+
+  @Override
+  public Response<List<AuthTokenInfo>> apply(AccountResource rsrc)
+      throws AuthException,
+          PermissionBackendException,
+          RepositoryNotFoundException,
+          IOException,
+          ConfigInvalidException {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
+    }
+    return Response.ok(apply(rsrc.getUser()));
+  }
+
+  public List<AuthTokenInfo> apply(IdentifiedUser user) throws IOException, ConfigInvalidException {
+    List<AuthTokenInfo> authTokenInfos = new ArrayList<>();
+    for (AuthToken token : tokenAccessor.getTokens(user.getAccountId())) {
+      authTokenInfos.add(newTokenInfo(token));
+    }
+    return authTokenInfos;
+  }
+
+  public static AuthTokenInfo newTokenInfo(AuthToken token) {
+    AuthTokenInfo info = new AuthTokenInfo();
+    info.id = token.id();
+    if (token.expirationDate().isPresent()) {
+      info.expiration = Timestamp.from(token.expirationDate().get());
+    }
+    return info;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index a3478f7..06579ac 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -14,26 +14,28 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import static com.google.gerrit.server.account.DirectAuthTokenAccessor.LEGACY_ID;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static com.google.gerrit.server.mail.EmailFactories.PASSWORD_UPDATED;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.google.common.io.BaseEncoding;
-import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.common.HttpPasswordInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
+import com.google.gerrit.server.account.PasswordMigrator;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
@@ -46,8 +48,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -61,54 +61,53 @@
  * possible to get it back from Gerrit.
  */
 @Singleton
+@Deprecated
 public class PutHttpPassword implements RestModifyView<AccountResource, HttpPasswordInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private static final int LEN = 31;
-  private static final SecureRandom rng;
-
-  static {
-    try {
-      rng = SecureRandom.getInstance("SHA1PRNG");
-    } catch (NoSuchAlgorithmException e) {
-      throw new IllegalStateException("Cannot create RNG for password generator", e);
-    }
-  }
-
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
-  private final ExternalIds externalIds;
+  private final AuthTokenAccessor tokenAccessor;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
-  private final EmailFactories emailFactories;
+  private final ExternalIds externalIds;
   private final ExternalIdFactory externalIdFactory;
   private final ExternalIdKeyFactory externalIdKeyFactory;
+  private final CreateToken putToken;
+  private final DeleteToken deleteToken;
+  private final EmailFactories emailFactories;
 
   @Inject
   PutHttpPassword(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
-      ExternalIds externalIds,
+      AuthTokenAccessor tokenAccessor,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
-      EmailFactories emailFactories,
+      ExternalIds externalIds,
       ExternalIdFactory externalIdFactory,
-      ExternalIdKeyFactory externalIdKeyFactory) {
+      ExternalIdKeyFactory externalIdKeyFactory,
+      CreateToken putToken,
+      DeleteToken deleteToken,
+      EmailFactories emailFactories) {
     this.self = self;
     this.permissionBackend = permissionBackend;
-    this.externalIds = externalIds;
+    this.tokenAccessor = tokenAccessor;
     this.accountsUpdateProvider = accountsUpdateProvider;
-    this.emailFactories = emailFactories;
+    this.externalIds = externalIds;
     this.externalIdFactory = externalIdFactory;
     this.externalIdKeyFactory = externalIdKeyFactory;
+    this.putToken = putToken;
+    this.deleteToken = deleteToken;
+    this.emailFactories = emailFactories;
   }
 
   @Override
   public Response<String> apply(AccountResource rsrc, HttpPasswordInput input)
-      throws AuthException,
-          ResourceNotFoundException,
-          ResourceConflictException,
-          IOException,
+      throws IOException,
           ConfigInvalidException,
-          PermissionBackendException {
+          PermissionBackendException,
+          BadRequestException,
+          RestApiException,
+          InvalidAuthTokenException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
@@ -116,70 +115,52 @@
     if (input == null) {
       input = new HttpPasswordInput();
     }
+
     input.httpPassword = Strings.emptyToNull(input.httpPassword);
+    boolean isDeleteOp = !input.generate && input.httpPassword == null;
 
-    String newPassword;
-    if (input.generate) {
-      newPassword = generate();
-    } else if (input.httpPassword == null) {
-      newPassword = null;
-    } else {
-      // Only administrators can explicitly set the password.
-      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-      newPassword = input.httpPassword;
-    }
-    return apply(rsrc.getUser(), newPassword);
-  }
-
-  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
-  public Response<String> apply(IdentifiedUser user, String newPassword)
-      throws ResourceNotFoundException,
-          ResourceConflictException,
-          IOException,
-          ConfigInvalidException {
-    String userName =
-        user.getUserName().orElseThrow(() -> new ResourceConflictException("username must be set"));
-    Optional<ExternalId> optionalExtId =
-        externalIds.get(externalIdKeyFactory.create(SCHEME_USERNAME, userName));
-    ExternalId extId = optionalExtId.orElseThrow(ResourceNotFoundException::new);
-    accountsUpdateProvider
-        .get()
-        .update(
-            "Set HTTP Password via API",
-            extId.accountId(),
-            u ->
-                u.updateExternalId(
-                    externalIdFactory.createWithPassword(
-                        extId.key(), extId.accountId(), extId.email(), newPassword)));
-
-    try {
-      emailFactories
-          .createOutgoingEmail(
-              PASSWORD_UPDATED,
-              emailFactories.createHttpPasswordUpdateEmail(
-                  user, newPassword == null ? "deleted" : "added or updated"))
-          .send();
-    } catch (EmailException e) {
-      logger.atSevere().withCause(e).log(
-          "Cannot send HttpPassword update message to %s", user.getAccount().preferredEmail());
-    }
-
-    return Strings.isNullOrEmpty(newPassword) ? Response.none() : Response.ok(newPassword);
-  }
-
-  @UsedAt(UsedAt.Project.PLUGIN_SERVICEUSER)
-  public static String generate() {
-    byte[] rand = new byte[LEN];
-    rng.nextBytes(rand);
-
-    byte[] enc = BaseEncoding.base64().encode(rand).getBytes(UTF_8);
-    StringBuilder r = new StringBuilder(enc.length);
-    for (int i = 0; i < enc.length; i++) {
-      if (enc[i] == '=') {
-        break;
+    Response<String> resp = Response.none();
+    if (tokenAccessor.getToken(rsrc.getUser().getAccountId(), LEGACY_ID).isPresent()) {
+      Optional<ExternalId> optionalExtId =
+          externalIds.get(
+              externalIdKeyFactory.create(SCHEME_USERNAME, rsrc.getUser().getUserName().get()));
+      ExternalId extId = optionalExtId.orElseThrow(ResourceNotFoundException::new);
+      accountsUpdateProvider
+          .get()
+          .update(
+              "Remove HTTP Password",
+              extId.accountId(),
+              u ->
+                  u.updateExternalId(
+                      externalIdFactory.createWithEmail(
+                          extId.key(), extId.accountId(), extId.email())));
+      if (isDeleteOp) {
+        try {
+          emailFactories
+              .createOutgoingEmail(
+                  PASSWORD_UPDATED,
+                  emailFactories.createHttpPasswordUpdateEmail(rsrc.getUser(), "deleted"))
+              .send();
+        } catch (EmailException e) {
+          logger.atSevere().withCause(e).log(
+              "Cannot send HttpPassword update message to %s",
+              rsrc.getUser().getAccount().preferredEmail());
+        }
       }
-      r.append((char) enc[i]);
+    } else {
+      resp = deleteToken.apply(rsrc.getUser(), PasswordMigrator.DEFAULT_ID, isDeleteOp);
     }
-    return r.toString();
+
+    if (isDeleteOp) {
+      return resp;
+    }
+
+    AuthTokenInput authTokenInput = new AuthTokenInput();
+    authTokenInput.token = input.httpPassword;
+    return Response.created(
+        putToken
+            .apply(rsrc, IdString.fromDecoded(PasswordMigrator.DEFAULT_ID), authTokenInput)
+            .value()
+            .token);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index f295389..bc97b1e 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -122,7 +122,7 @@
           .update(
               "Set Username via API",
               accountId,
-              u -> u.addExternalId(externalIdFactory.create(key, accountId, null, null)));
+              u -> u.addExternalId(externalIdFactory.create(key, accountId)));
     } catch (DuplicateKeyException dupeErr) {
       // If we are using this identity, don't report the exception.
       Optional<ExternalId> other = externalIds.get(key);
diff --git a/java/com/google/gerrit/server/restapi/account/TokensCollection.java b/java/com/google/gerrit/server/restapi/account/TokensCollection.java
new file mode 100644
index 0000000..c7eb58c
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/TokensCollection.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.account;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class TokensCollection implements ChildCollection<AccountResource, AccountResource.Token> {
+  private final DynamicMap<RestView<AccountResource.Token>> views;
+  private final GetTokens list;
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final AuthTokenAccessor tokenAccessor;
+
+  @Inject
+  TokensCollection(
+      DynamicMap<RestView<AccountResource.Token>> views,
+      GetTokens list,
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      AuthTokenAccessor tokenAccessor) {
+    this.views = views;
+    this.list = list;
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.tokenAccessor = tokenAccessor;
+  }
+
+  @Override
+  public RestView<AccountResource> list() {
+    return list;
+  }
+
+  @Override
+  public AccountResource.Token parse(AccountResource rsrc, IdString id)
+      throws ResourceNotFoundException,
+          PermissionBackendException,
+          AuthException,
+          IOException,
+          ConfigInvalidException {
+    if (!self.get().hasSameAccountId(rsrc.getUser())) {
+      permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+    }
+
+    for (AuthToken token : tokenAccessor.getTokens(rsrc.getUser().getAccountId())) {
+      if (token.id().equals(id.get())) {
+        return new AccountResource.Token(rsrc.getUser(), id.get());
+      }
+    }
+
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<AccountResource.Token>> views() {
+    return views;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
index 0c12226..7ea3c5b 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -177,7 +177,8 @@
               oi,
               revWalk,
               applyResult.getTreeId(),
-              commitMessage);
+              commitMessage,
+              /* validationOptions= */ null);
       if (changeInfo.containsGitConflicts == null
           && applyResult.getErrors().stream().anyMatch(Error::isGitConflict)) {
         changeInfo.containsGitConflicts = true;
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java b/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
index a8e46b3..6c05308 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -125,9 +126,17 @@
       CommitModification commitModification =
           getCommitModification(
               repository, projectState, originPatchSetForFix, targetPatchSet, fixReplacements);
-      ChangeEdit changeEdit =
-          changeEditModifier.combineWithModifiedPatchSetTree(
-              repository, changeNotes, targetPatchSet, commitModification);
+      ChangeEdit changeEdit;
+      try {
+        changeEdit =
+            changeEditModifier.combineWithModifiedPatchSetTree(
+                repository, changeNotes, targetPatchSet, commitModification);
+      } catch (MergeConflictException e) {
+        throw new ResourceConflictException(
+            "The suggested fix could not be applied because it conflicts with the existing change"
+                + " edit. Please apply the fix locally.",
+            e);
+      }
 
       return Response.ok(changeEditJson.toEditInfo(changeEdit, false));
     } catch (InvalidChangeOperationException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index cc4626e..e254727 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static com.google.gerrit.entities.Patch.FileMode.EXECUTABLE_FILE;
-import static com.google.gerrit.entities.Patch.FileMode.REGULAR_FILE;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
@@ -337,22 +335,12 @@
     }
 
     @Nullable
-    private Integer decimalAsOctal(Integer inputMode) throws BadRequestException {
+    private Integer octalAsDecimal(Integer inputMode) {
       if (inputMode == null) {
         return null;
       }
 
-      switch (inputMode) {
-        case 100755 -> {
-          return EXECUTABLE_FILE.getMode();
-        }
-        case 100644 -> {
-          return REGULAR_FILE.getMode();
-        }
-      }
-
-      throw new BadRequestException(
-          "file_mode (" + inputMode + ") was invalid: supported values are 100644 or 100755.");
+      return Integer.parseInt(Integer.toString(inputMode), 8);
     }
 
     public Response<Object> apply(
@@ -363,13 +351,13 @@
             IOException,
             PermissionBackendException {
 
-      if (fileContentInput.content == null && fileContentInput.binary_content == null) {
+      if (fileContentInput.content == null && fileContentInput.binaryContent == null) {
         throw new BadRequestException("either content or binary_content is required");
       }
 
       RawInput newContent;
-      if (fileContentInput.binary_content != null) {
-        Matcher m = BINARY_DATA_PATTERN.matcher(fileContentInput.binary_content);
+      if (fileContentInput.binaryContent != null) {
+        Matcher m = BINARY_DATA_PATTERN.matcher(fileContentInput.binaryContent);
         if (m.matches() && BASE64.equals(m.group(2))) {
           newContent = RawInputUtil.create(Base64.decode(m.group(3)));
         } else {
@@ -379,7 +367,7 @@
         newContent = fileContentInput.content;
       }
 
-      if (Patch.COMMIT_MSG.equals(path) && fileContentInput.binary_content == null) {
+      if (Patch.COMMIT_MSG.equals(path) && fileContentInput.binaryContent == null) {
         EditMessage.Input editMessageInput = new EditMessage.Input();
         editMessageInput.message =
             new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
@@ -396,7 +384,7 @@
             rsrc.getNotes(),
             path,
             newContent,
-            decimalAsOctal(fileContentInput.fileMode));
+            octalAsDecimal(fileContentInput.fileMode));
       } catch (InvalidChangeOperationException e) {
         throw new ResourceConflictException(e.getMessage());
       }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index fa728a4..9bbaa3f3 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -114,6 +114,7 @@
     post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class);
     delete(CHANGE_KIND, "private").to(DeletePrivate.class);
     get(CHANGE_KIND, "pure_revert").to(GetPureRevert.class);
+    get(CHANGE_KIND, "query").to(EvaluateChangeQueryExpression.class);
     post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
     post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
     post(CHANGE_KIND, "rebase:chain").to(RebaseChain.class);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 9715a5d..4ae82ce 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -31,14 +31,12 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.List;
-import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
@@ -49,8 +47,8 @@
   private final ChangeFinder changeFinder;
   private final ChangeResource.Factory changeResourceFactory;
   private final PermissionBackend permissionBackend;
-  private final ProjectCache projectCache;
   private final ChangeNotes.Factory changeNotesFactory;
+  private final ChangeData.Factory changeDataFactory;
 
   @Inject
   public ChangesCollection(
@@ -60,8 +58,8 @@
       ChangeFinder changeFinder,
       ChangeResource.Factory changeResourceFactory,
       PermissionBackend permissionBackend,
-      ProjectCache projectCache,
-      ChangeNotes.Factory changeNotesFactory) {
+      ChangeNotes.Factory changeNotesFactory,
+      ChangeData.Factory changeDataFactory) {
     this.changeNotesFactory = changeNotesFactory;
     this.user = user;
     this.queryFactory = queryFactory;
@@ -69,7 +67,7 @@
     this.changeFinder = changeFinder;
     this.changeResourceFactory = changeResourceFactory;
     this.permissionBackend = permissionBackend;
-    this.projectCache = projectCache;
+    this.changeDataFactory = changeDataFactory;
   }
 
   @Override
@@ -98,10 +96,10 @@
     }
 
     ChangeNotes change = notes.get(0);
+    checkProjectStatePermitsRead(change);
     if (!canRead(change)) {
       throw new ResourceNotFoundException(id);
     }
-    checkProjectStatePermitsRead(change.getProjectName());
     return changeResourceFactory.create(change, user.get());
   }
 
@@ -114,8 +112,8 @@
    */
   public ChangeResource parse(Project.NameKey project, Change.Id id, ObjectId metaRevId)
       throws ResourceConflictException, ResourceNotFoundException, PermissionBackendException {
-    checkProjectStatePermitsRead(project);
     ChangeNotes change = changeNotesFactory.createChecked(project, id, metaRevId);
+    checkProjectStatePermitsRead(change);
     if (!canRead(change)) {
       throw new ResourceNotFoundException(toIdString(id));
     }
@@ -138,10 +136,10 @@
     }
 
     ChangeNotes change = notes.get(0);
+    checkProjectStatePermitsRead(change);
     if (!canRead(change)) {
       throw new ResourceNotFoundException(toIdString(id));
     }
-    checkProjectStatePermitsRead(change.getProjectName());
     return changeResourceFactory.create(change, user.get());
   }
 
@@ -154,21 +152,11 @@
   }
 
   private boolean canRead(ChangeNotes notes) throws PermissionBackendException {
-    if (!permissionBackend.currentUser().change(notes).test(ChangePermission.READ)) {
-      return false;
-    }
-    Optional<ProjectState> projectState = projectCache.get(notes.getProjectName());
-    if (!projectState.isPresent()) {
-      return false;
-    }
-    return projectState.get().statePermitsRead();
+    return permissionBackend.currentUser().change(notes).test(ChangePermission.READ);
   }
 
-  private void checkProjectStatePermitsRead(Project.NameKey project)
-      throws ResourceNotFoundException, ResourceConflictException {
-    projectCache
-        .get(project)
-        .orElseThrow(() -> new ResourceNotFoundException("project not found: " + project.get()))
-        .checkStatePermitsRead();
+  private void checkProjectStatePermitsRead(ChangeNotes changeNotes)
+      throws ResourceConflictException {
+    changeDataFactory.create(changeNotes).checkProjectStatePermitsRead();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Check.java b/java/com/google/gerrit/server/restapi/change/Check.java
index f62aa5a..5efdb7c 100644
--- a/java/com/google/gerrit/server/restapi/change/Check.java
+++ b/java/com/google/gerrit/server/restapi/change/Check.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -54,12 +53,9 @@
   public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
       throws RestApiException, PermissionBackendException, NoSuchProjectException, IOException {
     PermissionBackend.WithUser perm = permissionBackend.currentUser();
-    if (!rsrc.isUserOwner()) {
-      try {
-        perm.project(rsrc.getProject()).check(ProjectPermission.READ_CONFIG);
-      } catch (AuthException e) {
-        perm.check(GlobalPermission.MAINTAIN_SERVER);
-      }
+    if (!rsrc.isUserOwner()
+        && !perm.project(rsrc.getProject()).test(ProjectPermission.READ_CONFIG)) {
+      perm.check(GlobalPermission.MAINTAIN_SERVER);
     }
     return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc));
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 272afc9..aa9b363 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -30,14 +30,12 @@
 import com.google.gerrit.entities.FixSuggestion;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.ContextLineInfo;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.comment.CommentContextCache;
@@ -106,10 +104,6 @@
     return new HumanCommentFormatter();
   }
 
-  public RobotCommentFormatter newRobotCommentFormatter() {
-    return new RobotCommentFormatter();
-  }
-
   private abstract class BaseCommentFormatter<F extends Comment, T extends CommentInfo> {
     public T format(F comment) throws PermissionBackendException {
       AccountLoader loader = fillAccounts ? accountLoaderFactory.get().create(true) : null;
@@ -281,19 +275,4 @@
 
     private HumanCommentFormatter() {}
   }
-
-  class RobotCommentFormatter extends BaseCommentFormatter<RobotComment, RobotCommentInfo> {
-    @Override
-    protected RobotCommentInfo toInfo(RobotComment c, AccountLoader loader) {
-      RobotCommentInfo rci = new RobotCommentInfo();
-      rci.robotId = c.robotId;
-      rci.robotRunId = c.robotRunId;
-      rci.url = c.url;
-      rci.properties = c.properties;
-      fillCommentInfo(c, rci, loader);
-      return rci;
-    }
-
-    private RobotCommentFormatter() {}
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 57be320..fa4296b 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -104,9 +104,7 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -116,6 +114,8 @@
 import org.eclipse.jgit.lib.TreeFormatter;
 import org.eclipse.jgit.patch.PatchApplier;
 import org.eclipse.jgit.patch.PatchApplier.Result.Error;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.ChangeIdUtil;
@@ -307,13 +307,13 @@
     }
 
     String subject = Strings.nullToEmpty(input.subject);
-    subject = subject.replaceAll("(?m)^#.*$\n?", "").trim();
+    subject = CommitMessageUtil.dropComments(subject).trim();
     if (subject.isEmpty()) {
       throw new BadRequestException("commit message must be non-empty");
     }
     input.subject = subject;
 
-    Optional<String> changeId = getChangeIdFromMessage(input.subject);
+    Optional<String> changeId = CommitMessageUtil.getChangeIdFromCommitMessageFooter(input.subject);
     if (changeId.isPresent()) {
       if (!queryProvider
           .get()
@@ -458,7 +458,7 @@
                 ? committer
                 : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
 
-        String commitMessage = getCommitMessage(input.subject, me);
+        String commitMessage = getCommitMessage(projectState, input.subject, me);
 
         CodeReviewCommit c;
         boolean hasGitConflicts = false;
@@ -491,6 +491,7 @@
           logger.atFine().log("tree ID after applying patch: %s", treeId.name());
           String appliedPatchCommitMessage =
               getCommitMessage(
+                  projectState,
                   ApplyPatchUtil.buildCommitMessage(
                       input.subject,
                       ImmutableList.of(),
@@ -663,18 +664,19 @@
     return parentCommit;
   }
 
-  private Optional<String> getChangeIdFromMessage(String subject) {
-    int indexOfChangeId = ChangeIdUtil.indexOfChangeId(subject, "\n");
-    if (indexOfChangeId == -1) {
-      return Optional.empty();
+  private boolean shouldAddSignedOffByTag(
+      ProjectState projectState, IdentifiedUser me, String commitMessage) {
+    if (FooterLine.fromMessage(commitMessage).stream()
+        .anyMatch(footer -> footer.matches(FooterKey.SIGNED_OFF_BY))) {
+      return false;
     }
-    return Optional.of(
-        subject.substring(
-            indexOfChangeId + 11 /* "Change-Id: "*/,
-            indexOfChangeId + 12 /* "Change-Id: I" */ + Constants.OBJECT_ID_STRING_LENGTH));
+    if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
+      return true;
+    }
+    return Boolean.TRUE.equals(me.state().generalPreferences().signedOffBy);
   }
 
-  private String getCommitMessage(String subject, IdentifiedUser me) {
+  private String getCommitMessage(ProjectState projectState, String subject, IdentifiedUser me) {
     // Add a Change-Id line if there isn't already one
     String commitMessage = subject;
     if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
@@ -682,7 +684,7 @@
       commitMessage = ChangeIdUtil.insertId(commitMessage, id);
     }
 
-    if (Boolean.TRUE.equals(me.state().generalPreferences().signedOffBy)) {
+    if (this.shouldAddSignedOffByTag(projectState, me, commitMessage)) {
       commitMessage =
           Joiner.on("\n")
               .join(
@@ -711,7 +713,7 @@
         rw.parseCommit(
             CommitUtil.createCommitWithTree(
                 oi, authorIdent, committerIdent, parents, commitMessage, treeId));
-    commit.setNoConflicts();
+    commit.setNoConflictsForNonMergeCommit();
     return commit;
   }
 
@@ -776,26 +778,21 @@
         firstNonNull(Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
     logger.atFine().log("merge strategy = %s", mergeStrategy);
 
-    try {
-      CodeReviewCommit mergeCommit =
-          MergeUtil.createMergeCommit(
-              oi,
-              repo.getConfig(),
-              mergeTip,
-              sourceCommit,
-              mergeStrategy,
-              merge.allowConflicts,
-              authorIdent,
-              committerIdent,
-              commitMessage,
-              rw,
-              this.useDiff3);
-      logger.atFine().log("tree ID of merge commit: %s", mergeCommit.getTree().getId().name());
-      return mergeCommit;
-    } catch (NoMergeBaseException e) {
-      throw new ResourceConflictException(
-          String.format("Cannot create merge commit: %s", e.getMessage()), e);
-    }
+    CodeReviewCommit mergeCommit =
+        MergeUtil.createMergeCommit(
+            oi,
+            repo.getConfig(),
+            mergeTip,
+            sourceCommit,
+            mergeStrategy,
+            merge.allowConflicts,
+            authorIdent,
+            committerIdent,
+            commitMessage,
+            rw,
+            this.useDiff3);
+    logger.atFine().log("tree ID of merge commit: %s", mergeCommit.getTree().getId().name());
+    return mergeCommit;
   }
 
   private static String messageForNewChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index c386b29..998a09e 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -100,8 +100,7 @@
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
       throw new BadRequestException("range endLine must be on the same line as the comment");
     } else if (in.inReplyTo != null
-        && !commentsUtil.getPublishedHumanComment(rsrc.getNotes(), in.inReplyTo).isPresent()
-        && !commentsUtil.getRobotComment(rsrc.getNotes(), in.inReplyTo).isPresent()) {
+        && !commentsUtil.getPublishedHumanComment(rsrc.getNotes(), in.inReplyTo).isPresent()) {
       throw new BadRequestException(
           String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 19ba6d1..2421912 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.GerritNoMergeBaseException;
 import com.google.gerrit.exceptions.InvalidMergeStrategyException;
 import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -288,7 +289,9 @@
       throws ResourceNotFoundException,
           MergeIdenticalTreeException,
           MergeConflictException,
-          IOException {
+          IOException,
+          InvalidMergeStrategyException,
+          GerritNoMergeBaseException {
 
     ObjectId parentCommit;
     if (in.inheritParent) {
diff --git a/java/com/google/gerrit/server/restapi/change/EvaluateChangeQueryExpression.java b/java/com/google/gerrit/server/restapi/change/EvaluateChangeQueryExpression.java
new file mode 100644
index 0000000..04cef70
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/EvaluateChangeQueryExpression.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.PredicateResult;
+import com.google.gerrit.extensions.common.EvaluateChangeQueryExpressionResultInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.List;
+import java.util.Map;
+import org.kohsuke.args4j.Option;
+
+/** REST endpoint to evaluate whether a change query expression matches the change. */
+public class EvaluateChangeQueryExpression implements RestReadView<ChangeResource> {
+  @Option(
+      name = "--expression",
+      usage = "Change query expression for which it should be checked if the change matches.")
+  public String expression;
+
+  @Option(
+      name = "--use-index",
+      usage =
+          "Whether the change query expression should be evaluated against the change state in the"
+              + " index.")
+  public boolean useIndex;
+
+  private final Provider<ChangeQueryBuilder> queryBuilder;
+  private final Provider<InternalChangeQuery> internalChangeQuery;
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Inject
+  EvaluateChangeQueryExpression(
+      Provider<ChangeQueryBuilder> queryBuilder,
+      Provider<InternalChangeQuery> internalChangeQuery) {
+    this.queryBuilder = queryBuilder;
+    this.internalChangeQuery = internalChangeQuery;
+  }
+
+  @Override
+  public Response<EvaluateChangeQueryExpressionResultInfo> apply(ChangeResource rsrc)
+      throws BadRequestException {
+    if (Strings.isNullOrEmpty(expression)) {
+      throw new BadRequestException("expression is required");
+    }
+
+    logger.atFine().log("parsing expression %s", expression);
+    Predicate<ChangeData> predicate = parseExpression(expression);
+    logger.atFine().log("evaluating predicate string %s", predicate.getPredicateString());
+    PredicateResult predicateResult = getChangeData(rsrc).evaluatePredicateTree(predicate);
+    return Response.ok(toInfo(predicateResult));
+  }
+
+  private ChangeData getChangeData(ChangeResource rsrc) {
+    if (useIndex) {
+      // Loading the change from the index populates ChangeData with the data that is stored in the
+      // index, including submit requirement results.
+      List<ChangeData> changeDatas =
+          internalChangeQuery.get().byProjectChangeNumber(rsrc.getProject(), rsrc.getId());
+      checkState(
+          changeDatas.size() == 1,
+          "Got %s matches for change %s, expected 1",
+          changeDatas.size() == 1,
+          rsrc.getId());
+      return Iterables.getOnlyElement(changeDatas);
+    }
+
+    // The ChangeData in ChangeResource has been loaded from NoteDb. It doesn't contain submit
+    // requirement results yet. Submit requirement results are loaded lazily by executing the submit
+    // requirements. Executing the submit requirements is rather expensive. This means if an
+    // expression is evaluated that requires checking if the change is submittable (e.g.
+    // "is:submittable") this is slower than using the change data from the index where submit
+    // requirement results are already present.
+    return rsrc.getChangeData();
+  }
+
+  private Predicate<ChangeData> parseExpression(String expression) throws BadRequestException {
+    try {
+      return queryBuilder.get().parse(expression);
+    } catch (QueryParseException e) {
+      throw new BadRequestException(String.format("invalid query expression: %s", e.getMessage()));
+    }
+  }
+
+  private static EvaluateChangeQueryExpressionResultInfo toInfo(PredicateResult predicateResult) {
+    EvaluateChangeQueryExpressionResultInfo info = new EvaluateChangeQueryExpressionResultInfo();
+    info.status = predicateResult.status();
+    info.passingAtoms = predicateResult.getPassingAtoms();
+    info.failingAtoms = predicateResult.getFailingAtoms();
+    ImmutableMap<String, String> atomExplanations =
+        predicateResult.getAtomExplanations().entrySet().stream()
+            .filter(e -> !Strings.isNullOrEmpty(e.getValue()))
+            .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+    info.atomExplanations = !atomExplanations.isEmpty() ? atomExplanations : null;
+    return info;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Fixes.java b/java/com/google/gerrit/server/restapi/change/Fixes.java
index 1d0b8df..b362f85 100644
--- a/java/com/google/gerrit/server/restapi/change/Fixes.java
+++ b/java/com/google/gerrit/server/restapi/change/Fixes.java
@@ -57,8 +57,6 @@
     List<Comment> allComments = new ArrayList<>();
     allComments.addAll(
         commentsUtil.publishedByPatchSet(changeNotes, revisionResource.getPatchSet().id()));
-    allComments.addAll(
-        commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().id()));
     for (Comment comment : allComments) {
       if (comment.fixSuggestions == null) {
         continue;
diff --git a/java/com/google/gerrit/server/restapi/change/GetBlame.java b/java/com/google/gerrit/server/restapi/change/GetBlame.java
index bc2755e..3f2d0b5 100644
--- a/java/com/google/gerrit/server/restapi/change/GetBlame.java
+++ b/java/com/google/gerrit/server/restapi/change/GetBlame.java
@@ -27,6 +27,8 @@
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.patch.AutoMerger;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gitiles.blame.cache.BlameCache;
@@ -127,17 +129,28 @@
     ListMultimap<BlameInfo, RangeInfo> ranges =
         MultimapBuilder.hashKeys().arrayListValues().build();
     List<BlameInfo> result = new ArrayList<>();
-    if (blameCache.findLastCommit(repository, id, path) == null) {
-      return result;
+    try (TraceContext.TraceTimer ignored =
+        TraceContext.newTimer("blameCache.findLastCommit", Metadata.empty())) {
+      if (blameCache.findLastCommit(repository, id, path) == null) {
+        return result;
+      }
     }
 
-    List<Region> blameRegions = blameCache.get(repository, id, path);
+    List<Region> blameRegions;
+    try (TraceContext.TraceTimer ignored =
+        TraceContext.newTimer("blameCache.get", Metadata.empty())) {
+      blameRegions = blameCache.get(repository, id, path);
+    }
+
     int from = 1;
     for (Region region : blameRegions) {
-      RevCommit commit = revWalk.parseCommit(region.getSourceCommit());
-      BlameInfo blameInfo = toBlameInfo(commit, region.getSourceAuthor());
-      ranges.put(blameInfo, new RangeInfo(from, from + region.getCount() - 1));
-      from += region.getCount();
+      try (TraceContext.TraceTimer ignored =
+          TraceContext.newTimer("process blame region", Metadata.empty())) {
+        RevCommit commit = revWalk.parseCommit(region.getSourceCommit());
+        BlameInfo blameInfo = toBlameInfo(commit, region.getSourceAuthor());
+        ranges.put(blameInfo, new RangeInfo(from, from + region.getCount() - 1));
+        from += region.getCount();
+      }
     }
 
     for (BlameInfo key : ranges.keySet()) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index e2417af..ce5be0d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -76,6 +76,13 @@
   @Option(name = "--parent", metaVar = "parent-number")
   private Integer parentNum;
 
+  @Option(
+      name = "--context",
+      aliases = {"-U"},
+      metaVar = "lines",
+      usage = "number of context lines")
+  private Integer context;
+
   @Inject
   GetPatch(GitRepositoryManager repoManager) {
     this.repoManager = repoManager;
@@ -141,7 +148,7 @@
               if (path == null) {
                 out.write(formatEmailHeader(commit).getBytes(UTF_8));
               }
-              DiffUtil.getFormattedDiff(repo, base, commit, path, out);
+              DiffUtil.getFormattedDiff(repo, base, commit, path, out, getContext());
             }
           }) {
 
@@ -212,4 +219,14 @@
   private static String fileName(RevWalk rw, RevCommit commit) throws IOException {
     return abbreviateName(commit, rw.getObjectReader()) + ".diff";
   }
+
+  private int getContext() {
+    if (context == null) {
+      return 3;
+    }
+    if (context < 0) {
+      return 0;
+    }
+    return context;
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index 6471a62..3a04dc7 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -123,6 +123,7 @@
       info._currentRevisionNumber = curr != null ? curr.get() : null;
       info.status = ChangeUtil.status(change).toUpperCase(Locale.US);
       info.submittable = computeSubmittable ? submittable(cd) : null;
+      info.workInProgress = change.isWorkInProgress() ? change.isWorkInProgress() : null;
     }
 
     info.commit = new CommitInfo();
diff --git a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
index 4ff9942..c2c362d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRobotComment.java
@@ -15,27 +15,22 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.RobotCommentResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetRobotComment implements RestReadView<RobotCommentResource> {
 
-  private final Provider<CommentJson> commentJson;
-
   @Inject
-  GetRobotComment(Provider<CommentJson> commentJson) {
-    this.commentJson = commentJson;
-  }
+  GetRobotComment() {}
 
   @Override
   public Response<RobotCommentInfo> apply(RobotCommentResource rsrc)
-      throws PermissionBackendException {
-    return Response.ok(commentJson.get().newRobotCommentFormatter().format(rsrc.getComment()));
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException("robot comments unsupported");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index d841183..af79aa3 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -14,56 +14,23 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.List;
 import java.util.Map;
 
 public class ListChangeRobotComments implements RestReadView<ChangeResource> {
-  private final ChangeData.Factory changeDataFactory;
-  private final Provider<CommentJson> commentJson;
-  private final CommentsUtil commentsUtil;
-  private final ChangeMessagesUtil changeMessagesUtil;
 
   @Inject
-  ListChangeRobotComments(
-      ChangeData.Factory changeDataFactory,
-      Provider<CommentJson> commentJson,
-      CommentsUtil commentsUtil,
-      ChangeMessagesUtil changeMessagesUtil) {
-    this.changeDataFactory = changeDataFactory;
-    this.commentJson = commentJson;
-    this.commentsUtil = commentsUtil;
-    this.changeMessagesUtil = changeMessagesUtil;
-  }
+  ListChangeRobotComments() {}
 
   @Override
   public Response<Map<String, List<RobotCommentInfo>>> apply(ChangeResource rsrc)
-      throws AuthException, PermissionBackendException {
-    ChangeData cd = changeDataFactory.create(rsrc.getNotes());
-    Map<String, List<RobotCommentInfo>> robotCommentsMap =
-        commentJson
-            .get()
-            .setFillAccounts(true)
-            .setFillPatchSet(true)
-            .newRobotCommentFormatter()
-            .format(commentsUtil.robotCommentsByChange(cd.notes()));
-    List<RobotCommentInfo> commentInfos =
-        robotCommentsMap.values().stream().flatMap(List::stream).collect(toList());
-    List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
-    return Response.ok(robotCommentsMap);
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException("robot comments unsupported");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index c5feb96..f6e04cf 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -14,75 +14,30 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import static java.util.stream.Collectors.toList;
-
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.entities.ChangeMessage;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.change.CommentJson.RobotCommentFormatter;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.List;
 import java.util.Map;
 
 @Singleton
 public class ListRobotComments implements RestReadView<RevisionResource> {
-  protected final Provider<CommentJson> commentJson;
-  protected final CommentsUtil commentsUtil;
-  protected final ChangeMessagesUtil changeMessagesUtil;
 
   @Inject
-  ListRobotComments(
-      Provider<CommentJson> commentJson,
-      CommentsUtil commentsUtil,
-      ChangeMessagesUtil changeMessagesUtil) {
-    this.commentJson = commentJson;
-    this.commentsUtil = commentsUtil;
-    this.changeMessagesUtil = changeMessagesUtil;
-  }
+  ListRobotComments() {}
 
   @Override
   public Response<Map<String, List<RobotCommentInfo>>> apply(RevisionResource rsrc)
-      throws PermissionBackendException {
-    return Response.ok(getAsMap(listComments(rsrc), rsrc));
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException("robot comments unsupported");
   }
 
-  public ImmutableList<RobotCommentInfo> getComments(RevisionResource rsrc)
-      throws PermissionBackendException {
-    return getAsList(listComments(rsrc), rsrc);
-  }
-
-  private ImmutableList<RobotCommentInfo> getAsList(
-      Iterable<RobotComment> comments, RevisionResource rsrc) throws PermissionBackendException {
-    ImmutableList<RobotCommentInfo> commentInfos = getCommentFormatter().formatAsList(comments);
-    List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
-    return commentInfos;
-  }
-
-  private Map<String, List<RobotCommentInfo>> getAsMap(
-      Iterable<RobotComment> comments, RevisionResource rsrc) throws PermissionBackendException {
-    Map<String, List<RobotCommentInfo>> commentInfosMap = getCommentFormatter().format(comments);
-    List<RobotCommentInfo> commentInfos =
-        commentInfosMap.values().stream().flatMap(List::stream).collect(toList());
-    List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
-    return commentInfosMap;
-  }
-
-  private List<RobotComment> listComments(RevisionResource rsrc) {
-    return commentsUtil.robotCommentsByPatchSet(rsrc.getNotes(), rsrc.getPatchSet().id());
-  }
-
-  private RobotCommentFormatter getCommentFormatter() {
-    return commentJson.get().setFillAccounts(true).newRobotCommentFormatter();
+  public ImmutableList<RobotCommentInfo> getComments(RevisionResource rsrc) {
+    return ImmutableList.of();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java b/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java
index 60c8e89..fe12119 100644
--- a/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java
+++ b/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
@@ -92,7 +93,8 @@
       ObjectInserter oi,
       CodeReviewRevWalk revWalk,
       ObjectId commitTree,
-      String commitMessage)
+      String commitMessage,
+      @Nullable ImmutableListMultimap<String, String> validationOptions)
       throws IOException, RestApiException, UpdateException {
     requireNonNull(destChange);
     requireNonNull(latestPatchset);
@@ -123,7 +125,9 @@
     Change resultChange;
     try (BatchUpdate bu = batchUpdateFactory.create(project, ident.get(), TimeUtil.now())) {
       bu.setRepository(repo, revWalk, oi);
-      resultChange = insertPatchSet(bu, repo, patchSetInserterFactory, destChange.notes(), commit);
+      resultChange =
+          insertPatchSet(
+              bu, repo, patchSetInserterFactory, destChange.notes(), commit, validationOptions);
     } catch (NoSuchChangeException | RepositoryNotFoundException e) {
       throw new ResourceConflictException(e.getMessage());
     }
@@ -156,13 +160,17 @@
       Repository git,
       PatchSetInserter.Factory patchSetInserterFactory,
       ChangeNotes destNotes,
-      CodeReviewCommit commit)
+      CodeReviewCommit commit,
+      ImmutableListMultimap<String, String> validationOptions)
       throws IOException, UpdateException, RestApiException {
     try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
       Change destChange = destNotes.getChange();
       PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
       PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, commit);
       inserter.setMessage(buildMessageForPatchSet(psId));
+      if (validationOptions != null) {
+        inserter.setValidationOptions(validationOptions);
+      }
       bu.addOp(destChange.getId(), inserter);
       bu.execute();
       return inserter.getChange();
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 7e37ce9..e388e3f 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -46,7 +46,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewerResult;
@@ -66,6 +65,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
@@ -260,10 +260,6 @@
       input.comments = cleanUpComments(input.comments);
       commentsValidator.checkComments(revision, input.comments);
     }
-    if (input.robotComments != null) {
-      input.robotComments = cleanUpComments(input.robotComments);
-      checkRobotComments(revision, input.robotComments);
-    }
 
     if (input.notify == null) {
       input.notify = defaultNotify(revision.getChange(), input);
@@ -630,7 +626,10 @@
           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
     }
 
-    IdentifiedUser reviewer = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
+    IdentifiedUser reviewer =
+        accountResolver
+            .resolve(in.onBehalfOf)
+            .asUniqueUserOnBehalfOf(caller, ImpersonationPermissionMode.THIS_USER);
     logger.atFine().log("on behalf of user was resolved to %s", reviewer.getLoggableName());
     try {
       permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ);
@@ -718,37 +717,6 @@
         .collect(toList());
   }
 
-  private void checkRobotComments(
-      RevisionResource revision, Map<String, List<RobotCommentInput>> in)
-      throws BadRequestException, PatchListNotAvailableException {
-    logger.atFine().log("checking robot comments");
-    for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) {
-      String commentPath = e.getKey();
-      for (RobotCommentInput c : e.getValue()) {
-        ensureRobotIdIsSet(c.robotId, commentPath);
-        ensureRobotRunIdIsSet(c.robotRunId, commentPath);
-        // Size is validated later, in CommentLimitsValidator.
-      }
-    }
-    commentsValidator.checkComments(revision, in);
-  }
-
-  private static void ensureRobotIdIsSet(String robotId, String commentPath)
-      throws BadRequestException {
-    if (robotId == null) {
-      throw new BadRequestException(
-          String.format("robotId is missing for comment on %s", commentPath));
-    }
-  }
-
-  private static void ensureRobotRunIdIsSet(String robotRunId, String commentPath)
-      throws BadRequestException {
-    if (robotRunId == null) {
-      throw new BadRequestException(
-          String.format("robotRunId is missing for comment on %s", commentPath));
-    }
-  }
-
   /**
    * Used to compare existing {@link HumanComment}-s with {@link CommentInput} comments by copying
    * only the fields to compare.
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index 4d6e24a..8af3739 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -41,11 +41,9 @@
 import com.google.gerrit.entities.LabelTypes;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -77,6 +75,7 @@
 import com.google.gerrit.server.update.CommentsRejectedException;
 import com.google.gerrit.server.update.PostUpdateContext;
 import com.google.gerrit.server.util.LabelVote;
+import com.google.gerrit.server.util.MarkdownImagesUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -236,6 +235,7 @@
   private final CommentAdded commentAdded;
   private final PluginSetContext<CommentValidator> commentValidators;
   private final PluginSetContext<OnPostReview> onPostReviews;
+  private final MarkdownImagesUtil markdownImagesUtil;
 
   private final ProjectState projectState;
   private final PatchSet.Id psId;
@@ -270,6 +270,7 @@
       CommentAdded commentAdded,
       PluginSetContext<CommentValidator> commentValidators,
       PluginSetContext<OnPostReview> onPostReviews,
+      MarkdownImagesUtil markdownImagesUtil,
       @Assisted ProjectState projectState,
       @Assisted PatchSet.Id psId,
       @Assisted ReviewInput in,
@@ -287,6 +288,7 @@
     this.onPostReviews = onPostReviews;
     this.publishPatchSetLevelComment =
         gerritConfig.getBoolean("event", "comment-added", "publishPatchSetLevelComment", true);
+    this.markdownImagesUtil = markdownImagesUtil;
 
     this.projectState = projectState;
     this.psId = psId;
@@ -304,14 +306,9 @@
     user = ctx.getIdentifiedUser();
     notes = ctx.getNotes();
     ps = psUtil.get(ctx.getNotes(), psId);
-    List<RobotComment> newRobotComments =
-        in.robotComments == null ? ImmutableList.of() : getNewRobotComments(ctx);
     boolean dirty = false;
     try (TraceContext.TraceTimer ignored = newTimer("insertComments")) {
-      dirty |= insertComments(ctx, newRobotComments);
-    }
-    try (TraceContext.TraceTimer ignored = newTimer("insertRobotComments")) {
-      dirty |= insertRobotComments(ctx, newRobotComments);
+      dirty |= insertComments(ctx);
     }
     try (TraceContext.TraceTimer ignored = newTimer("updateLabels")) {
       dirty |= updateLabels(projectState, ctx);
@@ -371,10 +368,9 @@
    * body.
    *
    * @param ctx context for performing the change update.
-   * @param newRobotComments robot comments. Used only for validation in this method.
    * @return true if any input comments where published.
    */
-  private boolean insertComments(ChangeContext ctx, List<RobotComment> newRobotComments)
+  private boolean insertComments(ChangeContext ctx)
       throws BadRequestException, CommentsRejectedException {
     Map<String, List<CommentInput>> inputComments = in.comments;
     if (inputComments == null) {
@@ -410,15 +406,11 @@
         validateComments(
             ctx,
             Streams.concat(
-                savedDraftsToPublish.values().stream(),
-                inputCommentsToPublish.stream(),
-                newRobotComments.stream()));
+                savedDraftsToPublish.values().stream(), inputCommentsToPublish.stream()));
         publishCommentUtil.publish(ctx, ctx.getUpdate(psId), filteredDrafts, in.tag);
         comments.addAll(savedDraftsToPublish.values());
       }
-      case KEEP ->
-          validateComments(
-              ctx, Streams.concat(inputCommentsToPublish.stream(), newRobotComments.stream()));
+      case KEEP -> validateComments(ctx, inputCommentsToPublish.stream());
     }
     commentsUtil.putHumanComments(
         ctx.getUpdate(psId), HumanComment.Status.PUBLISHED, inputCommentsToPublish);
@@ -496,20 +488,24 @@
             ctx.getChange().getChangeId(),
             ctx.getChange().getProject().get(),
             ctx.getChange().getDest().branch());
-    String changeMessage = Strings.nullToEmpty(in.message).trim();
+    String changeMessage = markdownImagesUtil.stripImages(Strings.nullToEmpty(in.message).trim());
     ImmutableList<CommentForValidation> draftsForValidation =
         Stream.concat(
                 comments.map(
-                    comment ->
-                        CommentForValidation.create(
-                            comment instanceof RobotComment
-                                ? CommentForValidation.CommentSource.ROBOT
-                                : CommentForValidation.CommentSource.HUMAN,
-                            comment.lineNbr > 0
-                                ? CommentForValidation.CommentType.INLINE_COMMENT
-                                : CommentForValidation.CommentType.FILE_COMMENT,
-                            comment.message,
-                            comment.getApproximateSize())),
+                    comment -> {
+                      String strippedMessage = markdownImagesUtil.stripImages(comment.message);
+                      int size =
+                          comment.getApproximateSize()
+                              - comment.message.length()
+                              + strippedMessage.length();
+                      return CommentForValidation.create(
+                          CommentForValidation.CommentSource.HUMAN,
+                          comment.lineNbr > 0
+                              ? CommentForValidation.CommentType.INLINE_COMMENT
+                              : CommentForValidation.CommentType.FILE_COMMENT,
+                          strippedMessage,
+                          size);
+                    }),
                 Stream.of(
                     CommentForValidation.create(
                         CommentForValidation.CommentSource.HUMAN,
@@ -551,68 +547,12 @@
     }
   }
 
-  private boolean insertRobotComments(ChangeContext ctx, List<RobotComment> newRobotComments) {
-    if (in.robotComments == null) {
-      return false;
-    }
-    commentsUtil.putRobotComments(ctx.getUpdate(psId), newRobotComments);
-    comments.addAll(newRobotComments);
-    return !newRobotComments.isEmpty();
-  }
-
-  private List<RobotComment> getNewRobotComments(ChangeContext ctx) {
-    List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
-
-    Set<CommentSetEntry> existingIds =
-        in.omitDuplicateComments ? readExistingRobotComments(ctx) : Collections.emptySet();
-
-    for (Map.Entry<String, List<RobotCommentInput>> ent : in.robotComments.entrySet()) {
-      String path = ent.getKey();
-      for (RobotCommentInput c : ent.getValue()) {
-        RobotComment e = createRobotCommentFromInput(ctx, path, c);
-        if (existingIds.contains(CommentSetEntry.create(e))) {
-          continue;
-        }
-        toAdd.add(e);
-      }
-    }
-    return toAdd;
-  }
-
-  private RobotComment createRobotCommentFromInput(
-      ChangeContext ctx, String path, RobotCommentInput robotCommentInput) {
-    RobotComment robotComment =
-        commentsUtil.newRobotComment(
-            ctx,
-            path,
-            psId,
-            robotCommentInput.side(),
-            robotCommentInput.message,
-            robotCommentInput.robotId,
-            robotCommentInput.robotRunId);
-    robotComment.parentUuid = Url.decode(robotCommentInput.inReplyTo);
-    robotComment.url = robotCommentInput.url;
-    robotComment.properties = robotCommentInput.properties;
-    robotComment.setLineNbrAndRange(robotCommentInput.line, robotCommentInput.range);
-    robotComment.tag = in.tag;
-    commentsUtil.setCommentCommitId(robotComment, ctx.getChange(), ps);
-    robotComment.fixSuggestions =
-        CommentsUtil.createFixSuggestionsFromInput(robotCommentInput.fixSuggestions);
-    return robotComment;
-  }
-
   private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) {
     return commentsUtil.publishedHumanCommentsByChange(ctx.getNotes()).stream()
         .map(CommentSetEntry::create)
         .collect(toSet());
   }
 
-  private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx) {
-    return commentsUtil.robotCommentsByChange(ctx.getNotes()).stream()
-        .map(CommentSetEntry::create)
-        .collect(toSet());
-  }
-
   private Map<String, HumanComment> changeDrafts(ChangeContext ctx) {
     return draftCommentsReader
         .getDraftsByChangeAndDraftAuthor(ctx.getNotes(), user.getAccountId())
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewFix.java b/java/com/google/gerrit/server/restapi/change/PreviewFix.java
index cc07536..48307cd6 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewFix.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewFix.java
@@ -96,6 +96,7 @@
     @Override
     public Response<Map<String, DiffInfo>> apply(FixResource fixResource)
         throws PermissionBackendException,
+            BadRequestException,
             ResourceNotFoundException,
             ResourceConflictException,
             AuthException,
@@ -157,6 +158,7 @@
   private Map<String, DiffInfo> previewAllFiles(
       Map<String, List<FixReplacement>> fixReplacementsPerFilePath)
       throws PermissionBackendException,
+          BadRequestException,
           ResourceNotFoundException,
           ResourceConflictException,
           AuthException,
@@ -182,6 +184,7 @@
       Repository git, String fileName, ImmutableList<FixReplacement> fixReplacements)
       throws PermissionBackendException,
           AuthException,
+          BadRequestException,
           LargeObjectException,
           InvalidChangeOperationException,
           IOException,
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index ef70aee..07db841 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -96,8 +96,7 @@
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
       throw new BadRequestException("range endLine must be on the same line as the comment");
     } else if (in.inReplyTo != null
-        && !commentsUtil.getPublishedHumanComment(rsrc.getNotes(), in.inReplyTo).isPresent()
-        && !commentsUtil.getRobotComment(rsrc.getNotes(), in.inReplyTo).isPresent()) {
+        && !commentsUtil.getPublishedHumanComment(rsrc.getNotes(), in.inReplyTo).isPresent()) {
       throw new BadRequestException(
           String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 34220b0..c1d31ca 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -152,6 +152,11 @@
           inserter.setMessage(
               String.format("Patch Set %s: Commit message was updated.", psId.getId()));
           inserter.setDescription("Edit commit message");
+
+          // Copy the conflicts information if present. Amending the commit to set a new commit
+          // message doesn't change anything about the conflicts.
+          ps.conflicts().ifPresent(conflicts -> inserter.setConflicts(conflicts));
+
           bu.setNotify(resolveNotify(input, resource));
           bu.addOp(resource.getChange().getId(), inserter);
           bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 1525762..af9367f 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
@@ -99,7 +100,11 @@
     IdentifiedUser rebaseAsUser;
     if (input.onBehalfOfUploader && !rsrc.getPatchSet().uploader().equals(rsrc.getAccountId())) {
       rebaseAsUser =
-          userFactory.runAs(/* remotePeer= */ null, rsrc.getPatchSet().uploader(), rsrc.getUser());
+          userFactory.runAs(
+              /* remotePeer= */ null,
+              rsrc.getPatchSet().uploader(),
+              rsrc.getUser(),
+              ImpersonationPermissionMode.THIS_USER);
       rsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
       rebaseUtil.checkCanRebaseOnBehalfOf(rsrc, input);
     } else {
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index 658b6c3..279ba69 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
@@ -172,7 +173,10 @@
               && !revRsrc.getPatchSet().uploader().equals(revRsrc.getAccountId())) {
             rebaseAsUser =
                 userFactory.runAs(
-                    /* remotePeer= */ null, revRsrc.getPatchSet().uploader(), revRsrc.getUser());
+                    /* remotePeer= */ null,
+                    revRsrc.getPatchSet().uploader(),
+                    revRsrc.getUser(),
+                    ImpersonationPermissionMode.THIS_USER);
             rebaseUtil.checkCanRebaseOnBehalfOf(revRsrc, input);
             revRsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
             anyRebaseOnBehalfOfUploader = true;
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index d33f8df..8faa6a1 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -102,6 +102,12 @@
         .orElseThrow(illegalState(rsrc.getProject()))
         .checkStatePermitsWrite();
 
+    if (input == null) {
+      // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#restore-change
+      // input can be null if no review comments are set
+      input = new RestoreInput();
+    }
+
     Op op = new Op(input);
     try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
       try (BatchUpdate u =
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 2f9f71e..b2cf76c 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -314,11 +314,11 @@
   private void createCherryPickedRevert(
       RevertInput revertInput, Project.NameKey project, ChangeNotes changeNotes, Instant timestamp)
       throws IOException, ConfigInvalidException, UpdateException, RestApiException {
-    ObjectId revCommitId =
+    RevCommit revCommit =
         commitUtil.createRevertCommit(revertInput.message, changeNotes, user.get(), timestamp);
     // TODO (paiking): As a future change, the revert should just be done directly on the
     // target rather than just creating a commit and then cherry-picking it.
-    cherryPickInput.message = revertInput.message;
+    cherryPickInput.message = revCommit.getFullMessage();
     ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
     Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
     RevCommit baseCommit = null;
@@ -337,7 +337,7 @@
         bu.addOp(
             changeNotes.getChange().getId(),
             new CreateCherryPickOp(
-                revCommitId,
+                revCommit,
                 generatedChangeId,
                 cherryPickRevertChangeId,
                 timestamp,
@@ -392,21 +392,21 @@
     if (initialMessage == null) {
       initialMessage =
           MessageFormat.format(
-              ChangeMessages.get().revertSubmissionDefaultMessage,
+              ChangeMessages.revertSubmissionDefaultMessage,
               changeNotes.getCurrentPatchSet().commitId().name());
     }
 
     // For performance purposes: Almost all cases will end here.
     if (!subject.startsWith("Revert")) {
       return MessageFormat.format(
-          ChangeMessages.get().revertSubmissionUserMessage, subject, initialMessage);
+          ChangeMessages.revertSubmissionUserMessage, subject, initialMessage);
     }
 
     Matcher matcher = patternRevertSubjectWithNum.matcher(subject);
 
     if (matcher.matches()) {
       return MessageFormat.format(
-          ChangeMessages.get().revertSubmissionOfRevertSubmissionUserMessage,
+          ChangeMessages.revertSubmissionOfRevertSubmissionUserMessage,
           Integer.valueOf(matcher.group(1)) + 1,
           matcher.group(2),
           changeNotes.getCurrentPatchSet().commitId().name());
@@ -415,14 +415,14 @@
     matcher = patternRevertSubject.matcher(subject);
     if (matcher.matches()) {
       return MessageFormat.format(
-          ChangeMessages.get().revertSubmissionOfRevertSubmissionUserMessage,
+          ChangeMessages.revertSubmissionOfRevertSubmissionUserMessage,
           2,
           matcher.group(1),
           changeNotes.getCurrentPatchSet().commitId().name());
     }
 
     return MessageFormat.format(
-        ChangeMessages.get().revertSubmissionUserMessage, subject, initialMessage);
+        ChangeMessages.revertSubmissionUserMessage, subject, initialMessage);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 2adb9fc..9d4a463 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -240,7 +241,7 @@
   private ImmutableList<Account.Id> getMatchingReviewers(
       ImmutableList<ChangeData> changes, String query) {
     ImmutableList<Account.Id> reviewerIds = getReviewers(changes);
-    Map<Account.Id, AccountState> reviewerStates =
+    ImmutableMap<Account.Id, AccountState> reviewerStates =
         accountCache.get(ImmutableSet.copyOf(reviewerIds));
     return reviewerIds.stream()
         .filter(reviewerId -> accountMatchesQuery(reviewerStates.get(reviewerId), query))
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 6e6fb82..3cc578d 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -221,16 +221,29 @@
     try (Timer0.Context ctx = metrics.filterVisibility.start()) {
       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) && accountControl.canSee(reviewer)) {
-          filteredRecommendations.add(reviewer);
+        if (!visibilityControl.isVisibleTo(reviewer)) {
+          logger.atFine().log("Filter out %s because this user cannot see the change", reviewer);
+          continue;
         }
+
+        // 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;
+        }
+
+        filteredRecommendations.add(reviewer);
       }
     }
     logger.atFine().log("Filtered recommendations: %s", filteredRecommendations);
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 2cc44b5..991a83e 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -34,8 +34,6 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -51,20 +49,17 @@
   private final ChangeEditUtil editUtil;
   private final PatchSetUtil psUtil;
   private final PermissionBackend permissionBackend;
-  private final ProjectCache projectCache;
 
   @Inject
   Revisions(
       DynamicMap<RestView<RevisionResource>> views,
       ChangeEditUtil editUtil,
       PatchSetUtil psUtil,
-      PermissionBackend permissionBackend,
-      ProjectCache projectCache) {
+      PermissionBackend permissionBackend) {
     this.views = views;
     this.editUtil = editUtil;
     this.psUtil = psUtil;
     this.permissionBackend = permissionBackend;
-    this.projectCache = projectCache;
   }
 
   @Override
@@ -108,7 +103,7 @@
             .user(change.getUser())
             .change(change.getNotes())
             .test(ChangePermission.READ)
-        && projectCache.get(change.getProject()).map(ProjectState::statePermitsRead).orElse(false);
+        && change.getChangeData().projectStatePermitsRead();
   }
 
   private ImmutableList<RevisionResource> find(ChangeResource change, String id)
diff --git a/java/com/google/gerrit/server/restapi/change/RobotComments.java b/java/com/google/gerrit/server/restapi/change/RobotComments.java
index e02a39f..387bb68 100644
--- a/java/com/google/gerrit/server/restapi/change/RobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/RobotComments.java
@@ -14,16 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.RobotCommentResource;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -32,16 +29,11 @@
 public class RobotComments implements ChildCollection<RevisionResource, RobotCommentResource> {
   private final DynamicMap<RestView<RobotCommentResource>> views;
   private final ListRobotComments list;
-  private final CommentsUtil commentsUtil;
 
   @Inject
-  RobotComments(
-      DynamicMap<RestView<RobotCommentResource>> views,
-      ListRobotComments list,
-      CommentsUtil commentsUtil) {
+  RobotComments(DynamicMap<RestView<RobotCommentResource>> views, ListRobotComments list) {
     this.views = views;
     this.list = list;
-    this.commentsUtil = commentsUtil;
   }
 
   @Override
@@ -57,14 +49,6 @@
   @Override
   public RobotCommentResource parse(RevisionResource rev, IdString id)
       throws ResourceNotFoundException {
-    String uuid = id.get();
-    ChangeNotes notes = rev.getNotes();
-
-    for (RobotComment c : commentsUtil.robotCommentsByPatchSet(notes, rev.getPatchSet().id())) {
-      if (uuid.equals(c.key.uuid)) {
-        return new RobotCommentResource(rev, c);
-      }
-    }
-    throw new ResourceNotFoundException(id);
+    throw new ResourceNotFoundException("robot comments unsupported");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 36dae1a..94e4cf0 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.base.MoreObjects;
@@ -47,6 +46,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeJson;
@@ -58,8 +58,6 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.submit.ChangeSet;
@@ -125,8 +123,8 @@
   private final boolean submitWholeTopic;
   private final Provider<InternalChangeQuery> queryProvider;
   private final PatchSetUtil psUtil;
-  private final ProjectCache projectCache;
   private final ChangeJson.Factory json;
+  private final ChangeData.Factory changeDataFactory;
 
   private final boolean useMergeabilityCheck;
 
@@ -140,8 +138,8 @@
       @GerritServerConfig Config cfg,
       Provider<InternalChangeQuery> queryProvider,
       PatchSetUtil psUtil,
-      ProjectCache projectCache,
-      ChangeJson.Factory json) {
+      ChangeJson.Factory json,
+      ChangeData.Factory changeDataFactory) {
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.mergeOpProvider = mergeOpProvider;
@@ -174,8 +172,8 @@
                 cfg.getString("change", null, "submitTopicTooltip"), DEFAULT_TOPIC_TOOLTIP));
     this.queryProvider = queryProvider;
     this.psUtil = psUtil;
-    this.projectCache = projectCache;
     this.json = json;
+    this.changeDataFactory = changeDataFactory;
     this.useMergeabilityCheck = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi();
   }
 
@@ -198,10 +196,7 @@
     if (input.onBehalfOf != null) {
       submitter = onBehalfOf(rsrc, input);
     }
-    projectCache
-        .get(rsrc.getProject())
-        .orElseThrow(illegalState(rsrc.getProject()))
-        .checkStatePermitsWrite();
+    changeDataFactory.create(rsrc.getChange()).checkStatePermitsWrite();
 
     return Response.ok(json.noOptions().format(mergeChange(rsrc, submitter, input)));
   }
@@ -288,14 +283,12 @@
   @Override
   public UiAction.Description getDescription(RevisionResource resource)
       throws IOException, PermissionBackendException {
-    Change change = resource.getChange();
+    ChangeData changeData = resource.getChangeResource().getChangeData();
+    Change change = changeData.change();
     if (!change.isNew() || !resource.isCurrent()) {
       return null; // submit not visible
     }
-    if (!projectCache
-        .get(resource.getProject())
-        .map(ProjectState::statePermitsWrite)
-        .orElse(false)) {
+    if (!changeData.projectStatePermitsWrite()) {
       return null; // submit not visible
     }
 
@@ -459,9 +452,14 @@
 
     CurrentUser caller = rsrc.getUser();
     IdentifiedUser submitter =
-        accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
+        accountResolver
+            .resolve(in.onBehalfOf)
+            .asUniqueUserOnBehalfOf(caller, ImpersonationPermissionMode.REAL_USER);
     try {
-      permissionBackend.user(submitter).change(rsrc.getNotes()).check(ChangePermission.READ);
+      permissionBackend
+          .user(submitter, ImpersonationPermissionMode.THIS_USER)
+          .change(rsrc.getNotes())
+          .check(ChangePermission.READ);
     } catch (AuthException e) {
       throw new UnprocessableEntityException(
           String.format("on_behalf_of account %s cannot see change", submitter.getAccountId()), e);
diff --git a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
index 39be75e..ca0c796 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
 import static com.google.gerrit.server.config.ExperimentResource.EXPERIMENT_KIND;
+import static com.google.gerrit.server.config.GlobalLabelResource.GLOBAL_LABEL_KIND;
+import static com.google.gerrit.server.config.GlobalSubmitRequirementResource.GLOBAL_SUBMIT_REQUIREMENT_KIND;
 import static com.google.gerrit.server.config.IndexResource.INDEX_KIND;
 import static com.google.gerrit.server.config.IndexVersionResource.INDEX_VERSION_KIND;
 import static com.google.gerrit.server.config.TaskResource.TASK_KIND;
@@ -31,6 +33,8 @@
     DynamicMap.mapOf(binder(), CapabilityResource.CAPABILITY_KIND);
     DynamicMap.mapOf(binder(), CONFIG_KIND);
     DynamicMap.mapOf(binder(), EXPERIMENT_KIND);
+    DynamicMap.mapOf(binder(), GLOBAL_LABEL_KIND);
+    DynamicMap.mapOf(binder(), GLOBAL_SUBMIT_REQUIREMENT_KIND);
     DynamicMap.mapOf(binder(), TASK_KIND);
     DynamicMap.mapOf(binder(), TopMenuResource.TOP_MENU_KIND);
     DynamicMap.mapOf(binder(), INDEX_KIND);
@@ -45,7 +49,15 @@
     get(EXPERIMENT_KIND).to(GetExperiment.class);
 
     post(CONFIG_KIND, "index.changes").to(IndexChanges.class);
+
+    child(CONFIG_KIND, "indexes").to(IndexCollection.class);
+    post(INDEX_KIND, "snapshot").to(SnapshotIndex.class);
+    get(INDEX_KIND).to(GetIndex.class);
+
     get(CONFIG_KIND, "info").to(GetServerInfo.class);
+
+    child(CONFIG_KIND, "labels").to(GlobalLabelsCollection.class);
+
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
     get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
@@ -64,15 +76,15 @@
     child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
     get(CONFIG_KIND, "version").to(GetVersion.class);
 
-    child(CONFIG_KIND, "indexes").to(IndexCollection.class);
-    post(INDEX_KIND, "snapshot").to(SnapshotIndex.class);
-    get(INDEX_KIND).to(GetIndex.class);
+    child(CONFIG_KIND, "submit-requirements").to(GlobalSubmitRequirementsCollection.class);
 
     child(INDEX_KIND, "versions").to(IndexVersionsCollection.class);
     get(INDEX_VERSION_KIND).to(GetIndexVersion.class);
     post(INDEX_VERSION_KIND, "snapshot").to(SnapshotIndexVersion.class);
     post(INDEX_VERSION_KIND, "reindex").to(ReindexIndexVersion.class);
 
+    post(CONFIG_KIND, "passwords.to.tokens").to(MigratePasswordsToTokens.class);
+    post(CONFIG_KIND, "reduce.token.lifetime").to(ReduceMaxTokenLifetime.class);
     // The caches and summary REST endpoints are bound via RestCacheAdminModule.
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 0b254ce..9129bfb 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.common.DownloadInfo;
 import com.google.gerrit.extensions.common.DownloadSchemeInfo;
 import com.google.gerrit.extensions.common.GerritInfo;
+import com.google.gerrit.extensions.common.GroupsInfo;
 import com.google.gerrit.extensions.common.MetadataInfo;
 import com.google.gerrit.extensions.common.PluginConfigInfo;
 import com.google.gerrit.extensions.common.ReceiveInfo;
@@ -155,6 +156,7 @@
     info.change = getChangeInfo();
     info.download = getDownloadInfo();
     info.gerrit = getGerritInfo();
+    info.groups = getGroupsInfo();
     info.noteDbEnabled = true;
     info.plugin = getPluginInfo();
     info.defaultTheme = getDefaultTheme();
@@ -164,6 +166,7 @@
     info.user = getUserInfo();
     info.receive = getReceiveInfo();
     info.submitRequirementDashboardColumns = getSubmitRequirementDashboardColumns();
+    info.dashboardShowAllLabels = getDashboardShowAllLabels();
     info.metadata = getMetadata();
     return Response.ok(info);
   }
@@ -182,6 +185,9 @@
     info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
     info.switchAccountUrl = authConfig.getSwitchAccountUrl();
     info.gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
+    if (authConfig.getMaxAuthTokenLifetime().isPresent()) {
+      info.maxTokenLifetime = authConfig.getMaxAuthTokenLifetime().get().toMinutes();
+    }
 
     if (info.useContributorAgreements != null) {
       ImmutableCollection<ContributorAgreement> agreements =
@@ -225,9 +231,10 @@
         toBoolean(this.config.getBoolean("change", null, "disablePrivateChanges", false));
     info.mergeabilityComputationBehavior =
         MergeabilityComputationBehavior.fromConfig(config).name();
-    info.enableRobotComments = toBoolean(config.getBoolean("change", "enableRobotComments", false));
     info.conflictsPredicateEnabled =
         toBoolean(config.getBoolean("change", "conflictsPredicateEnabled", true));
+    info.allowMarkdownBase64ImagesInComments =
+        toBoolean(config.getBoolean("change", "allowMarkdownBase64ImagesInComments", false));
     return info;
   }
 
@@ -300,6 +307,13 @@
     return info;
   }
 
+  private GroupsInfo getGroupsInfo() {
+    GroupsInfo info = new GroupsInfo();
+    info.enableDeleteGroup =
+        toBoolean(config.getBoolean("groups", null, "enableDeleteGroup", false));
+    return info;
+  }
+
   @Nullable
   private String getDocUrl() {
     String docUrl = config.getString("gerrit", null, "docUrl");
@@ -377,6 +391,10 @@
     return Arrays.asList(config.getStringList("dashboard", null, "submitRequirementColumns"));
   }
 
+  private Boolean getDashboardShowAllLabels() {
+    return toBoolean(config.getBoolean("dashboard", null, "showAllLabels", false));
+  }
+
   private ImmutableList<MetadataInfo> getMetadata() {
     ArrayList<MetadataInfo> metadataList = new ArrayList<>();
     serverStateProviders.runEach(
diff --git a/java/com/google/gerrit/server/restapi/config/GlobalLabelsCollection.java b/java/com/google/gerrit/server/restapi/config/GlobalLabelsCollection.java
new file mode 100644
index 0000000..dae32fb
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/GlobalLabelsCollection.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.GlobalLabelResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GlobalLabelsCollection
+    implements ChildCollection<ConfigResource, GlobalLabelResource> {
+  private final Provider<ListGlobalLabels> list;
+  private final DynamicMap<RestView<GlobalLabelResource>> views;
+
+  @Inject
+  GlobalLabelsCollection(
+      Provider<ListGlobalLabels> list, DynamicMap<RestView<GlobalLabelResource>> views) {
+    this.list = list;
+    this.views = views;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public GlobalLabelResource parse(ConfigResource parent, IdString id)
+      throws ResourceNotFoundException {
+    // We don't support "GET /config/server/labels/<label-name>" as we have no need for this REST
+    // endpoint. Return "404 Not Found" if anyone tries to call this non-existing REST endpoint.
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public DynamicMap<RestView<GlobalLabelResource>> views() {
+    return views;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/GlobalSubmitRequirementsCollection.java b/java/com/google/gerrit/server/restapi/config/GlobalSubmitRequirementsCollection.java
new file mode 100644
index 0000000..e48af64
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/GlobalSubmitRequirementsCollection.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.GlobalSubmitRequirementResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GlobalSubmitRequirementsCollection
+    implements ChildCollection<ConfigResource, GlobalSubmitRequirementResource> {
+  private final Provider<ListGlobalSubmitRequirements> list;
+  private final DynamicMap<RestView<GlobalSubmitRequirementResource>> views;
+
+  @Inject
+  GlobalSubmitRequirementsCollection(
+      Provider<ListGlobalSubmitRequirements> list,
+      DynamicMap<RestView<GlobalSubmitRequirementResource>> views) {
+    this.list = list;
+    this.views = views;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public GlobalSubmitRequirementResource parse(ConfigResource parent, IdString id)
+      throws ResourceNotFoundException {
+    // We don't support "GET /config/server/submit-requirements/<submit-requirement-name>" as we
+    // have no need for this REST endpoint. Return "404 Not Found" if anyone tries to call this
+    // non-existing REST endpoint.
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public DynamicMap<RestView<GlobalSubmitRequirementResource>> views() {
+    return views;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index 6c8bf74..5b36950 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -68,8 +68,8 @@
   private Map<String, CapabilityInfo> collectCoreCapabilities()
       throws IllegalAccessException, NoSuchFieldException {
     Map<String, CapabilityInfo> output = new HashMap<>();
-    Class<? extends CapabilityConstants> bundleClass = CapabilityConstants.get().getClass();
-    CapabilityConstants c = CapabilityConstants.get();
+    Class<? extends CapabilityConstants> bundleClass = new CapabilityConstants().getClass();
+    CapabilityConstants c = new CapabilityConstants();
     for (String id : GlobalCapability.getAllNames()) {
       String name = (String) bundleClass.getField(id).get(c);
       output.put(id, new CapabilityInfo(id, name));
diff --git a/java/com/google/gerrit/server/restapi/config/ListGlobalLabels.java b/java/com/google/gerrit/server/restapi/config/ListGlobalLabels.java
new file mode 100644
index 0000000..8714140
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ListGlobalLabels.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.config;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.project.LabelDefinitionJson;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ListGlobalLabels implements RestReadView<ConfigResource> {
+  private final Provider<CurrentUser> user;
+  private final PluginSetContext<LabelType> globalLabelTypes;
+
+  @Inject
+  ListGlobalLabels(Provider<CurrentUser> user, PluginSetContext<LabelType> globalLabelTypes) {
+    this.user = user;
+    this.globalLabelTypes = globalLabelTypes;
+  }
+
+  @Override
+  public Response<ImmutableList<LabelDefinitionInfo>> apply(ConfigResource resource)
+      throws AuthException {
+    if (!user.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    return Response.ok(
+        globalLabelTypes.stream()
+            .sorted(comparing(LabelType::getName))
+            .map(
+                globalLabelType ->
+                    LabelDefinitionJson.format(/* projectName= */ null, globalLabelType))
+            .collect(toImmutableList()));
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListGlobalSubmitRequirements.java b/java/com/google/gerrit/server/restapi/config/ListGlobalSubmitRequirements.java
new file mode 100644
index 0000000..4651acb
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ListGlobalSubmitRequirements.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.config;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.project.SubmitRequirementJson;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ListGlobalSubmitRequirements implements RestReadView<ConfigResource> {
+  private final Provider<CurrentUser> user;
+  private final PluginSetContext<SubmitRequirement> globalSubmitRequirement;
+
+  @Inject
+  ListGlobalSubmitRequirements(
+      Provider<CurrentUser> user, PluginSetContext<SubmitRequirement> globalSubmitRequirement) {
+    this.user = user;
+    this.globalSubmitRequirement = globalSubmitRequirement;
+  }
+
+  @Override
+  public Response<ImmutableList<SubmitRequirementInfo>> apply(ConfigResource resource)
+      throws AuthException {
+    if (!user.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    return Response.ok(
+        globalSubmitRequirement.stream()
+            .sorted(comparing(SubmitRequirement::name))
+            .map(
+                globalSubmitRequirement ->
+                    SubmitRequirementJson.format(/* projectName= */ null, globalSubmitRequirement))
+            .collect(toImmutableList()));
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/MigratePasswordsToTokens.java b/java/com/google/gerrit/server/restapi/config/MigratePasswordsToTokens.java
new file mode 100644
index 0000000..522792e
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/MigratePasswordsToTokens.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.config;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.PasswordMigrator;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.restapi.account.CreateToken;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.concurrent.Future;
+
+@RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
+@Singleton
+public class MigratePasswordsToTokens
+    implements RestModifyView<
+        ConfigResource, com.google.gerrit.server.restapi.config.MigratePasswordsToTokens.Input> {
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Optional<Duration> maxTokenLifetime;
+  private final WorkQueue workQueue;
+  private final PasswordMigrator.Factory passwordMigratorFactory;
+
+  static class Input {
+    String lifetime;
+  }
+
+  @Inject
+  public MigratePasswordsToTokens(
+      AuthConfig authConfig,
+      WorkQueue workQueue,
+      PasswordMigrator.Factory passwordMigratorFactory) {
+    this.maxTokenLifetime = authConfig.getMaxAuthTokenLifetime();
+    this.workQueue = workQueue;
+    this.passwordMigratorFactory = passwordMigratorFactory;
+  }
+
+  @Override
+  public Response<?> apply(ConfigResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+    Optional<Instant> expirationDate =
+        CreateToken.getExpirationInstant(input.lifetime, Optional.empty());
+
+    if (maxTokenLifetime.isPresent()) {
+      if (expirationDate.isEmpty()
+          || expirationDate.get().isAfter(Instant.now().plus(maxTokenLifetime.get()))) {
+        throw new BadRequestException(
+            "A lifetime shorter than the maximum allowed lifetime has to be provided.");
+      }
+    }
+
+    @SuppressWarnings("unused")
+    Future<?> possiblyIgnoredError =
+        workQueue.getDefaultQueue().submit(passwordMigratorFactory.create(expirationDate));
+    return Response.accepted("Password Migrator task added to work queue.");
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ReduceMaxTokenLifetime.java b/java/com/google/gerrit/server/restapi/config/ReduceMaxTokenLifetime.java
new file mode 100644
index 0000000..db1aad1
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ReduceMaxTokenLifetime.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.config;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.MaxAuthTokenLifetimeApplier;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.restapi.account.CreateToken;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.concurrent.Future;
+
+@RequiresCapability(GlobalCapability.MAINTAIN_SERVER)
+@Singleton
+public class ReduceMaxTokenLifetime
+    implements RestModifyView<ConfigResource, ReduceMaxTokenLifetime.Input> {
+  static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final WorkQueue workQueue;
+  private final MaxAuthTokenLifetimeApplier.Factory maxAuthTokenLifetimeApplierFactory;
+
+  static class Input {
+    String lifetime;
+  }
+
+  @Inject
+  public ReduceMaxTokenLifetime(
+      WorkQueue workQueue, MaxAuthTokenLifetimeApplier.Factory maxAuthTokenLifetimeApplierFactory) {
+    this.workQueue = workQueue;
+    this.maxAuthTokenLifetimeApplierFactory = maxAuthTokenLifetimeApplierFactory;
+  }
+
+  @Override
+  public Response<?> apply(ConfigResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+    Optional<Instant> expirationDate =
+        CreateToken.getExpirationInstant(input.lifetime, Optional.empty());
+    if (expirationDate.isEmpty()) {
+      return Response.none();
+    }
+    @SuppressWarnings("unused")
+    Future<?> possiblyIgnoredError =
+        workQueue
+            .getDefaultQueue()
+            .submit(maxAuthTokenLifetimeApplierFactory.create(expirationDate.get()));
+    return Response.accepted("Password Migrator task added to work queue.");
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/CreateFlow.java b/java/com/google/gerrit/server/restapi/flow/CreateFlow.java
new file mode 100644
index 0000000..94c893f
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/CreateFlow.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowPermissionDeniedException;
+import com.google.gerrit.server.flow.FlowServiceUtil;
+import com.google.gerrit.server.flow.InvalidFlowException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * REST endpoint to create a flow .
+ *
+ * <p>This REST endpoint handles {@code POST /change/<change-id>/flows} requests.
+ */
+@Singleton
+public class CreateFlow
+    implements RestCollectionModifyView<ChangeResource, FlowResource, FlowInput> {
+  @VisibleForTesting public static final int DEFAULT_MAX_FLOWS_PER_CHANGE = 20;
+
+  private final Provider<Config> cfgProvider;
+  private final FlowServiceUtil flowServiceUtil;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  CreateFlow(
+      @GerritServerConfig Provider<Config> cfgProvider,
+      FlowServiceUtil flowServiceUtil,
+      Provider<CurrentUser> self) {
+    this.cfgProvider = cfgProvider;
+    this.flowServiceUtil = flowServiceUtil;
+    this.self = self;
+  }
+
+  @Override
+  public Response<FlowInfo> apply(ChangeResource changeResource, FlowInput flowInput)
+      throws AuthException,
+          BadRequestException,
+          MethodNotAllowedException,
+          ResourceConflictException {
+    if (!self.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    if (flowInput == null) {
+      flowInput = new FlowInput();
+    }
+
+    int maxFlowsPerChange =
+        cfgProvider.get().getInt("flows", "maxPerChange", DEFAULT_MAX_FLOWS_PER_CHANGE);
+    if (maxFlowsPerChange > 0
+        && flowServiceUtil
+                .getFlowServiceOrThrow()
+                .listFlows(changeResource.getProject(), changeResource.getId())
+                .size()
+            >= maxFlowsPerChange) {
+      throw new ResourceConflictException(
+          String.format("Too many flows (max %s flow allowed per change)", maxFlowsPerChange));
+    }
+
+    FlowCreation flowCreation =
+        FlowJson.createFlowCreation(
+            changeResource.getProject(),
+            changeResource.getId(),
+            self.get().asIdentifiedUser().getAccountId(),
+            flowInput);
+
+    try {
+      Flow flow = flowServiceUtil.getFlowServiceOrThrow().createFlow(flowCreation);
+      return Response.created(FlowJson.format(flow));
+    } catch (FlowPermissionDeniedException e) {
+      throw new AuthException(e.getMessage(), e);
+    } catch (InvalidFlowException e) {
+      throw new BadRequestException(e.getMessage(), e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/DeleteFlow.java b/java/com/google/gerrit/server/restapi/flow/DeleteFlow.java
new file mode 100644
index 0000000..07dc216
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/DeleteFlow.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.flow.FlowPermissionDeniedException;
+import com.google.gerrit.server.flow.FlowServiceUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+/**
+ * REST endpoint to delete a flow .
+ *
+ * <p>This REST endpoint handles {@code DELETE /change/<change-id>/flows/<flow-id>} requests.
+ */
+@Singleton
+public class DeleteFlow implements RestModifyView<FlowResource, Input> {
+  private final FlowServiceUtil flowServiceUtil;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  DeleteFlow(FlowServiceUtil flowServiceUtil, Provider<CurrentUser> self) {
+    this.flowServiceUtil = flowServiceUtil;
+    this.self = self;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public Response<FlowInfo> apply(FlowResource flowResource, Input input)
+      throws AuthException, MethodNotAllowedException {
+    if (!self.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    try {
+      flowServiceUtil.getFlowServiceOrThrow().deleteFlow(flowResource.getFlow().key());
+    } catch (FlowPermissionDeniedException e) {
+      throw new AuthException(e.getMessage(), e);
+    }
+
+    return Response.none();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/FlowCollection.java b/java/com/google/gerrit/server/restapi/flow/FlowCollection.java
new file mode 100644
index 0000000..ce703e9
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/FlowCollection.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowKey;
+import com.google.gerrit.server.flow.FlowServiceUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** REST collection that serves requests to {@code /changes/<change-id>/flows}. */
+@Singleton
+public class FlowCollection implements ChildCollection<ChangeResource, FlowResource> {
+  private final FlowServiceUtil flowServiceUtil;
+  private final DynamicMap<RestView<FlowResource>> views;
+  private final ListFlows listFlows;
+
+  @Inject
+  FlowCollection(
+      FlowServiceUtil flowServiceUtil,
+      ListFlows listFlows,
+      DynamicMap<RestView<FlowResource>> views) {
+    this.flowServiceUtil = flowServiceUtil;
+    this.listFlows = listFlows;
+    this.views = views;
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    return listFlows;
+  }
+
+  @Override
+  public FlowResource parse(ChangeResource changeResource, IdString flowUuid)
+      throws ResourceNotFoundException, MethodNotAllowedException {
+    FlowKey flowKey =
+        FlowKey.create(changeResource.getProject(), changeResource.getId(), flowUuid.get());
+    Flow flow =
+        flowServiceUtil
+            .getFlowServiceOrThrow()
+            .getFlow(flowKey)
+            .orElseThrow(
+                () ->
+                    new ResourceNotFoundException(
+                        String.format("Flow %s not found.", flowUuid.get())));
+    return new FlowResource(flow);
+  }
+
+  @Override
+  public DynamicMap<RestView<FlowResource>> views() {
+    return views;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/FlowJson.java b/java/com/google/gerrit/server/restapi/flow/FlowJson.java
new file mode 100644
index 0000000..f1f1ea8
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/FlowJson.java
@@ -0,0 +1,193 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.FlowActionInfo;
+import com.google.gerrit.extensions.common.FlowExpressionInfo;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.extensions.common.FlowStageInfo;
+import com.google.gerrit.extensions.common.FlowStageState;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowAction;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowExpression;
+import com.google.gerrit.server.flow.FlowStage;
+import com.google.gerrit.server.flow.FlowStageEvaluationStatus;
+
+/**
+ * Produces flow-related entities, like {@link FlowInfo}s, which are serialized to JSON afterwards.
+ */
+public class FlowJson {
+  /** Formats the given {@link Flow} instance as a {@link FlowInfo}. */
+  public static FlowInfo format(Flow flow) {
+    requireNonNull(flow, "flow");
+
+    FlowInfo flowInfo = new FlowInfo();
+    flowInfo.uuid = flow.key().uuid();
+    flowInfo.owner = new AccountInfo(flow.ownerId().get());
+    flowInfo.setCreated(flow.createdOn());
+    flowInfo.stages = flow.stages().stream().map(FlowJson::format).collect(toImmutableList());
+
+    if (flow.lastEvaluatedOn().isPresent()) {
+      flowInfo.setLastEvaluated(flow.lastEvaluatedOn().get());
+    }
+
+    return flowInfo;
+  }
+
+  /** Formats the given {@link FlowStage} instance as a {@link FlowStageInfo}. */
+  private static FlowStageInfo format(FlowStage flowStage) {
+    requireNonNull(flowStage, "flowStage");
+
+    FlowStageInfo flowStageInfo = new FlowStageInfo();
+    flowStageInfo.expression = format(flowStage.expression());
+    flowStageInfo.state = mapState(flowStage.status().state());
+    flowStageInfo.message = flowStage.status().message().orElse(null);
+    return flowStageInfo;
+  }
+
+  /** Formats the given {@link FlowExpression} instance as a {@link FlowExpressionInfo}. */
+  private static FlowExpressionInfo format(FlowExpression flowExpression) {
+    requireNonNull(flowExpression, "flowExpression");
+
+    FlowExpressionInfo flowExpressionInfo = new FlowExpressionInfo();
+    flowExpressionInfo.condition = flowExpression.condition();
+    if (flowExpression.action().isPresent()) {
+      flowExpressionInfo.action = format(flowExpression.action().get());
+    }
+    return flowExpressionInfo;
+  }
+
+  /** Formats the given {@link FlowAction} instance as a {@link FlowActionInfo}. */
+  private static FlowActionInfo format(FlowAction flowAction) {
+    requireNonNull(flowAction, "flowAction");
+
+    FlowActionInfo flowActionInfo = new FlowActionInfo();
+    flowActionInfo.name = flowAction.name();
+    flowActionInfo.parameters = flowAction.parameters();
+    return flowActionInfo;
+  }
+
+  /**
+   * Maps the given {@link com.google.gerrit.server.flow.FlowStageEvaluationStatus.State} to a
+   * {@link FlowStageState}.
+   */
+  @VisibleForTesting
+  public static FlowStageState mapState(FlowStageEvaluationStatus.State flowStageState) {
+    requireNonNull(flowStageState, "flowStageState");
+
+    return switch (flowStageState) {
+      case UNKNOWN ->
+          throw new IllegalStateException("The flow stage state has not been initialized");
+      case DONE -> FlowStageState.DONE;
+      case PENDING -> FlowStageState.PENDING;
+      case FAILED -> FlowStageState.FAILED;
+      case TERMINATED -> FlowStageState.TERMINATED;
+    };
+  }
+
+  /**
+   * Create a {@link FlowCreation} from the given {@link FlowInput}.
+   *
+   * @throws BadRequestException thrown if mandatory properties are missing
+   */
+  public static FlowCreation createFlowCreation(
+      Project.NameKey projectName, Change.Id changeId, Account.Id ownerId, FlowInput flowInput)
+      throws BadRequestException {
+    requireNonNull(projectName, "projectName");
+    requireNonNull(changeId, "changeId");
+    requireNonNull(ownerId, "ownerId");
+    requireNonNull(flowInput, "flowInput");
+
+    if (flowInput.stageExpressions == null || flowInput.stageExpressions.isEmpty()) {
+      throw new BadRequestException("at least one stage expression is required");
+    }
+
+    if (Iterables.getLast(flowInput.stageExpressions).action == null) {
+      throw new BadRequestException("the last stage expression is required to have an action");
+    }
+
+    FlowCreation.Builder flowCreationBuilder =
+        FlowCreation.builder().projectName(projectName).changeId(changeId).ownerId(ownerId);
+
+    for (FlowExpressionInfo flowExpressionInfo : flowInput.stageExpressions) {
+      flowCreationBuilder.addStageExpression(createFlowExpression(flowExpressionInfo));
+    }
+
+    return flowCreationBuilder.build();
+  }
+
+  /**
+   * Create a {@link FlowExpression} from the given {@link FlowExpressionInfo}.
+   *
+   * @throws BadRequestException thrown if mandatory properties are missing
+   */
+  public static FlowExpression createFlowExpression(FlowExpressionInfo flowExpressionInfo)
+      throws BadRequestException {
+    requireNonNull(flowExpressionInfo, "flowExpressionInfo");
+
+    if (Strings.isNullOrEmpty(flowExpressionInfo.condition)) {
+      throw new BadRequestException("condition in stage expression is required");
+    }
+    var builder = FlowExpression.builder().condition(flowExpressionInfo.condition);
+    if (flowExpressionInfo.action != null) {
+      builder.action(createFlowAction(flowExpressionInfo.action));
+    }
+
+    return builder.build();
+  }
+
+  /**
+   * Create a {@link FlowAction} from the given {@link FlowActionInfo}.
+   *
+   * @throws BadRequestException thrown if mandatory properties are missing
+   */
+  public static FlowAction createFlowAction(FlowActionInfo flowActionInfo)
+      throws BadRequestException {
+    requireNonNull(flowActionInfo, "flowActionInfo");
+
+    if (Strings.isNullOrEmpty(flowActionInfo.name)) {
+      throw new BadRequestException("name in action is required");
+    }
+
+    FlowAction.Builder flowActionBuilder = FlowAction.builder().name(flowActionInfo.name);
+
+    if (flowActionInfo.parameters != null) {
+      flowActionBuilder.parameters(ImmutableList.copyOf(flowActionInfo.parameters));
+    }
+
+    return flowActionBuilder.build();
+  }
+
+  /**
+   * Private constructor to prevent instantiation of this class.
+   *
+   * <p>This class contains only static methods and hence never needs to be instantiated.
+   */
+  private FlowJson() {}
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/FlowResource.java b/java/com/google/gerrit/server/restapi/flow/FlowResource.java
new file mode 100644
index 0000000..7a45cb5
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/FlowResource.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.flow.Flow;
+import com.google.inject.TypeLiteral;
+
+/** REST resource that represents members in the {@link FlowCollection}. */
+public class FlowResource implements RestResource {
+  public static final TypeLiteral<RestView<FlowResource>> FLOW_KIND = new TypeLiteral<>() {};
+
+  private final Flow flow;
+
+  public FlowResource(Flow flow) {
+    this.flow = flow;
+  }
+
+  public Flow getFlow() {
+    return flow;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java b/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java
new file mode 100644
index 0000000..442e6b1
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/FlowRestApiModule.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
+import static com.google.gerrit.server.restapi.flow.FlowResource.FLOW_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+/** Guice module to bind the flow REST API. */
+public class FlowRestApiModule extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(FlowCollection.class);
+
+    DynamicMap.mapOf(binder(), FLOW_KIND);
+
+    child(CHANGE_KIND, "flows").to(FlowCollection.class);
+    postOnCollection(FLOW_KIND).to(CreateFlow.class);
+
+    get(FLOW_KIND).to(GetFlow.class);
+    delete(FLOW_KIND).to(DeleteFlow.class);
+
+    get(CHANGE_KIND, "is-flows-enabled").to(IsFlowsEnabled.class);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/GetFlow.java b/java/com/google/gerrit/server/restapi/flow/GetFlow.java
new file mode 100644
index 0000000..da2fddb
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/GetFlow.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
+
+/**
+ * REST endpoint to get a flow .
+ *
+ * <p>This REST endpoint handles {@code GET /change/<change-id>/flows/<flow-id>} requests.
+ */
+@Singleton
+public class GetFlow implements RestReadView<FlowResource> {
+  @Override
+  public Response<FlowInfo> apply(FlowResource flowResource) {
+    return Response.ok(FlowJson.format(flowResource.getFlow()));
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java b/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
new file mode 100644
index 0000000..a4e2c24
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/IsFlowsEnabled.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import com.google.gerrit.extensions.common.IsFlowsEnabledInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.gerrit.server.flow.FlowServiceUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Optional;
+
+/**
+ * REST endpoint to check if Flows is enabled for the user.
+ *
+ * <p>This REST endpoint handles {@code GET /change/<change-id>/isFlowsEnabled} requests.
+ */
+@Singleton
+public class IsFlowsEnabled implements RestReadView<ChangeResource> {
+  private final FlowServiceUtil flowServiceUtil;
+
+  @Inject
+  IsFlowsEnabled(FlowServiceUtil flowServiceUtil) {
+    this.flowServiceUtil = flowServiceUtil;
+  }
+
+  @Override
+  public Response<IsFlowsEnabledInfo> apply(ChangeResource changeResource) throws RestApiException {
+    Optional<FlowService> flowService = flowServiceUtil.getFlowService();
+    if (flowService.isEmpty()) {
+      return Response.ok(new IsFlowsEnabledInfo(false));
+    }
+    IsFlowsEnabledInfo enabledInfo =
+        new IsFlowsEnabledInfo(
+            flowService.get().isFlowsEnabled(changeResource.getProject(), changeResource.getId()));
+    return Response.ok(enabledInfo);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/flow/ListFlows.java b/java/com/google/gerrit/server/restapi/flow/ListFlows.java
new file mode 100644
index 0000000..0c43e93
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/flow/ListFlows.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.flow;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowServiceUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+
+/**
+ * REST endpoint to list members of the {@link FlowCollection}.
+ *
+ * <p>This REST endpoint handles {@code GET /change/<change-id>/flows} requests.
+ */
+@Singleton
+public class ListFlows implements RestReadView<ChangeResource> {
+  private final FlowServiceUtil flowServiceUtil;
+
+  @Inject
+  ListFlows(FlowServiceUtil flowServiceUtil) {
+    this.flowServiceUtil = flowServiceUtil;
+  }
+
+  @Override
+  public Response<List<FlowInfo>> apply(ChangeResource changeResource)
+      throws MethodNotAllowedException {
+    ImmutableList<Flow> flows =
+        flowServiceUtil
+            .getFlowServiceOrThrow()
+            .listFlows(changeResource.getProject(), changeResource.getId());
+    return Response.ok(flows.stream().map(FlowJson::format).collect(toImmutableList()));
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 3b16129..5763cdf1 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -314,7 +314,7 @@
     }
     List<GroupReference> groupRefs =
         groupBackend.suggest(suggest, projects.stream().findFirst().orElse(null)).stream()
-            .limit(limit <= 0 ? 10 : Math.min(limit, 10))
+            .limit(limit <= 0 ? 30 : Math.min(limit, 30))
             .collect(toList());
 
     List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
diff --git a/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java b/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java
index a84068d..6a99bed 100644
--- a/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/AbstractPostCollection.java
@@ -87,11 +87,11 @@
       configChanged = true;
     }
     if (input.create != null && !input.create.isEmpty()) {
-      for (TItemInput labelInput : input.create) {
-        if (labelInput == null) {
+      for (TItemInput itemInput : input.create) {
+        if (itemInput == null) {
           throw new BadRequestException("The create property contains a null item");
         }
-        createItem(config, labelInput);
+        createItem(config, itemInput);
       }
       configChanged = true;
     }
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 160dbdc..66ca854 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -87,12 +86,10 @@
 
       Account.Id match = accountResolver.resolve(input.account).asUnique().account().id();
 
-      try {
-        permissionBackend
-            .absentUser(match)
-            .project(rsrc.getNameKey())
-            .check(ProjectPermission.ACCESS);
-      } catch (AuthException e) {
+      if (!permissionBackend
+          .absentUser(match)
+          .project(rsrc.getNameKey())
+          .test(ProjectPermission.ACCESS)) {
         return Response.ok(
             createInfo(
                 HttpServletResponse.SC_FORBIDDEN,
@@ -115,20 +112,17 @@
       }
 
       String message = null;
-      if (!Strings.isNullOrEmpty(input.ref)) {
-        try {
-          permissionBackend
+      if (!Strings.isNullOrEmpty(input.ref)
+          && !permissionBackend
               .absentUser(match)
               .ref(BranchNameKey.create(rsrc.getNameKey(), input.ref))
-              .check(refPerm);
-        } catch (AuthException e) {
-          return Response.ok(
-              createInfo(
-                  HttpServletResponse.SC_FORBIDDEN,
-                  String.format(
-                      "user %s lacks permission %s for %s in project %s",
-                      match, input.permission, input.ref, rsrc.getName())));
-        }
+              .test(refPerm)) {
+        return Response.ok(
+            createInfo(
+                HttpServletResponse.SC_FORBIDDEN,
+                String.format(
+                    "user %s lacks permission %s for %s in project %s",
+                    match, input.permission, input.ref, rsrc.getName())));
       } else {
         // We say access is okay if there are no refs, but this warrants a warning,
         // as access denied looks the same as no branches to the user.
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index b7fad53..ef1d619 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -113,13 +113,24 @@
       if (input.revision != null) {
         input.revision = input.revision.trim();
       }
+      if (input.sourceRef != null) {
+        input.sourceRef = input.sourceRef.trim();
+      }
       if (input.createEmptyCommit) {
-        if (!Strings.isNullOrEmpty(input.revision)) {
-          throw new BadRequestException("create_empty_commit and revision are mutually exclusive");
+        if (!Strings.isNullOrEmpty(input.revision) || !Strings.isNullOrEmpty(input.sourceRef)) {
+          throw new BadRequestException(
+              "create_empty_commit and revision/source_ref are mutually exclusive");
         }
       } else {
         if (Strings.isNullOrEmpty(input.revision)) {
+          if (!Strings.isNullOrEmpty(input.sourceRef)) {
+            throw new BadRequestException("must not provide source_ref if not providing revision");
+          }
           input.revision = Constants.HEAD;
+        } else if (!Strings.isNullOrEmpty(input.sourceRef)) {
+          if (input.revision.startsWith(RefNames.REFS)) {
+            throw new BadRequestException("must not provide source_ref if revision is a ref name");
+          }
         }
       }
 
@@ -255,11 +266,17 @@
           PermissionBackendException,
           ResourceConflictException,
           IOException,
-          NoSuchProjectException {
+          NoSuchProjectException,
+          UnprocessableEntityException {
     if (input.createEmptyCommit) {
       permissionBackend.user(identifiedUser.get()).ref(branchNameKey).check(RefPermission.CREATE);
     } else {
-      Ref sourceRef = repo.exactRef(input.revision);
+      Ref sourceRef;
+      if (!Strings.isNullOrEmpty(input.sourceRef)) {
+        sourceRef = repo.exactRef(input.sourceRef);
+      } else {
+        sourceRef = repo.exactRef(input.revision);
+      }
       if (sourceRef == null) {
         createRefControl.checkCreateRef(
             identifiedUser, repo, branchNameKey, revObject, /* forPush= */ false);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index 4ccb074..35102d8 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -71,11 +74,41 @@
     if (input.name != null && !input.name.equals(id.get())) {
       throw new BadRequestException("name in input must match name in URL");
     }
+
+    if (input.function == null) {
+      input.function = LabelFunction.NO_OP.getFunctionName();
+    }
+
+    LabelDefinitionInputValidator.validate(input);
+
+    return Response.created(createLabelWithoutInputValidation(rsrc.getNameKey(), id.get(), input));
+  }
+
+  /**
+   * Create label without any input validation.
+   *
+   * <p>This is useful to create labels with deprecated functions from tests.
+   *
+   * @param projectName the name of the project in which the label should be created
+   * @param input the input for creating the input
+   * @return the definition of the created label
+   */
+  @VisibleForTesting
+  @CanIgnoreReturnValue
+  public LabelDefinitionInfo createLabelWithoutInputValidation(
+      Project.NameKey projectName, String labelName, LabelDefinitionInput input)
+      throws AuthException,
+          BadRequestException,
+          MethodNotAllowedException,
+          PermissionBackendException,
+          ConfigInvalidException,
+          IOException,
+          ResourceConflictException {
     try (var configUpdater =
-        repoMetaDataUpdater.configUpdater(rsrc.getNameKey(), input.commitMessage, "Update label")) {
-      LabelType labelType = createLabel(configUpdater.getConfig(), id.get(), input);
+        repoMetaDataUpdater.configUpdater(projectName, input.commitMessage, "Update label")) {
+      LabelType labelType = createLabelType(configUpdater.getConfig(), labelName, input);
       configUpdater.commitConfigUpdate();
-      return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType));
+      return LabelDefinitionJson.format(projectName, labelType);
     }
   }
 
@@ -90,7 +123,7 @@
    * @throws ResourceConflictException if the label cannot be created due to a conflict
    */
   @SuppressWarnings("deprecation")
-  public LabelType createLabel(ProjectConfig config, String label, LabelDefinitionInput input)
+  public LabelType createLabelType(ProjectConfig config, String label, LabelDefinitionInput input)
       throws BadRequestException, ResourceConflictException {
     if (config.getLabelSections().containsKey(label)) {
       throw new ResourceConflictException(String.format("label %s already exists", label));
@@ -140,6 +173,7 @@
       labelType.setCanOverride(input.canOverride);
     }
 
+    input.copyCondition = Strings.emptyToNull(input.copyCondition);
     if (input.copyCondition != null) {
       try {
         @SuppressWarnings("unused")
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index f02b49f..053ac3c 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -166,6 +166,7 @@
     } catch (ConfigInvalidException e) {
       throw new BadRequestException(e.getMessage());
     }
+    args.initOnly = input.initOnly;
 
     Lock nameLock = lockManager.call(lockManager -> lockManager.getLock(args.getProject().get()));
     nameLock.lock();
@@ -190,7 +191,8 @@
         @SuppressWarnings("unused")
         var unused = putConfig.get().apply(projectState, in);
       }
-      return Response.created(json.format(projectState));
+      ProjectInfo projectInfo = json.format(projectState);
+      return input.initOnly ? Response.ok(projectInfo) : Response.created(projectInfo);
     } finally {
       nameLock.unlock();
     }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
index c11d2cd..86d42cb 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
@@ -75,7 +75,7 @@
       SubmitRequirement submitRequirement = updateConfig(config, id, input);
 
       configUpdater.commitConfigUpdate();
-      return Response.created(SubmitRequirementJson.format(submitRequirement));
+      return Response.created(SubmitRequirementJson.format(rsrc.getNameKey(), submitRequirement));
     } catch (ConfigInvalidException e) {
       throw new IOException("Failed to read project config", e);
     } catch (ResourceConflictException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index e7357d2..f74ddf0 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -18,6 +18,7 @@
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
@@ -47,6 +48,8 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.time.ZoneId;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.TagCommand;
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
@@ -61,6 +64,12 @@
 @Singleton
 public class CreateTag implements RestCollectionCreateView<ProjectResource, TagResource, TagInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final Pattern SIGNATURE_PATTERN =
+      Pattern.compile(Pattern.quote("-----BEGIN ") + "(.*)" + Pattern.quote(" SIGNATURE-----\n"));
+
+  private static final ImmutableList<String> ALLOWED_SIGNATURES = ImmutableList.of("SSH");
+
   private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
   private final TagCache tagCache;
@@ -92,6 +101,9 @@
     if (input.ref != null && !ref.equals(input.ref)) {
       throw new BadRequestException("ref must match URL");
     }
+    if (input.date != null && TimeUtil.now().isBefore(input.date.toInstant())) {
+      throw new BadRequestException("date cannot be in the future");
+    }
     if (input.revision != null) {
       input.revision = input.revision.trim();
     }
@@ -111,15 +123,31 @@
           RevObject object = rw.parseAny(revid);
           rw.reset();
           boolean isAnnotated = Strings.emptyToNull(input.message) != null;
-          boolean isSigned =
-              isAnnotated && input.message.contains("-----BEGIN PGP SIGNATURE-----\n");
-          if (isSigned) {
-            throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\"");
-          } else if (isAnnotated) {
-            if (!check(perm, RefPermission.CREATE_TAG)) {
-              throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
-            }
 
+          boolean isSigned = false;
+          if (input.message != null) {
+            Matcher signatureMatcher = SIGNATURE_PATTERN.matcher(input.message);
+            isSigned = signatureMatcher.find();
+
+            if (isSigned) {
+              String signatureKind = signatureMatcher.group(1);
+              if (!ALLOWED_SIGNATURES.contains(signatureKind)) {
+                throw new MethodNotAllowedException(
+                    String.format(
+                        "Cannot create signed tag \"%s\": %s signature not supported",
+                        ref, signatureKind));
+              }
+            }
+          }
+
+          if (isSigned) {
+            if (!perm.test(RefPermission.CREATE_SIGNED_TAG)) {
+              throw new AuthException(String.format("Cannot create signed tag \"%s\"", ref));
+            }
+          } else if (isAnnotated) {
+            if (!perm.test(RefPermission.CREATE_TAG)) {
+              throw new AuthException(String.format("Cannot create annotated tag \"%s\"", ref));
+            }
           } else {
             perm.check(RefPermission.CREATE);
           }
@@ -133,7 +161,7 @@
                     .setObjectId(object)
                     .setName(ref.substring(R_TAGS.length()))
                     .setAnnotated(isAnnotated)
-                    .setSigned(isSigned);
+                    .setSigned(false);
 
             if (isAnnotated) {
               tag.setMessage(input.message)
@@ -141,7 +169,9 @@
                       resource
                           .getUser()
                           .asIdentifiedUser()
-                          .newCommitterIdent(TimeUtil.now(), ZoneId.systemDefault()));
+                          .newCommitterIdent(
+                              input.date != null ? input.date.toInstant() : TimeUtil.now(),
+                              ZoneId.systemDefault()));
             }
 
             try {
@@ -170,14 +200,4 @@
       }
     }
   }
-
-  private static boolean check(PermissionBackend.ForRef perm, RefPermission permission)
-      throws PermissionBackendException {
-    try {
-      perm.check(permission);
-      return true;
-    } catch (AuthException e) {
-      return false;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteChanges.java b/java/com/google/gerrit/server/restapi/project/DeleteChanges.java
new file mode 100644
index 0000000..9868354
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/DeleteChanges.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.project;
+
+import static com.google.gerrit.extensions.api.projects.DeleteChangesResult.FAILURE;
+import static com.google.gerrit.extensions.api.projects.DeleteChangesResult.NOT_UNIQUE;
+import static com.google.gerrit.extensions.api.projects.DeleteChangesResult.SUCCESS;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.api.projects.DeleteChangesInput;
+import com.google.gerrit.extensions.api.projects.DeleteChangesResult;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.change.DeleteChangeOp;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class DeleteChanges implements RestModifyView<ProjectResource, DeleteChangesInput> {
+
+  private final PermissionBackend permissionBackend;
+  private final ChangeFinder changeFinder;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final DeleteChangeOp.Factory opFactory;
+
+  @Inject
+  public DeleteChanges(
+      PermissionBackend permissionBackend,
+      ChangeFinder changeFinder,
+      BatchUpdate.Factory batchUpdateFactory,
+      DeleteChangeOp.Factory opFactory) {
+    this.permissionBackend = permissionBackend;
+    this.changeFinder = changeFinder;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.opFactory = opFactory;
+  }
+
+  @Override
+  public Response<Map<DeleteChangesResult, Collection<String>>> apply(
+      ProjectResource resource, DeleteChangesInput input)
+      throws RestApiException, UpdateException, PermissionBackendException {
+    if (input == null || input.changes == null || input.changes.isEmpty()) {
+      throw new BadRequestException("Change Ids must be specified");
+    }
+    ListMultimap<DeleteChangesResult, String> responseBody = ArrayListMultimap.create();
+    List<String> deletableChanges = new ArrayList<>();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          batchUpdateFactory.create(resource.getNameKey(), resource.getUser(), TimeUtil.now())) {
+        for (String change : input.changes) {
+          List<ChangeNotes> cn = changeFinder.find(change);
+          if (cn.isEmpty()) {
+            responseBody.put(FAILURE, change);
+          } else if (cn.size() > 1) {
+            responseBody.put(NOT_UNIQUE, change);
+          } else {
+            if (isChangeDeletable(cn.getFirst())) {
+              checkPermissions(cn);
+              Change.Id changeId = cn.getFirst().getChange().getId();
+              bu.addOp(changeId, opFactory.create(changeId));
+              deletableChanges.add(change);
+            } else {
+              responseBody.put(FAILURE, change);
+            }
+          }
+        }
+        bu.execute();
+        responseBody.putAll(SUCCESS, deletableChanges);
+      }
+    }
+    return Response.ok(responseBody.asMap());
+  }
+
+  public void checkPermissions(List<ChangeNotes> cn)
+      throws PermissionBackendException, AuthException {
+    if (cn.getFirst() != null) {
+      permissionBackend.currentUser().change(cn.getFirst()).check(ChangePermission.DELETE);
+    }
+  }
+
+  private static boolean isChangeDeletable(ChangeNotes cn) {
+    // Merged changes must not be deleted.
+    // New or abandoned changes can be deleted with the right permissions.
+    return !cn.getChange().isMerged();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 388946e..c2060e5 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -228,18 +228,14 @@
     if (isConfigRef(refName)) {
       // Never allow to delete the meta config branch.
       command.setResult(Result.REJECTED_OTHER_REASON, "not allowed to delete branch " + refName);
-    } else {
-      try {
-        permissionBackend
-            .currentUser()
-            .project(projectState.getNameKey())
-            .ref(refName)
-            .check(RefPermission.DELETE);
-      } catch (AuthException denied) {
-        command.setResult(
-            Result.REJECTED_OTHER_REASON,
-            "it doesn't exist or you do not have permission to delete it");
-      }
+    } else if (!permissionBackend
+        .currentUser()
+        .project(projectState.getNameKey())
+        .ref(refName)
+        .test(RefPermission.DELETE)) {
+      command.setResult(
+          Result.REJECTED_OTHER_REASON,
+          "it doesn't exist or you do not have permission to delete it");
     }
 
     if (!projectState.statePermitsWrite()) {
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 5b8357d..eb279be 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -54,7 +53,6 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectJson;
@@ -177,8 +175,8 @@
     info.local = new HashMap<>();
     info.ownerOf = new HashSet<>();
     Map<AccountGroup.UUID, GroupInfo> groups = new HashMap<>();
-    boolean canReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
-    boolean canWriteConfig = check(perm, ProjectPermission.WRITE_CONFIG);
+    boolean canReadConfig = perm.ref(RefNames.REFS_CONFIG).test(READ);
+    boolean canWriteConfig = perm.test(ProjectPermission.WRITE_CONFIG);
 
     // Check if the project state permits read only when the user is not allowed to write the config
     // (=owner). This is so that the owner can still read (and in the next step write) the project's
@@ -199,14 +197,14 @@
         }
 
       } else if (AccessSection.isValidRefSectionName(name)) {
-        if (check(perm, name, WRITE_CONFIG)) {
+        if (perm.ref(name).test(WRITE_CONFIG)) {
           info.local.put(name, createAccessSection(groups, section));
           info.ownerOf.add(name);
 
         } else if (canReadConfig) {
           info.local.put(name, createAccessSection(groups, section));
 
-        } else if (check(perm, name, READ)) {
+        } else if (perm.ref(name).test(READ)) {
           // Filter the section to only add rules describing groups that
           // are visible to the current-user. This includes any group the
           // user is a member of, as well as groups they own or that
@@ -237,15 +235,11 @@
       }
     }
 
-    if (info.ownerOf.isEmpty()) {
-      try {
-        permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-        // Special case: If the section list is empty, this project has no current
-        // access control information. Fall back to site administrators.
-        info.ownerOf.add(AccessSection.ALL);
-      } catch (AuthException e) {
-        // Do nothing.
-      }
+    if (info.ownerOf.isEmpty()
+        && permissionBackend.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
+      // Special case: If the section list is empty, this project has no current
+      // access control information. Fall back to site administrators.
+      info.ownerOf.add(AccessSection.ALL);
     }
 
     if (config.getRevision() != null) {
@@ -301,26 +295,6 @@
     }
   }
 
-  private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm)
-      throws PermissionBackendException {
-    try {
-      ctx.ref(ref).check(perm);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
-
-  private static boolean check(PermissionBackend.ForProject ctx, ProjectPermission perm)
-      throws PermissionBackendException {
-    try {
-      ctx.check(perm);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
-
   private AccessSectionInfo createAccessSection(
       Map<AccountGroup.UUID, GroupInfo> groups, AccessSection section) {
     AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
diff --git a/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java
index ce482e3..7f92337 100644
--- a/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java
@@ -26,6 +26,7 @@
 public class GetSubmitRequirement implements RestReadView<SubmitRequirementResource> {
   @Override
   public Response<SubmitRequirementInfo> apply(SubmitRequirementResource rsrc) {
-    return Response.ok(SubmitRequirementJson.format(rsrc.getSubmitRequirement()));
+    return Response.ok(
+        SubmitRequirementJson.format(rsrc.getProject().getNameKey(), rsrc.getSubmitRequirement()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputValidator.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputValidator.java
new file mode 100644
index 0000000..ea7be80
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputValidator.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+
+/** Validates {@link LabelDefinitionInput}'s. */
+class LabelDefinitionInputValidator {
+  static void validate(LabelDefinitionInput input) throws BadRequestException {
+    validate(/* labelName= */ null, input);
+  }
+
+  static void validate(@Nullable String labelName, LabelDefinitionInput input)
+      throws BadRequestException {
+    if (input.function != null) {
+      if (LabelFunction.ANY_WITH_BLOCK.getFunctionName().equals(input.function)
+          || LabelFunction.MAX_WITH_BLOCK.getFunctionName().equals(input.function)
+          || LabelFunction.MAX_NO_BLOCK.getFunctionName().equals(input.function)) {
+        StringBuilder msg = new StringBuilder();
+        msg.append(String.format("Function %s", input.function));
+        if (labelName != null) {
+          msg.append(String.format(" of label %s", labelName));
+        }
+        msg.append(" is deprecated.");
+        msg.append(
+            String.format(
+                " The function can only be set to %s. Use submit requirements instead of label"
+                    + " functions.",
+                ImmutableList.of(
+                    LabelFunction.NO_BLOCK.getFunctionName(),
+                    LabelFunction.NO_OP.getFunctionName(),
+                    LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+        throw new BadRequestException(msg.toString());
+      }
+    }
+  }
+
+  private LabelDefinitionInputValidator() {}
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index 1c2ce5a..72a4d9f 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -61,6 +60,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
@@ -179,8 +179,12 @@
     // Filter on refs/heads/*, substring and regex without checking ref visibility
     List<Ref> allBranches = readAllBranches(rsrc);
     Set<String> targets = getTargets(allBranches);
+    // If you filter on "main" then you also want to find "HEAD" pointing to "main", not just
+    // where "main" is pointing to. Thus match on both name and target name for symbolic refs.
+    Function<Ref, String> refNameExtractor =
+        (Ref r) -> r.getName() + (r.isSymbolic() ? " " + r.getTarget().getName() : "");
     ImmutableList<Ref> filtered =
-        new RefFilter<>(Constants.R_HEADS, (Ref r) -> r.getName())
+        new RefFilter<>(Constants.R_HEADS, refNameExtractor)
             .subString(matchSubstring).regex(matchRegex).filter(allBranches).stream()
                 .sorted(new RefComparator())
                 .collect(ImmutableList.toImmutableList());
@@ -328,9 +332,7 @@
       //
       String target = ref.getTarget().getName();
 
-      try {
-        perm.ref(target).check(RefPermission.READ);
-      } catch (AuthException e) {
+      if (!perm.ref(target).test(RefPermission.READ)) {
         return Optional.empty();
       }
 
@@ -356,15 +358,12 @@
       return Optional.of(info);
     }
 
-    try {
-      perm.ref(ref.getName()).check(RefPermission.READ);
-      BranchInfo branchInfo =
-          createBranchInfo(perm.ref(ref.getName()), ref, projectState, currentUser, targets);
-      return Optional.of(branchInfo);
-    } catch (AuthException e) {
-      // Do nothing.
+    if (!perm.ref(ref.getName()).test(RefPermission.READ)) {
       return Optional.empty();
     }
+    BranchInfo branchInfo =
+        createBranchInfo(perm.ref(ref.getName()), ref, projectState, currentUser, targets);
+    return Optional.of(branchInfo);
   }
 
   private static Set<String> getTargets(List<Ref> refs) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 59a33a2..08f4f01 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -20,7 +20,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.DashboardInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -111,11 +110,8 @@
         RevWalk rw = new RevWalk(git)) {
       ImmutableList.Builder<DashboardInfo> all = ImmutableList.builder();
       for (Ref ref : git.getRefDatabase().getRefsByPrefix(REFS_DASHBOARDS)) {
-        try {
-          perm.ref(ref.getName()).check(RefPermission.READ);
+        if (perm.ref(ref.getName()).test(RefPermission.READ)) {
           all.addAll(scanDashboards(state.getProject(), git, rw, ref, project, setDefault));
-        } catch (AuthException e) {
-          // Do nothing.
         }
       }
       return all.build();
diff --git a/java/com/google/gerrit/server/restapi/project/ListLabels.java b/java/com/google/gerrit/server/restapi/project/ListLabels.java
index 8be2646..d94b4b3 100644
--- a/java/com/google/gerrit/server/restapi/project/ListLabels.java
+++ b/java/com/google/gerrit/server/restapi/project/ListLabels.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static java.util.Comparator.comparing;
+
 import com.google.common.collect.ImmutableCollection;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.LabelType;
@@ -32,6 +34,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.LabelDefinitionJson;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
@@ -50,15 +53,18 @@
   private final Provider<CurrentUser> user;
   private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
+  private final PluginSetContext<LabelType> globalLabelTypes;
 
   @Inject
   public ListLabels(
       Provider<CurrentUser> user,
       PermissionBackend permissionBackend,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      PluginSetContext<LabelType> globalLabelTypes) {
     this.user = user;
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
+    this.globalLabelTypes = globalLabelTypes;
   }
 
   @Option(name = "--inherited", usage = "to include inherited label definitions")
@@ -94,6 +100,14 @@
 
     if (inherited) {
       List<LabelDefinitionInfo> allLabels = new ArrayList<>();
+
+      globalLabelTypes.stream()
+          .sorted(comparing(LabelType::getName))
+          .forEach(
+              globalLabelType ->
+                  allLabels.add(
+                      LabelDefinitionJson.format(/* projectName= */ null, globalLabelType)));
+
       for (ProjectState projectState : rsrc.getProjectState().treeInOrder()) {
         try {
           permissionBackend
@@ -125,8 +139,7 @@
 
   public List<LabelDefinitionInfo> filterLabelsThatUserCanVoteOnRef(
       ProjectResource rsrc, List<LabelDefinitionInfo> allLabels)
-      throws AuthException,
-          PermissionBackendException,
+      throws PermissionBackendException,
           ResourceConflictException,
           RepositoryNotFoundException,
           IOException {
@@ -161,11 +174,14 @@
         continue;
       }
 
+      // We assume that user is interested in labels that can be voted on if
+      // the user is the change owner.
       Set<LabelPermission.WithValue> can =
           permissionBackend
               .currentUser()
               .project(rsrc.getNameKey())
               .ref(branchNameKey.branch())
+              .changeToBeCreated(/* isOwner= */ true)
               .test(labelType);
 
       for (LabelValue v : labelType.getValues()) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
index ae03d6a..e1d1a1a 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -601,19 +600,13 @@
     Project.NameKey name = state.getNameKey();
     Boolean b = checked.get(name);
     if (b == null) {
-      try {
-        // Hidden projects(permitsRead = false) should only be accessible by the project owners.
-        // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
-        // be allowed for other users). Allowing project owners to access here will help them to
-        // view
-        // and update the config of hidden projects easily.
-        ProjectPermission permissionToCheck =
-            state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
-        perm.project(name).check(permissionToCheck);
-        b = true;
-      } catch (AuthException denied) {
-        b = false;
-      }
+      // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+      // READ_CONFIG is checked here because it's only allowed to project owners (ACCESS may also
+      // be allowed for other users). Allowing project owners to access here will help them to
+      // view and update the config of hidden projects easily.
+      ProjectPermission permissionToCheck =
+          state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+      b = perm.project(name).test(permissionToCheck);
       checked.put(name, b);
     }
     return b;
@@ -679,13 +672,10 @@
       PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
       for (int i = 0; i < showBranch.size(); i++) {
         Ref ref = git.findRef(showBranch.get(i));
-        if (ref != null && ref.getObjectId() != null) {
-          try {
-            perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
-            result[i] = ref;
-          } catch (AuthException e) {
-            continue;
-          }
+        if (ref != null
+            && ref.getObjectId() != null
+            && perm.ref(ref.getLeaf().getName()).test(RefPermission.READ)) {
+          result[i] = ref;
         }
       }
     } catch (IOException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListSubmitRequirements.java b/java/com/google/gerrit/server/restapi/project/ListSubmitRequirements.java
index 69e2cd3..c099923 100644
--- a/java/com/google/gerrit/server/restapi/project/ListSubmitRequirements.java
+++ b/java/com/google/gerrit/server/restapi/project/ListSubmitRequirements.java
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static java.util.Comparator.comparing;
+
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -23,6 +26,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SubmitRequirementJson;
@@ -36,11 +40,16 @@
 public class ListSubmitRequirements implements RestReadView<ProjectResource> {
   private final Provider<CurrentUser> user;
   private final PermissionBackend permissionBackend;
+  private final PluginSetContext<SubmitRequirement> globalSubmitRequirements;
 
   @Inject
-  public ListSubmitRequirements(Provider<CurrentUser> user, PermissionBackend permissionBackend) {
+  public ListSubmitRequirements(
+      Provider<CurrentUser> user,
+      PermissionBackend permissionBackend,
+      PluginSetContext<SubmitRequirement> globalSubmitRequirements) {
     this.user = user;
     this.permissionBackend = permissionBackend;
+    this.globalSubmitRequirements = globalSubmitRequirements;
   }
 
   @Option(name = "--inherited", usage = "to include inherited submit requirements")
@@ -60,6 +69,15 @@
 
     if (inherited) {
       List<SubmitRequirementInfo> allSubmitRequirements = new ArrayList<>();
+
+      globalSubmitRequirements.stream()
+          .sorted(comparing(SubmitRequirement::name))
+          .forEach(
+              globalSubmitRequirement ->
+                  allSubmitRequirements.add(
+                      SubmitRequirementJson.format(
+                          /* projectName= */ null, globalSubmitRequirement)));
+
       for (ProjectState projectState : rsrc.getProjectState().treeInOrder()) {
         try {
           permissionBackend
@@ -80,7 +98,9 @@
 
   private ImmutableList<SubmitRequirementInfo> listSubmitRequirements(ProjectState projectState) {
     return projectState.getConfig().getSubmitRequirementSections().values().stream()
-        .map(SubmitRequirementJson::format)
+        .map(
+            submitRequirement ->
+                SubmitRequirementJson.format(projectState.getNameKey(), submitRequirement))
         .collect(ImmutableList.toImmutableList());
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabels.java b/java/com/google/gerrit/server/restapi/project/PostLabels.java
index 7f502ff..2182784 100644
--- a/java/com/google/gerrit/server/restapi/project/PostLabels.java
+++ b/java/com/google/gerrit/server/restapi/project/PostLabels.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.extensions.common.BatchLabelInput;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
@@ -54,17 +55,19 @@
   }
 
   @Override
-  protected boolean updateItem(ProjectConfig config, String name, LabelDefinitionInput resource)
+  protected boolean updateItem(ProjectConfig config, String name, LabelDefinitionInput labelInput)
       throws BadRequestException, ResourceConflictException, UnprocessableEntityException {
     LabelType labelType = config.getLabelSections().get(name);
     if (labelType == null) {
       throw new UnprocessableEntityException(String.format("label %s not found", name));
     }
-    if (resource.commitMessage != null) {
+    if (labelInput.commitMessage != null) {
       throw new BadRequestException("commit message on label definition input not supported");
     }
 
-    return setLabel.updateLabel(config, labelType, resource);
+    LabelDefinitionInputValidator.validate(name, labelInput);
+
+    return setLabel.updateLabel(config, labelType, labelInput);
   }
 
   @Override
@@ -76,8 +79,14 @@
     if (labelInput.commitMessage != null) {
       throw new BadRequestException("commit message on label definition input not supported");
     }
+    if (labelInput.function == null) {
+      labelInput.function = LabelFunction.NO_OP.getFunctionName();
+    }
+
+    LabelDefinitionInputValidator.validate(labelInput.name, labelInput);
+
     @SuppressWarnings("unused")
-    var unused = createLabel.createLabel(config, labelInput.name.trim(), labelInput);
+    var unused = createLabel.createLabelType(config, labelInput.name.trim(), labelInput);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java b/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
index 2ca027b..4e9d432 100644
--- a/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.extensions.common.BatchLabelInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -26,6 +28,7 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Map;
 import javax.inject.Singleton;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -48,6 +51,24 @@
           ConfigInvalidException,
           UpdateException,
           RestApiException {
+    if (input == null) {
+      input = new BatchLabelInput();
+    }
+
+    if (input.create != null) {
+      for (LabelDefinitionInput labelDefinitionInput : input.create) {
+        if (labelDefinitionInput.function == null) {
+          labelDefinitionInput.function = LabelFunction.NO_OP.getFunctionName();
+        }
+        LabelDefinitionInputValidator.validate(labelDefinitionInput.name, labelDefinitionInput);
+      }
+    }
+    if (input.update != null) {
+      for (Map.Entry<String, LabelDefinitionInput> updateEntry : input.update.entrySet()) {
+        LabelDefinitionInputValidator.validate(updateEntry.getKey(), updateEntry.getValue());
+      }
+    }
+
     try (ConfigChangeCreator creator =
         repoMetaDataUpdater.configChangeCreator(
             rsrc.getNameKey(), input.commitMessage, "Review labels change")) {
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index 18dbe13..f5647ec 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -128,6 +128,8 @@
     delete(TAG_KIND).to(DeleteTag.class);
 
     post(PROJECT_KIND, "tags:delete").to(DeleteTags.class);
+
+    post(PROJECT_KIND, "changes:delete").to(DeleteChanges.class);
   }
 
   /** Separately bind batch functionality. */
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index 8917719..2d9cead 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.ProjectUtil;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.NeedsParams;
@@ -152,17 +151,14 @@
       // 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()) {
-        try {
-          permissionBackend.currentUser().project(nameKey).check(ProjectPermission.ACCESS);
-        } catch (AuthException e) {
+        if (!permissionBackend.currentUser().project(nameKey).test(ProjectPermission.ACCESS)) {
           return null;
         }
-      } else {
-        try {
-          permissionBackend.currentUser().project(nameKey).check(ProjectPermission.WRITE_CONFIG);
-        } catch (AuthException e) {
-          state.get().checkStatePermitsRead();
-        }
+      } else if (!permissionBackend
+          .currentUser()
+          .project(nameKey)
+          .test(ProjectPermission.WRITE_CONFIG)) {
+        state.get().checkStatePermitsRead();
       }
     }
     return new ProjectResource(state.get(), user.get());
diff --git a/java/com/google/gerrit/server/restapi/project/PutProject.java b/java/com/google/gerrit/server/restapi/project/PutProject.java
index 5b11143..cbec6a6 100644
--- a/java/com/google/gerrit/server/restapi/project/PutProject.java
+++ b/java/com/google/gerrit/server/restapi/project/PutProject.java
@@ -14,18 +14,41 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
+@RequiresCapability(GlobalCapability.CREATE_PROJECT)
 @Singleton
 public class PutProject implements RestModifyView<ProjectResource, ProjectInput> {
+  private final CreateProject createProject;
+
+  @Inject
+  PutProject(CreateProject createProject) {
+    this.createProject = createProject;
+  }
+
   @Override
-  public Response<?> apply(ProjectResource resource, ProjectInput input)
-      throws ResourceConflictException {
-    throw new ResourceConflictException("Project \"" + resource.getName() + "\" already exists");
+  public Response<?> apply(ProjectResource projectResource, ProjectInput input)
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
+    if (input.initOnly) {
+      return createProject.apply(
+          TopLevelResource.INSTANCE, IdString.fromDecoded(input.name), input);
+    }
+
+    throw new ResourceConflictException(
+        "Project \"" + projectResource.getName() + "\" already exists");
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
index 7cc4d75..a34b7d8 100644
--- a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.Strings;
@@ -30,7 +29,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.CurrentUser;
@@ -126,25 +124,20 @@
   @MustBeClosed
   public ConfigChangeCreator configChangeCreator(
       Project.NameKey projectName, @Nullable String message, String defaultMessage)
-      throws PermissionBackendException,
-          AuthException,
-          ResourceConflictException,
-          IOException,
-          ConfigInvalidException {
+      throws PermissionBackendException, AuthException, IOException, ConfigInvalidException {
     message = validateMessage(message, defaultMessage);
     PermissionBackend.ForProject forProject =
         permissionBackend.user(user.get()).project(projectName);
-    if (!check(forProject, ProjectPermission.READ_CONFIG)) {
+    if (!forProject.test(ProjectPermission.READ_CONFIG)) {
       throw new AuthException(RefNames.REFS_CONFIG + " not visible");
     }
-    if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
+    if (!forProject.test(ProjectPermission.WRITE_CONFIG)) {
       try {
         forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
       } catch (AuthException denied) {
         throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG, denied);
       }
     }
-    projectCache.get(projectName).orElseThrow(illegalState(projectName)).checkStatePermitsWrite();
     // The MetaDataUpdate instance gets closed in the ConfigChangeCreator.close() method.
     MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName);
     try {
@@ -360,14 +353,4 @@
     }
     return message;
   }
-
-  private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
-      throws PermissionBackendException {
-    try {
-      perm.check(p);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index 5a58a4f..33d4640 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -61,6 +61,10 @@
       input = new LabelDefinitionInput();
     }
 
+    if (input.function != null) {
+      LabelDefinitionInputValidator.validate(input);
+    }
+
     LabelType labelType = rsrc.getLabelType();
 
     try (var configUpdater =
diff --git a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
index 80346a6..d3a251e 100644
--- a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
@@ -82,7 +82,8 @@
 
       configUpdater.commitConfigUpdate();
 
-      return Response.created(SubmitRequirementJson.format(submitRequirement));
+      return Response.created(
+          SubmitRequirementJson.format(rsrc.getProject().getNameKey(), submitRequirement));
     } catch (ConfigInvalidException e) {
       throw new IOException("Failed to read project config", e);
     }
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index c6d7dc9..fcd50f6 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedConfigFile;
 import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -40,7 +41,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
-import javax.inject.Inject;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -126,12 +126,12 @@
    */
   public Status executeMigration(Project.NameKey project, UpdateUI ui)
       throws IOException, ConfigInvalidException {
+    boolean updated = false;
     if (hasPrologRules(project)) {
       ui.message(String.format("Skipping project %s because it has prolog rules", project));
       return Status.HAS_PROLOG;
     }
     VersionedConfigFile projectConfig = new VersionedConfigFile(ProjectConfig.PROJECT_CONFIG);
-    boolean migrationPerformed = false;
     try (Repository repo = repoManager.openRepository(project);
         MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo)) {
       if (hasMigrationAlreadyRun(repo)) {
@@ -146,7 +146,6 @@
       Config cfg = projectConfig.getConfig();
       Map<String, LabelAttributes> labelTypes = getLabelTypes(cfg);
       Map<String, SubmitRequirement> existingSubmitRequirements = loadSubmitRequirements(cfg);
-      boolean updated = false;
       for (Map.Entry<String, LabelAttributes> lt : labelTypes.entrySet()) {
         String labelName = lt.getKey();
         LabelAttributes attributes = lt.getValue();
@@ -186,10 +185,9 @@
       }
       if (updated) {
         commit(projectConfig, md);
-        migrationPerformed = true;
       }
     }
-    return migrationPerformed ? Status.MIGRATED : Status.NO_CHANGE;
+    return updated ? Status.MIGRATED : Status.NO_CHANGE;
   }
 
   /**
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 6afb289..82efa23 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -71,6 +71,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -467,11 +468,12 @@
     try {
       Set<ChangePermission> can =
           permissionBackend
-              .user(caller.getRealUser())
+              .user(caller)
               .change(cd)
               .test(
                   EnumSet.of(
                       ChangePermission.READ, ChangePermission.SUBMIT, ChangePermission.SUBMIT_AS));
+      String callerName = caller.getUserForPermission().getLoggableName();
       if (!can.contains(ChangePermission.READ)) {
         // The READ permission should already be handled during generation of ChangeSet, however
         // MergeSuperSetComputation is an interface and on API level doesn't guarantee that this
@@ -481,7 +483,7 @@
           logger.atFine().log(
               "Change %d cannot be submitted by user %s because it depends on change %d which the"
                   + "user cannot read",
-              triggeringChangeId.get(), caller.getRealUser().getLoggableName(), cd.getId().get());
+              triggeringChangeId.get(), callerName, cd.getId().get());
           problems.add(
               ChangeProblem.create(
                   cd.getId(),
@@ -490,7 +492,7 @@
         } else {
           logger.atFine().log(
               "Change %d cannot be submitted by user %s because they don't have READ permission",
-              triggeringChangeId.get(), caller.getRealUser().getLoggableName());
+              triggeringChangeId.get(), callerName);
           problems.add(
               ChangeProblem.create(
                   cd.getId(), String.format("Change %d is not visible", triggeringChangeId.get())));
@@ -502,11 +504,11 @@
           logger.atFine().log(
               "Change %d cannot be submitted by user %s because it depends on change %d which the"
                   + "user cannot submit",
-              triggeringChangeId.get(), caller.getRealUser().getLoggableName(), cd.getId().get());
+              triggeringChangeId.get(), callerName, cd.getId().get());
         } else {
           logger.atFine().log(
               "Change %d cannot be submitted by user %s because they don't have SUBMIT permission",
-              triggeringChangeId.get(), caller.getRealUser().getLoggableName());
+              triggeringChangeId.get(), callerName);
         }
         problems.add(
             ChangeProblem.create(
@@ -514,30 +516,29 @@
                 String.format("Insufficient permission to submit change %d", cd.getId().get())));
         return;
       }
-      if (caller.isImpersonating()) {
-        if (!permissionBackend.user(caller).change(cd).test(ChangePermission.READ)) {
+      if (caller.isImpersonated()) {
+        String impersonatedName = caller.getLoggableName();
+        if (!permissionBackend
+            .user(caller, ImpersonationPermissionMode.THIS_USER)
+            .change(cd)
+            .test(ChangePermission.READ)) {
           if (triggeringChangeId.get() != cd.getId().get()) {
             logger.atFine().log(
                 "Change %d cannot be submitted by user %s on behalf of user %s because it depends"
                     + " on change %d which the on-behalf-of user does not have READ permission for",
-                triggeringChangeId.get(),
-                caller.getRealUser().getLoggableName(),
-                caller.getLoggableName(),
-                cd.getId().get());
+                triggeringChangeId.get(), callerName, impersonatedName, cd.getId().get());
           } else {
             logger.atFine().log(
                 "Change %d cannot be submitted by user %s on behalf of user %s because the"
                     + " on-behalf-of user does not have READ permission",
-                triggeringChangeId.get(),
-                caller.getRealUser().getLoggableName(),
-                caller.getLoggableName());
+                triggeringChangeId.get(), callerName, impersonatedName);
           }
           problems.add(
               ChangeProblem.create(
                   cd.getId(),
                   String.format(
                       "On-behalf-of user %s lacks permission to read change %d",
-                      caller.getLoggableName(), cd.getId().get())));
+                      impersonatedName, cd.getId().get())));
           return;
         }
         if (!can.contains(ChangePermission.SUBMIT_AS)) {
@@ -545,24 +546,19 @@
             logger.atFine().log(
                 "Change %d cannot be submitted by user %s on behalf of user %s because it depends"
                     + " on change %d which the user does not have SUBMIT_AS permission for",
-                triggeringChangeId.get(),
-                caller.getRealUser().getLoggableName(),
-                caller.getLoggableName(),
-                cd.getId().get());
+                triggeringChangeId.get(), callerName, impersonatedName, cd.getId().get());
           } else {
             logger.atFine().log(
                 "Change %d cannot be submitted by user %s on behalf of user %s because they do not"
                     + " have SUBMIT_AS permission",
-                triggeringChangeId.get(),
-                caller.getRealUser().getLoggableName(),
-                caller.getLoggableName());
+                triggeringChangeId.get(), callerName, impersonatedName);
           }
           problems.add(
               ChangeProblem.create(
                   cd.getId(),
                   String.format(
                       "Insufficient permission to submit change %d on behalf of user %s",
-                      cd.getId().get(), caller.getLoggableName())));
+                      cd.getId().get(), impersonatedName)));
           return;
         }
       }
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index 4190a7d..fc05e31 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -21,10 +21,11 @@
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
+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.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -128,29 +129,29 @@
   public ChangeSet completeChangeSet(
       Change change, CurrentUser user, boolean includingTopicClosure, boolean backfill)
       throws IOException, PermissionBackendException {
-    try {
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "MergeSuperSet#completeChangeSet",
+            Metadata.builder().changeId(change.getId().get()).build())) {
       if (orm == null) {
         orm = repoManagerProvider.get();
         closeOrm = true;
       }
       ChangeData cd = changeDataFactory.create(change.getProject(), change.getId());
-      boolean visible = false;
-      if (cd != null) {
-        if (projectCache.get(cd.project()).map(ProjectState::statePermitsRead).orElse(false)) {
-          try {
-            permissionBackend.user(user).change(cd).check(ChangePermission.READ);
-            visible = true;
-          } catch (AuthException e) {
-            // Do nothing.
-          }
-        }
-      }
+      boolean visible =
+          cd != null
+              && cd.projectStatePermitsRead()
+              && permissionBackend.user(user).change(cd).test(ChangePermission.READ);
 
       ChangeSet changeSet = new ChangeSet(cd, visible);
       if (wholeTopicEnabled(cfg) || includingTopicClosure) {
         return completeChangeSetIncludingTopics(changeSet, user, backfill);
       }
-      try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
+      try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation);
+          TraceTimer timer2 =
+              TraceContext.newTimer(
+                  "MergeSuperSet#completeWithoutTopic",
+                  Metadata.builder().changeId(change.getId().get()).build())) {
         return mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user, backfill);
       }
     } finally {
@@ -176,60 +177,67 @@
   private ChangeSet topicClosure(
       ChangeSet changeSet, CurrentUser user, Set<String> topicsSeen, Set<String> visibleTopicsSeen)
       throws PermissionBackendException {
-    List<ChangeData> visibleChanges = new ArrayList<>();
-    List<ChangeData> nonVisibleChanges = new ArrayList<>();
+    try (TraceTimer timer = TraceContext.newTimer("MergeSuperSet#topicClosure", Metadata.empty())) {
+      List<ChangeData> visibleChanges = new ArrayList<>();
+      List<ChangeData> nonVisibleChanges = new ArrayList<>();
 
-    for (ChangeData cd : changeSet.changes()) {
-      visibleChanges.add(cd);
-      String topic = cd.change().getTopic();
-      if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
-        continue;
+      for (ChangeData cd : changeSet.changes()) {
+        visibleChanges.add(cd);
+        String topic = cd.change().getTopic();
+        if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
+          continue;
+        }
+        for (ChangeData topicCd : byTopicOpen(topic)) {
+          if (canRead(user, topicCd)) {
+            visibleChanges.add(topicCd);
+          } else {
+            nonVisibleChanges.add(topicCd);
+          }
+        }
+        topicsSeen.add(topic);
+        visibleTopicsSeen.add(topic);
       }
-      for (ChangeData topicCd : byTopicOpen(topic)) {
-        if (canRead(user, topicCd)) {
-          visibleChanges.add(topicCd);
-        } else {
+      for (ChangeData cd : changeSet.nonVisibleChanges()) {
+        nonVisibleChanges.add(cd);
+        String topic = cd.change().getTopic();
+        if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
+          continue;
+        }
+        for (ChangeData topicCd : byTopicOpen(topic)) {
           nonVisibleChanges.add(topicCd);
         }
+        topicsSeen.add(topic);
       }
-      topicsSeen.add(topic);
-      visibleTopicsSeen.add(topic);
+      return new ChangeSet(visibleChanges, nonVisibleChanges);
     }
-    for (ChangeData cd : changeSet.nonVisibleChanges()) {
-      nonVisibleChanges.add(cd);
-      String topic = cd.change().getTopic();
-      if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
-        continue;
-      }
-      for (ChangeData topicCd : byTopicOpen(topic)) {
-        nonVisibleChanges.add(topicCd);
-      }
-      topicsSeen.add(topic);
-    }
-    return new ChangeSet(visibleChanges, nonVisibleChanges);
   }
 
   private ChangeSet completeChangeSetIncludingTopics(
       ChangeSet changeSet, CurrentUser user, boolean backfill)
       throws IOException, PermissionBackendException {
-    Set<String> topicsSeen = new HashSet<>();
-    Set<String> visibleTopicsSeen = new HashSet<>();
-    int oldSeen;
-    int seen;
+    try (TraceTimer timer =
+        TraceContext.newTimer("MergeSuperSet#completeChangeSetIncludingTopics", Metadata.empty())) {
+      Set<String> topicsSeen = new HashSet<>();
+      Set<String> visibleTopicsSeen = new HashSet<>();
+      int oldSeen;
+      int seen;
 
-    changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
-    seen = topicsSeen.size() + visibleTopicsSeen.size();
-
-    do {
-      oldSeen = seen;
-      try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
-        changeSet =
-            mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user, backfill);
-      }
       changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
       seen = topicsSeen.size() + visibleTopicsSeen.size();
-    } while (seen != oldSeen);
-    return changeSet;
+
+      do {
+        oldSeen = seen;
+        try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation);
+            TraceTimer timer2 =
+                TraceContext.newTimer("MergeSuperSet#completeWithoutTopic", Metadata.empty())) {
+          changeSet =
+              mergeSuperSetComputation.get().completeWithoutTopic(orm, changeSet, user, backfill);
+        }
+        changeSet = topicClosure(changeSet, user, topicsSeen, visibleTopicsSeen);
+        seen = topicsSeen.size() + visibleTopicsSeen.size();
+      } while (seen != oldSeen);
+      return changeSet;
+    }
   }
 
   private List<ChangeData> byTopicOpen(String topic) {
@@ -240,11 +248,6 @@
     if (!projectCache.get(cd.project()).map(ProjectState::statePermitsRead).orElse(false)) {
       return false;
     }
-    try {
-      permissionBackend.user(user).change(cd).check(ChangePermission.READ);
-      return true;
-    } catch (AuthException e) {
-      return false;
-    }
+    return permissionBackend.user(user).change(cd).test(ChangePermission.READ);
   }
 }
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 4b0ee42..8642030 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -572,6 +572,7 @@
   public String formatCause(Throwable t) {
     while ((t instanceof UpdateException
             || t instanceof StorageException
+            || t instanceof RetryException
             || t instanceof ExecutionException)
         && t.getCause() != null) {
       t = t.getCause();
diff --git a/java/com/google/gerrit/server/update/RetryableAction.java b/java/com/google/gerrit/server/update/RetryableAction.java
index 1a713d2..37944ce 100644
--- a/java/com/google/gerrit/server/update/RetryableAction.java
+++ b/java/com/google/gerrit/server/update/RetryableAction.java
@@ -55,10 +55,10 @@
   public enum ActionType {
     ACCOUNT_UPDATE,
     CHANGE_UPDATE,
-    GIT_UPDATE,
     GROUP_UPDATE,
     INDEX_QUERY,
     PLUGIN_UPDATE,
+    REPO_CREATION,
     REST_READ_REQUEST,
     REST_WRITE_REQUEST,
     SEND_EMAIL,
diff --git a/java/com/google/gerrit/server/util/CommitMessageUtil.java b/java/com/google/gerrit/server/util/CommitMessageUtil.java
index dc9c2d9..6c031c6 100644
--- a/java/com/google/gerrit/server/util/CommitMessageUtil.java
+++ b/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -104,4 +104,19 @@
     }
     return Optional.empty();
   }
+
+  /**
+   * Drops all comment lines from the given commit message.
+   *
+   * @param commitMessage the commit message
+   * @return the commit message with comment lines removed, {@code null} if the input was {@code
+   *     null}
+   */
+  @Nullable
+  public static String dropComments(@Nullable String commitMessage) {
+    if (commitMessage == null) {
+      return null;
+    }
+    return commitMessage.replaceAll("(?m)^#.*$\n?", "");
+  }
 }
diff --git a/java/com/google/gerrit/server/util/MarkdownImagesUtil.java b/java/com/google/gerrit/server/util/MarkdownImagesUtil.java
new file mode 100644
index 0000000..0c62f05
--- /dev/null
+++ b/java/com/google/gerrit/server/util/MarkdownImagesUtil.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class MarkdownImagesUtil {
+  // This regex is adapted from the frontend's IMAGE_MIME_PATTERN.
+  // It matches base64-encoded images in Markdown format: ![alt-text](data:image/...).
+  // We use (?i) for case insensitivity and \s* to allow spaces.
+  public static final String BASE64_IMAGE_REGEX =
+      "!\\[[^\\]]*\\]\\s*\\(\\s*(?i)data:image/[^,]*;base64,[^)]*\\)";
+
+  private static final Pattern BASE64_IMAGE_PATTERN = Pattern.compile(BASE64_IMAGE_REGEX);
+
+  private static final String IMAGE_PLACEHOLDER = "![base64-image]";
+
+  private final boolean allowMarkdownBase64ImagesInComments;
+
+  @Inject
+  MarkdownImagesUtil(@GerritServerConfig Config serverConfig) {
+    this.allowMarkdownBase64ImagesInComments =
+        serverConfig.getBoolean("change", "allowMarkdownBase64ImagesInComments", false);
+  }
+
+  public String stripImages(String comment) {
+    if (!allowMarkdownBase64ImagesInComments || comment == null) {
+      return comment;
+    }
+    // Replace the image and its surrounding whitespace with a single space.
+    String combinedText = BASE64_IMAGE_PATTERN.matcher(comment).replaceAll(" ");
+    return combinedText;
+  }
+
+  public String replaceImagesWithPlaceholder(String comment) {
+    if (!allowMarkdownBase64ImagesInComments || comment == null) {
+      return comment;
+    }
+    return BASE64_IMAGE_PATTERN.matcher(comment).replaceAll(IMAGE_PLACEHOLDER);
+  }
+}
diff --git a/java/com/google/gerrit/server/util/PluginLogFile.java b/java/com/google/gerrit/server/util/PluginLogFile.java
index 345e1b3..664a69f 100644
--- a/java/com/google/gerrit/server/util/PluginLogFile.java
+++ b/java/com/google/gerrit/server/util/PluginLogFile.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.errorprone.annotations.InlineMe;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.config.LogConfig;
 import org.apache.log4j.Layout;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
@@ -26,26 +29,60 @@
   private final ServerInformation serverInfo;
   private final String logName;
   private final Layout layout;
+  private final Layout jsonLayout;
+  private final boolean textLogging;
+  private final boolean jsonLogging;
 
+  /** Kept for backwards compatibility until all plugins have been updated. */
+  @Deprecated
+  @InlineMe(replacement = "this(systemLog, serverInfo, logName, layout, null, true, false)")
   public PluginLogFile(
       SystemLog systemLog, ServerInformation serverInfo, String logName, Layout layout) {
+    this(systemLog, serverInfo, logName, layout, null, true, false);
+  }
+
+  public PluginLogFile(
+      SystemLog systemLog,
+      ServerInformation serverInfo,
+      String logName,
+      Layout layout,
+      @Nullable Layout jsonLayout,
+      LogConfig config) {
+    this(
+        systemLog,
+        serverInfo,
+        logName,
+        layout,
+        jsonLayout,
+        config.isTextLogging(),
+        config.isJsonLogging());
+  }
+
+  public PluginLogFile(
+      SystemLog systemLog,
+      ServerInformation serverInfo,
+      String logName,
+      Layout layout,
+      @Nullable Layout jsonLayout,
+      boolean textLogging,
+      boolean jsonLogging) {
     this.systemLog = systemLog;
     this.serverInfo = serverInfo;
     this.logName = logName;
     this.layout = layout;
+    this.jsonLayout = jsonLayout;
+    this.textLogging = textLogging;
+    this.jsonLogging = jsonLogging && jsonLayout != null;
   }
 
   @Override
   public void start() {
-    Logger logger = LogManager.getLogger(logName);
-    if (logger.getAppender(logName) == null) {
-      synchronized (systemLog) {
-        if (logger.getAppender(logName) == null) {
-          logger.addAppender(systemLog.createAsyncAppender(logName, layout, true, true));
-        }
-      }
+    if (textLogging) {
+      initLogger(logName, layout);
     }
-    logger.setAdditivity(false);
+    if (jsonLogging) {
+      initLogger(logName, ".json", jsonLayout);
+    }
   }
 
   @Override
@@ -59,4 +96,21 @@
       LogManager.getLogger(logName).removeAllAppenders();
     }
   }
+
+  private void initLogger(String logName, Layout layout) {
+    initLogger(logName, "", layout);
+  }
+
+  private void initLogger(String logName, String logFileExtension, Layout layout) {
+    Logger logger = LogManager.getLogger(logName);
+    String appenderName = logName + logFileExtension;
+    if (logger.getAppender(appenderName) == null) {
+      synchronized (systemLog) {
+        if (logger.getAppender(appenderName) == null) {
+          logger.addAppender(systemLog.createAsyncAppender(appenderName, layout, true, true));
+        }
+      }
+    }
+    logger.setAdditivity(false);
+  }
 }
diff --git a/java/com/google/gerrit/server/util/SystemLog.java b/java/com/google/gerrit/server/util/SystemLog.java
index ec2f386..89e4b58 100644
--- a/java/com/google/gerrit/server/util/SystemLog.java
+++ b/java/com/google/gerrit/server/util/SystemLog.java
@@ -20,6 +20,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -48,10 +49,10 @@
   private final boolean rotateLogs;
 
   @Inject
-  public SystemLog(SitePaths site, @GerritServerConfig Config config) {
+  public SystemLog(SitePaths site, @GerritServerConfig Config config, LogConfig logConfig) {
     this.site = site;
     this.asyncLoggingBufferSize = config.getInt("core", "asyncLoggingBufferSize", 64);
-    this.rotateLogs = config.getBoolean("log", "rotate", true);
+    this.rotateLogs = logConfig.shouldRotate();
   }
 
   public static boolean shouldConfigure() {
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 0a97549..fa04827 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -21,9 +21,7 @@
         "//java/com/google/gerrit/server/ioutil",
         "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
-        "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
-        "//java/com/google/gerrit/server/version",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/logging",
         "//lib:args4j",
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 10eaff3..f20acb8 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
@@ -320,9 +319,8 @@
   private boolean isAdminHighPriorityCommand() {
     if (getClass().getAnnotation(AdminHighPriorityCommand.class) != null) {
       try {
-        permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
-        return true;
-      } catch (AuthException | PermissionBackendException e) {
+        return permissionBackend.user(user).test(GlobalPermission.ADMINISTRATE_SERVER);
+      } catch (PermissionBackendException e) {
         return false;
       }
     }
diff --git a/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index 3e9dd08..50d5382 100644
--- a/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeResource;
@@ -87,9 +86,8 @@
     List<ChangeNotes> toAdd = new ArrayList<>(changes.size());
     boolean canMaintainServer;
     try {
-      permissionBackend.currentUser().check(GlobalPermission.MAINTAIN_SERVER);
-      canMaintainServer = true;
-    } catch (AuthException | PermissionBackendException e) {
+      canMaintainServer = permissionBackend.currentUser().test(GlobalPermission.MAINTAIN_SERVER);
+    } catch (PermissionBackendException e) {
       canMaintainServer = false;
     }
     for (ChangeNotes notes : matched) {
@@ -104,11 +102,8 @@
           continue;
         }
 
-        try {
-          permissionBackend.currentUser().change(notes).check(ChangePermission.READ);
+        if (permissionBackend.currentUser().change(notes).test(ChangePermission.READ)) {
           toAdd.add(notes);
-        } catch (AuthException e) {
-          // Do nothing.
         }
       }
     }
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 24178eb..c07c095 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -81,6 +81,7 @@
               Throwable error = null;
               try (RequestStateContext requestStateContext =
                   RequestStateContext.open()
+                      .setPerformanceSummaryProvider(performanceLogContext)
                       .addRequestStateProvider(
                           deadlineCheckerFactory.create(requestInfo, deadline))) {
                 requestListeners.runEach(l -> l.onRequest(requestInfo));
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 7c3f871..83db146 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
 import com.google.gerrit.server.config.GerritConfigListener;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.util.SystemLog;
@@ -79,14 +80,15 @@
       final Provider<Context> context,
       SystemLog systemLog,
       @GerritServerConfig Config config,
+      LogConfig logConfig,
       GroupAuditService auditService) {
     this.session = session;
     this.context = context;
     this.auditService = auditService;
     this.systemLog = systemLog;
 
-    this.json = config.getBoolean("log", "jsonLogging", false);
-    this.text = config.getBoolean("log", "textLogging", true) || !json;
+    this.json = logConfig.isJsonLogging();
+    this.text = logConfig.isTextLogging();
 
     if (config.getBoolean("sshd", "requestLog", true)) {
       enableLogging();
diff --git a/java/com/google/gerrit/sshd/SuExec.java b/java/com/google/gerrit/sshd/SuExec.java
index 3c6e8c2..7ba4093 100644
--- a/java/com/google/gerrit/sshd/SuExec.java
+++ b/java/com/google/gerrit/sshd/SuExec.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -145,7 +146,10 @@
     if (caller instanceof PeerDaemonUser) {
       caller = null;
     }
-    return new SshSession(session, peer, userFactory.runAs(peer, accountId, caller));
+    return new SshSession(
+        session,
+        peer,
+        userFactory.runAs(peer, accountId, caller, ImpersonationPermissionMode.THIS_USER));
   }
 
   private static String join(List<String> args) {
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 660b0df..6d2c0d1 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -16,15 +16,18 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.account.InvalidAuthTokenException;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.CreateAccount;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -65,6 +68,16 @@
       usage = "password for HTTP authentication")
   private String httpPassword;
 
+  @Option(name = "--token", metaVar = "TOKEN", usage = "token for HTTP authentication")
+  private String token;
+
+  @Option(
+      name = "--token-id",
+      depends = {"--token"},
+      metaVar = "TOKEN",
+      usage = "ID for the authentication token")
+  private String tokenId;
+
   @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "name of the user account")
   private String username;
 
@@ -80,12 +93,25 @@
     input.name = fullName;
     input.sshKey = readSshKey();
     input.httpPassword = httpPassword;
+
+    if (!Strings.isNullOrEmpty(token)) {
+      AuthTokenInput tokenInput = new AuthTokenInput();
+      if (!Strings.isNullOrEmpty(tokenId)) {
+        tokenInput.id = tokenId;
+      }
+      tokenInput.token = token;
+
+      input.tokens = List.of(tokenInput);
+    } else {
+      input.tokens = List.of();
+    }
+
     input.groups = Lists.transform(groups, AccountGroup.Id::toString);
     try {
       @SuppressWarnings("unused")
       var unused =
           createAccount.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(username), input);
-    } catch (RestApiException e) {
+    } catch (RestApiException | InvalidAuthTokenException e) {
       throw die(e.getMessage());
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 6f2381d..897bb20 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -23,6 +23,8 @@
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.common.HttpPasswordInput;
 import com.google.gerrit.extensions.common.Input;
@@ -43,10 +45,12 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.account.AddSshKey;
 import com.google.gerrit.server.restapi.account.CreateEmail;
+import com.google.gerrit.server.restapi.account.CreateToken;
 import com.google.gerrit.server.restapi.account.DeleteActive;
 import com.google.gerrit.server.restapi.account.DeleteEmail;
 import com.google.gerrit.server.restapi.account.DeleteExternalIds;
 import com.google.gerrit.server.restapi.account.DeleteSshKey;
+import com.google.gerrit.server.restapi.account.DeleteToken;
 import com.google.gerrit.server.restapi.account.GetEmails;
 import com.google.gerrit.server.restapi.account.GetSshKeys;
 import com.google.gerrit.server.restapi.account.PutActive;
@@ -61,6 +65,7 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -125,6 +130,21 @@
   @Option(name = "--generate-http-password", usage = "generate a new HTTP password for the account")
   private boolean generateHttpPassword;
 
+  @Option(name = "--token", metaVar = "TOKEN", usage = "token for HTTP authentication")
+  private List<String> tokens = new ArrayList<>();
+
+  @Option(
+      name = "--generate-token",
+      metaVar = "TOKENID",
+      usage = "generate a new token for the account")
+  private List<String> tokenIdsToGenerate = new ArrayList<>();
+
+  @Option(
+      name = "--delete-token",
+      metaVar = "TOKENID",
+      usage = "delete token with given ID for the account")
+  private List<String> tokenIdsToDelete = new ArrayList<>();
+
   @Option(
       name = "--delete-external-id",
       metaVar = "EXTERNALID",
@@ -147,6 +167,10 @@
 
   @Inject private PutHttpPassword putHttpPassword;
 
+  @Inject private CreateToken createToken;
+
+  @Inject private DeleteToken deleteToken;
+
   @Inject private PutActive putActive;
 
   @Inject private DeleteActive deleteActive;
@@ -165,6 +189,7 @@
 
   private AccountResource rsrc;
 
+  @SuppressWarnings("UnnecessaryAssignment")
   @Override
   public void run() throws Exception {
     enableGracefulStop();
@@ -231,7 +256,6 @@
   }
 
   private void setAccount() throws Failure {
-    user = genericUserFactory.create(id);
     rsrc = new AccountResource(user.asIdentifiedUser());
     try {
       for (String email : addEmails) {
@@ -265,6 +289,25 @@
         }
       }
 
+      for (String token : tokens) {
+        AuthTokenInput tokenInput = new AuthTokenInput();
+        tokenInput.id = String.format("token_%d", Instant.now().toEpochMilli());
+        tokenInput.token = token;
+        createToken.apply(rsrc, IdString.fromDecoded(tokenInput.id), tokenInput);
+      }
+
+      for (String tokenId : tokenIdsToGenerate) {
+        AuthTokenInput tokenInput = new AuthTokenInput();
+        tokenInput.id = tokenId;
+        Response<AuthTokenInfo> resp =
+            createToken.apply(rsrc, IdString.fromDecoded(tokenInput.id), tokenInput);
+        stdout.print(String.format("New token (id = %s): %s", resp.value().id, resp.value().token));
+      }
+
+      for (String tokenId : tokenIdsToDelete) {
+        deleteToken.apply(rsrc.getUser(), tokenId, true);
+      }
+
       if (active) {
         @SuppressWarnings("unused")
         var unused = putActive.apply(rsrc, null);
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index fc01f15..7a958b7 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.common.CacheInfo;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.cache.CacheDisplay;
 import com.google.gerrit.server.config.ConfigResource;
@@ -129,9 +128,8 @@
 
       boolean showJvm;
       try {
-        permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER);
-        showJvm = true;
-      } catch (AuthException | PermissionBackendException e) {
+        showJvm = permissionBackend.user(self).test(GlobalPermission.MAINTAIN_SERVER);
+      } catch (PermissionBackendException e) {
         // Silently ignore and do not display detailed JVM information.
         showJvm = false;
       }
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 14915bf..1c44776 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -58,6 +58,12 @@
   private boolean wide;
 
   @Option(
+      name = "--queue",
+      aliases = {"-Q"},
+      usage = "only show tasks from the specified queue")
+  private String queueName;
+
+  @Option(
       name = "--by-queue",
       aliases = {"-q"},
       usage = "group tasks by queue and print queue info")
@@ -108,6 +114,10 @@
 
     boolean viewAll = permissionBackend.user(currentUser).testOrFalse(GlobalPermission.VIEW_QUEUE);
     long now = TimeUtil.nowMs();
+    if (queueName != null && !queueName.isEmpty()) {
+      tasks = tasks.stream().filter(t -> queueName.equalsIgnoreCase(t.queueName)).toList();
+    }
+
     if (groupByQueue) {
       ListMultimap<String, TaskInfo> byQueue = byQueue(tasks);
       for (String queueName : byQueue.keySet()) {
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 8bcda6f..e47aa6d 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.change.ArchiveFormatInternal;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -263,10 +262,9 @@
       return false;
     }
 
-    try {
-      permissionBackend.user(user).project(projectName).check(ProjectPermission.READ);
+    if (permissionBackend.user(user).project(projectName).test(ProjectPermission.READ)) {
       return true;
-    } catch (AuthException e) {
+    } else {
       // Check reachability of the specific revision.
       try (RevWalk rw = new RevWalk(repo)) {
         RevCommit commit = rw.parseCommit(revId);
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 33f8668..f0f2df2 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -46,8 +46,8 @@
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:test-ref-update-context",
-        "//javatests/com/google/gerrit/util/http/testutil",
         "//lib:guava",
+        "//lib:guava-retrying",
         "//lib:h2",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index 330b602..92ca7e6 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -20,10 +20,10 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.util.time.TimeUtil;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Fake implementation of {@link AccountCache} for testing. */
@@ -53,7 +53,8 @@
   }
 
   @Override
-  public synchronized Map<Account.Id, AccountState> get(Set<Account.Id> accountIds) {
+  public synchronized ImmutableMap<Account.Id, AccountState> get(
+      Collection<Account.Id> accountIds) {
     return ImmutableMap.copyOf(Maps.filterKeys(byId, accountIds::contains));
   }
 
diff --git a/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java b/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java
index 0c612f0..bfc56f9 100644
--- a/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java
@@ -135,7 +135,8 @@
       int matchedPsNumber = -1;
       Optional<PatchSetWithReviewedFiles> result = Optional.empty();
       for (Entity entity : store) {
-        if (entity.accountId() != accountId || !entity.psId().changeId().equals(psId.changeId())) {
+        if (!entity.accountId().equals(accountId)
+            || !entity.psId().changeId().equals(psId.changeId())) {
           continue;
         }
         int entityPsNumber = Integer.parseInt(entity.psId().getId());
diff --git a/java/com/google/gerrit/testing/GitRepositoryReferenceCountingManager.java b/java/com/google/gerrit/testing/GitRepositoryReferenceCountingManager.java
index 63edac8..90d2ae5 100644
--- a/java/com/google/gerrit/testing/GitRepositoryReferenceCountingManager.java
+++ b/java/com/google/gerrit/testing/GitRepositoryReferenceCountingManager.java
@@ -16,7 +16,11 @@
 
 import static org.junit.Assert.fail;
 
+import com.github.rholder.retry.RetryException;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicates;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
@@ -33,12 +37,15 @@
 import java.util.NavigableSet;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.runner.Description;
 
 public class GitRepositoryReferenceCountingManager implements GitRepositoryManager {
+  private static final int TIMEOUT_WAITING_FOR_CLOSED_REPOSITORIES_SEC = 30;
   private final GitRepositoryManager delegate;
   private Set<RepositoryTracking> openRepositories;
   private final AllUsersName allUsersName;
@@ -196,12 +203,7 @@
   }
 
   public void assertThatAllRepositoriesAreClosed(String testName) {
-    List<String> repositoriesToReport =
-        MoreObjects.<Set<RepositoryTracking>>firstNonNull(openRepositories, Collections.emptySet())
-            .stream()
-            .map(RepositoryTracking::reportIfOpen)
-            .flatMap(Optional::stream)
-            .toList();
+    List<String> repositoriesToReport = waitUntilAllRepositoriesAreClosed();
     if (!repositoriesToReport.isEmpty()) {
       fail(
           "All repositories were expected to be closed at the end of the following test:\n"
@@ -222,6 +224,29 @@
     }
   }
 
+  private List<String> waitUntilAllRepositoriesAreClosed() {
+    try {
+      return RetryerBuilder.<List<String>>newBuilder()
+          .retryIfResult(Predicates.not(List::isEmpty))
+          .withStopStrategy(
+              StopStrategies.stopAfterDelay(
+                  TIMEOUT_WAITING_FOR_CLOSED_REPOSITORIES_SEC, TimeUnit.SECONDS))
+          .build()
+          .call(this::getOpenRepositoriesToReport);
+    } catch (ExecutionException | RetryException e) {
+      return getOpenRepositoriesToReport();
+    }
+  }
+
+  private List<String> getOpenRepositoriesToReport() {
+    return MoreObjects.<Set<RepositoryTracking>>firstNonNull(
+            openRepositories, Collections.emptySet())
+        .stream()
+        .map(RepositoryTracking::reportIfOpen)
+        .flatMap(Optional::stream)
+        .toList();
+  }
+
   @Override
   public Status getRepositoryStatus(Project.NameKey name) {
     return delegate.getRepositoryStatus(name);
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 893a4d5..657e1f2 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
 import com.google.gerrit.auth.AuthModule;
 import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -48,6 +49,8 @@
 import com.google.gerrit.server.Sequence;
 import com.google.gerrit.server.Sequence.LightweightGroups;
 import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.AuthTokenModule;
+import com.google.gerrit.server.account.CachingAuthTokenModule;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl;
 import com.google.gerrit.server.account.storage.notedb.AccountNoteDbReadStorageModule;
@@ -235,6 +238,15 @@
     install(new SuperprojectUpdateSubmissionListenerModule());
     install(new WorkQueueModule());
 
+    boolean useAuthTokenCache =
+        authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP
+            || authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.HTTP_LDAP;
+    if (useAuthTokenCache) {
+      install(new CachingAuthTokenModule());
+    } else {
+      install(new AuthTokenModule());
+    }
+
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
 
     // It would be nice to use Jimfs for the SitePath, but the biggest blocker is that JGit does not
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
index 7db26e3..c17ec12 100644
--- a/java/com/google/gerrit/testing/TestCommentHelper.java
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -17,12 +17,10 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.Side;
@@ -33,7 +31,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 
 /** Test helper for dealing with comments/drafts. */
@@ -124,26 +121,6 @@
     return range;
   }
 
-  public static RobotCommentInput createRobotCommentInputWithMandatoryFields(String path) {
-    RobotCommentInput in = new RobotCommentInput();
-    in.robotId = "happyRobot";
-    in.robotRunId = "1";
-    in.message = "nit: trailing whitespace";
-    in.path = path;
-    return in;
-  }
-
-  public static RobotCommentInput createRobotCommentInput(
-      String path, FixSuggestionInfo... fixSuggestionInfos) {
-    RobotCommentInput in = TestCommentHelper.createRobotCommentInputWithMandatoryFields(path);
-    in.url = "http://www.happy-robot.com";
-    in.properties = new HashMap<>();
-    in.properties.put("key1", "value1");
-    in.properties.put("key2", "value2");
-    in.fixSuggestions = Arrays.asList(fixSuggestionInfos);
-    return in;
-  }
-
   public static CommentInput createCommentInputWithMandatoryFields(String path) {
     CommentInput in = new CommentInput();
     in.message = "nit: trailing whitespace";
@@ -160,38 +137,6 @@
     return in;
   }
 
-  public void addRobotComment(String targetChangeId, RobotCommentInput robotCommentInput)
-      throws Exception {
-    addRobotComment(targetChangeId, robotCommentInput, "robot comment test");
-  }
-
-  public void addRobotComment(Change.Id targetChangeId, RobotCommentInput robotCommentInput)
-      throws Exception {
-    addRobotComment(targetChangeId, robotCommentInput, "robot comment test");
-  }
-
-  public void addRobotComment(
-      String targetChangeId, RobotCommentInput robotCommentInput, String message) throws Exception {
-    ReviewInput reviewInput = createReviewInput(robotCommentInput, message);
-    gApi.changes().id(targetChangeId).current().review(reviewInput);
-  }
-
-  public void addRobotComment(
-      Change.Id targetChangeId, RobotCommentInput robotCommentInput, String message)
-      throws Exception {
-    ReviewInput reviewInput = createReviewInput(robotCommentInput, message);
-    gApi.changes().id(targetChangeId.get()).current().review(reviewInput);
-  }
-
-  private ReviewInput createReviewInput(RobotCommentInput robotCommentInput, String message) {
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.robotComments =
-        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
-    reviewInput.message = message;
-    reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
-    return reviewInput;
-  }
-
   public void addComment(String targetChangeId, CommentInput commentInput) throws Exception {
     addComment(targetChangeId, commentInput, "comment test");
   }
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index da8d5d9..2ba06ba 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -438,7 +438,7 @@
 
     private Map<String, QueuedOption> queuedOptionsByName = new LinkedHashMap<>();
 
-    private class QueuedOption {
+    private static class QueuedOption {
       public final Option option;
 
       @SuppressWarnings("rawtypes")
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
index b0817fe..bcf401e 100644
--- a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -24,6 +24,8 @@
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.testing.FakeEmailSender;
 import java.net.URI;
@@ -95,12 +97,15 @@
   }
 
   @Test
-  public void messageIdHeaderFromPasswordUpdate() throws Exception {
+  public void messageIdHeaderFromAuthTokenUpdate() throws Exception {
     sender.clear();
-    String newPassword = gApi.accounts().self().generateHttpPassword();
-    assertThat(newPassword).isNotNull();
+    AuthTokenInput token = new AuthTokenInput();
+    token.id = "testToken";
+    AuthTokenInfo tokenInfo = gApi.accounts().self().createToken(token);
+    assertThat(tokenInfo).isNotNull();
+    assertThat(tokenInfo.token).isNotNull();
     assertThat(getMessageId(sender))
-        .containsMatch("<HTTP_password_change-" + admin.id().toString() + ".*@.*>");
+        .containsMatch("<Auth_token_change-" + admin.id().toString() + ".*@.*>");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index e85a032..e11d856 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -290,6 +290,24 @@
     verifyNoMoreInteractions(cache, indexer, includeCache);
   }
 
+  @Test
+  public void newCreateProjectsAreDeleted() throws Exception {
+    Project.NameKey project2 = Project.nameKey("bar");
+
+    ProjectCache projectCache = mock(ProjectCache.class);
+
+    try (ProjectResetter resetProject =
+        builder(null, null, null, null, null, null, projectCache)
+            .build(new ProjectResetter.Config.Builder().deleteNewProjects().build())) {
+      Repository repo2 = repoManager.createRepository(project2);
+      createRef("refs/heads/master");
+      createRef(repo2, RefNames.REFS_CONFIG);
+    }
+
+    assertThat(repoManager.list()).doesNotContain(project2);
+    verify(projectCache, only()).evictAndReindex(project2);
+  }
+
   @CanIgnoreReturnValue
   private Ref createRef(String ref) throws IOException {
     return createRef(repo, ref);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 3e2a15e..4272a88 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -424,7 +424,7 @@
       accountIndexedCounter.assertReindexOf(accountId, 1);
       assertThat(getExternalIdsReader().byAccount(accountId))
           .containsExactly(
-              getExternalIdFactory().createUsername(input.username, accountId, null),
+              getExternalIdFactory().createUsername(input.username, accountId),
               getExternalIdFactory().createEmail(accountId, input.email));
     }
   }
@@ -1156,6 +1156,28 @@
   }
 
   @Test
+  public void getEmailsOfOtherAccountIncludesEmailsFromNonMailToExternalIds() throws Exception {
+    String email = "preferred4@example.com";
+    TestAccount foo = accountCreator.create(name("foo"), email, "Foo", null);
+
+    String secondaryEmail = "secondary4@example.com";
+    accountsUpdateProvider
+        .get()
+        .update(
+            "Add External ID",
+            foo.id(),
+            u ->
+                u.addExternalId(
+                    getExternalIdFactory().createWithEmail("x", "1", foo.id(), secondaryEmail)));
+
+    assertThat(
+            gApi.accounts().id(foo.id().get()).getEmails().stream()
+                .map(e -> e.email)
+                .collect(toSet()))
+        .containsExactly(email, secondaryEmail);
+  }
+
+  @Test
   public void addEmail() throws Exception {
     AccountIndexedCounter accountIndexedCounter = getAccountIndexedCounter();
     try (Registration registration =
@@ -1974,8 +1996,7 @@
   @Test
   public void addMalformedGpgKey() throws Exception {
     String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\ntest\n-----END PGP PUBLIC KEY BLOCK-----";
-    BadRequestException thrown = assertThrows(BadRequestException.class, () -> addGpgKey(key));
-    assertThat(thrown).hasMessageThat().contains("Failed to parse GPG keys");
+    BadRequestException unused = assertThrows(BadRequestException.class, () -> addGpgKey(key));
   }
 
   @Test
@@ -2790,7 +2811,8 @@
     String newPassword = gApi.accounts().self().generateHttpPassword();
     assertThat(newPassword).isNotNull();
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
+    assertThat(sender.getMessages().get(0).body())
+        .contains("The authentication token with id \"default\" was added");
   }
 
   @Test
@@ -2800,7 +2822,8 @@
     String newPassword = gApi.accounts().id(user.id().get()).generateHttpPassword();
     assertThat(newPassword).isNotNull();
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
+    assertThat(sender.getMessages().get(0).body())
+        .contains("The authentication token with id \"default\" was added");
   }
 
   @Test
@@ -2849,7 +2872,8 @@
     assertThat(gApi.accounts().id(user.id().get()).setHttpPassword(httpPassword))
         .isEqualTo(httpPassword);
     assertThat(sender.getMessages()).hasSize(1);
-    assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
+    assertThat(sender.getMessages().get(0).body())
+        .contains("The authentication token with id \"default\" was added");
   }
 
   @Test
@@ -3542,6 +3566,36 @@
     accountCreator.evict(deleted.id());
   }
 
+  @Test
+  public void deleteAccount_removeUserFromGroups() throws Exception {
+    TestAccount userAccount = accountCreator.create("user-remove-user-from-groups");
+    TestAccount userAccount2 = accountCreator.create("user-2");
+    AccountGroup.UUID engineers1Group =
+        groupOperations
+            .newGroup()
+            .addMember(userAccount.id())
+            .addMember(userAccount2.id())
+            .create();
+    AccountGroup.UUID engineers2Group =
+        groupOperations.newGroup().addMember(userAccount.id()).create();
+
+    assertThat(groupOperations.group(engineers1Group).get().members())
+        .containsExactly(userAccount.id(), userAccount2.id());
+    assertThat(groupOperations.group(engineers2Group).get().members())
+        .containsExactly(userAccount.id());
+
+    requestScopeOperations.setApiUser(userAccount.id());
+    gApi.accounts().self().delete();
+
+    requestScopeOperations.setApiUser(admin.id());
+    assertThat(groupOperations.group(engineers1Group).get().members())
+        .containsExactly(userAccount2.id());
+    assertThat(groupOperations.group(engineers2Group).get().members()).isEmpty();
+
+    // Clean up the test framework
+    accountCreator.evict(userAccount.id());
+  }
+
   @UsedAt(UsedAt.Project.GOOGLE)
   protected int getUsersWithDraftsCount(Change.Id changeId) throws Exception {
     // The getStarredChangesCount and getUsersWithDraftsCount should be 2 distinct methods,
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 59a0e76..472d1c2 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.entities.RefNames.REFS_EXTERNAL_IDS;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GOOGLE_OAUTH;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 import static java.util.stream.Collectors.toSet;
@@ -477,9 +478,8 @@
     AuthRequest who = authRequestFactory.createForEmail(email);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains("Email 'foo@example.com' in use by another account");
+    assertThrownContains(
+        thrown, "Email 'foo@example.com' in use by another account", accountId.toString());
   }
 
   @Test
@@ -503,9 +503,8 @@
     who.setEmailAddress(email);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains("Email 'foo@example.com' in use by another account");
+    assertThrownContains(
+        thrown, "Email 'foo@example.com' in use by another account", accountId.toString());
   }
 
   @Test
@@ -542,9 +541,8 @@
     who.setEmailAddress(newEmail);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
-    assertThat(thrown)
-        .hasMessageThat()
-        .isEqualTo("Email 'bar@example.com' in use by another account");
+    assertThrownContains(
+        thrown, "Email 'bar@example.com' in use by another account", accountId2.toString());
 
     // Verify that the email in the external ID was not updated.
     Optional<ExternalId> gerritExtId = externalIds.get(gerritExtIdKey);
@@ -588,7 +586,12 @@
     AccountState account = accounts.get(authResult.getAccountId()).get();
     ImmutableSet<ExternalId> accountExternalIds = account.externalIds();
     assertThat(accountExternalIds).isNotEmpty();
-    Set<String> emails = ExternalId.getEmails(accountExternalIds).collect(toSet());
+    Set<String> emails =
+        accountExternalIds.stream()
+            .filter(e -> e.isScheme(SCHEME_MAILTO))
+            .map(e -> e.key().id())
+            .distinct()
+            .collect(toSet());
     assertThat(emails).contains(email);
 
     // Verify the preferred email
@@ -786,9 +789,8 @@
     AuthRequest who = authRequestFactory.createForExternalUser(username1);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.link(accountId2, who));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains("Identity 'external:foo' in use by another account");
+    assertThrownContains(
+        thrown, "Identity 'external:foo' in use by another account", accountId1.toString());
   }
 
   @Test
@@ -821,9 +823,8 @@
     AuthRequest who = authRequestFactory.createForEmail(email);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.link(accountId2, who));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains("Email 'foo@example.com' in use by another account");
+    assertThrownContains(
+        thrown, "Email 'foo@example.com' in use by another account", accountId.toString());
   }
 
   @Test
@@ -927,4 +928,12 @@
     assertThat(authResult.getExternalId()).isEqualTo(expectedExtIdKey);
     assertThat(authResult.isNew()).isFalse();
   }
+
+  private void assertThrownContains(Throwable thrown, String... expectedMessages) {
+    String message = thrown.getMessage();
+    assertThat(message).isNotNull();
+    for (String expected : expectedMessages) {
+      assertThat(message).contains(expected);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index f462614..2803603 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.entities.AccountGroup;
-import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.ContributorAgreement;
 import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.InternalGroup;
@@ -53,8 +52,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import java.util.List;
@@ -71,13 +68,7 @@
   @Inject private RequestScopeOperations requestScopeOperations;
 
   protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = projectConfigFactory.read(md);
-      config.updateProject(
-          p -> p.setBooleanConfig(BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS, value));
-      config.commit(md);
-      projectCache.evictAndReindex(config.getProject());
-    }
+    projectOperations.project(project).forUpdate().useContributorAgreements(value).update();
   }
 
   protected ContributorAgreement configureContributorAgreement(boolean autoVerify)
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index db038d1..20d42fb 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -57,13 +57,13 @@
     assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable");
     assertThat(o.my)
         .containsExactly(
-            new MenuItem("Dashboard", "#/dashboard/self", null),
-            new MenuItem("Draft Comments", "#/q/has:draft", null),
-            new MenuItem("Edits", "#/q/has:edit", null),
-            new MenuItem("Watched Changes", "#/q/is:watched+is:open", null),
-            new MenuItem("Starred Changes", "#/q/is:starred", null),
-            new MenuItem("All Visible Changes", "#/q/is:visible", null),
-            new MenuItem("Groups", "#/settings/#Groups", null));
+            new MenuItem("Dashboard", "/dashboard/self", null),
+            new MenuItem("Draft Comments", "/q/has:draft", null),
+            new MenuItem("Edits", "/q/has:edit", null),
+            new MenuItem("Watched Changes", "/q/is:watched+is:open", null),
+            new MenuItem("Starred Changes", "/q/is:starred", null),
+            new MenuItem("All Visible Changes", "/q/is:visible", null),
+            new MenuItem("Groups", "/settings/#Groups", null));
     assertThat(o.changeTable).isEmpty();
 
     GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index 264fa09..8e5c0d9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -114,7 +115,7 @@
     Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
 
     // Create change
-    Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+    Change.Id change = changeOperations.newChange().project(project).owner(testUser).createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -268,6 +269,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void applyGerritBasedPatchWithMultipleFiles_success() throws Exception {
     PushOneCommit.Result commonBaseCommit =
         createChange("File for modification", MODIFIED_FILE_NAME, MODIFIED_FILE_ORIGINAL_CONTENT);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index bfac9ef..0eff580 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -91,17 +91,16 @@
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Address;
-import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
@@ -115,6 +114,7 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -131,9 +131,9 @@
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewerResult;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -187,6 +187,7 @@
 import com.google.gerrit.server.query.change.ChangeNumberVirtualIdAlgorithm;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.gerrit.server.rules.PrologSubmitRuleUtil;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -203,6 +204,7 @@
 import java.sql.Timestamp;
 import java.text.MessageFormat;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -240,7 +242,6 @@
 @UseTimezone(timezone = "US/Eastern")
 @VerifyNoPiiInChangeNotes(true)
 public class ChangeIT extends AbstractDaemonTest {
-
   @Inject private AccountOperations accountOperations;
   @Inject private ChangeIndexCollection changeIndexCollection;
   @Inject private GroupOperations groupOperations;
@@ -261,107 +262,122 @@
   private Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
 
   @Inject private ChangeNumberVirtualIdAlgorithm changeNumberVirtualIdAlgorithm;
+  @Inject private PrologSubmitRuleUtil prologSubmitRuleUtil;
 
   @Test
-  @GerritConfig(
-      name = "experiments.enabled",
-      value = "GerritBackendFeature__return_new_change_info_id")
   public void get() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String id = project.get() + "~" + r.getChange().getId().get();
-    ChangeInfo c = info(id);
-    assertThat(c.id).isEqualTo(id);
-    assertThat(c.project).isEqualTo(project.get());
-    assertThat(c.branch).isEqualTo("master");
-    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(c.subject).isEqualTo("test commit");
-    assertThat(c.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
-    assertThat(c.mergeable).isNull();
-    assertThat(c.changeId).isEqualTo(r.getChangeId());
-    assertThat(c.created).isEqualTo(c.updated);
-    assertThat(c._number).isEqualTo(r.getChange().getId().get());
-    assertThat(c.currentRevisionNumber).isEqualTo(r.getPatchSetId().get());
+    TestChange change = changeOperations.newChange().createAndGet();
 
-    assertThat(c.owner._accountId).isEqualTo(admin.id().get());
-    assertThat(c.owner.name).isNull();
-    assertThat(c.owner.email).isNull();
-    assertThat(c.owner.username).isNull();
-    assertThat(c.owner.avatars).isNull();
-    assertThat(c.submissionId).isNull();
+    ChangeInfo changeInfo = gApi.changes().id(change.id()).info();
+
+    assertThat(changeInfo.id).isEqualTo(change.project() + "~" + change.numericChangeId());
+    assertThat(changeInfo.project).isEqualTo(change.project().get());
+    assertThat(changeInfo.branch).isEqualTo(change.dest().shortName());
+    assertThat(changeInfo.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(changeInfo.subject).isEqualTo(change.subject());
+    assertThat(changeInfo.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(changeInfo.mergeable).isNull();
+    assertThat(changeInfo.changeId).isEqualTo(change.changeId());
+    assertThat(changeInfo._number).isEqualTo(change.numericChangeId().get());
+    assertThat(changeInfo.currentRevisionNumber)
+        .isEqualTo(changeOperations.change(change.id()).currentPatchset().get().patchsetId().get());
+
+    // With NoteDb timestamps are rounded to seconds.
+    assertThat(changeInfo.created)
+        .isEqualTo(Timestamp.from(change.createdOn().truncatedTo(ChronoUnit.SECONDS)));
+    assertThat(changeInfo.created).isEqualTo(changeInfo.updated);
+
+    assertThat(changeInfo.owner._accountId).isEqualTo(change.owner().get());
+    assertThat(changeInfo.owner.name).isNull();
+    assertThat(changeInfo.owner.email).isNull();
+    assertThat(changeInfo.owner.username).isNull();
+    assertThat(changeInfo.owner.avatars).isNull();
+    assertThat(changeInfo.owner.deleted).isNull();
+
+    assertThat(changeInfo.submissionId).isNull();
   }
 
   @Test
   public void cannotGetInvisibleChange() throws Exception {
-    PushOneCommit.Result r = createChange();
+    TestChange change = changeOperations.newChange().createAndGet();
 
     // Remove read access
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
         .add(block(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
         .update();
 
     requestScopeOperations.setApiUser(user.id());
     ResourceNotFoundException thrown =
-        assertThrows(
-            ResourceNotFoundException.class,
-            () -> gApi.changes().id(project.get(), r.getChange().getId().get()).get());
+        assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(change.id()).get());
     assertThat(thrown)
         .hasMessageThat()
-        .isEqualTo(String.format("Not found: %s~%d", project.get(), r.getChange().getId().get()));
+        .isEqualTo(
+            String.format(
+                "Not found: %s~%d", change.project().get(), change.numericChangeId().get()));
   }
 
   @Test
   public void adminCanGetChangeWithoutExplicitReadPermission() throws Exception {
-    PushOneCommit.Result r = createChange();
+    TestChange change = changeOperations.newChange().createAndGet();
 
     // Remove read access
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
         .add(block(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
         .update();
 
     requestScopeOperations.setApiUser(admin.id());
-    ChangeInfo changeInfo = gApi.changes().id(project.get(), r.getChange().getId().get()).get();
+    ChangeInfo changeInfo = gApi.changes().id(change.id()).get();
     assertThat(changeInfo.id)
-        .isEqualTo(String.format("%s~%d", project.get(), r.getChange().getId().get()));
+        .isEqualTo(String.format("%s~%d", change.project().get(), change.numericChangeId().get()));
   }
 
   @Test
   public void diffStatShouldComputeInsertionsAndDeletions() throws Exception {
-    String fileName = "a_new_file.txt";
-    String fileContent = "First line\nSecond line\n";
-    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
-    String triplet = project.get() + "~master~" + result.getChangeId();
-    ChangeInfo change = gApi.changes().id(triplet).get();
-    assertThat(change.insertions).isNotNull();
-    assertThat(change.deletions).isNotNull();
+    ChangeIdentifier changeIdentifier =
+        changeOperations
+            .newChange()
+            .file("a_new_file.txt")
+            .content("First line\nSecond line\n")
+            .create();
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+    assertThat(changeInfo.insertions).isEqualTo(2);
+    assertThat(changeInfo.deletions).isEqualTo(0);
   }
 
   @Test
   public void diffStatShouldSkipInsertionsAndDeletions() throws Exception {
-    String fileName = "a_new_file.txt";
-    String fileContent = "First line\nSecond line\n";
-    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
-    String triplet = project.get() + "~master~" + result.getChangeId();
-    ChangeInfo change =
-        gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
-    assertThat(change.insertions).isNull();
-    assertThat(change.deletions).isNull();
+    ChangeIdentifier changeIdentifier =
+        changeOperations
+            .newChange()
+            .file("a_new_file.txt")
+            .content("First line\nSecond line\n")
+            .create();
+    ChangeInfo changeInfo =
+        gApi.changes().id(changeIdentifier).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
+    assertThat(changeInfo.insertions).isNull();
+    assertThat(changeInfo.deletions).isNull();
   }
 
   @Test
   public void skipDiffstatOptionAvoidsAllDiffComputations() throws Exception {
-    String fileName = "a_new_file.txt";
-    String fileContent = "First line\nSecond line\n";
-    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
-    String triplet = project.get() + "~master~" + result.getChangeId();
+    ChangeIdentifier changeIdentifier =
+        changeOperations
+            .newChange()
+            .file("a_new_file.txt")
+            .content("First line\nSecond line\n")
+            .create();
+
     CacheStats startIntra = cloneStats(intraCache.stats());
     CacheStats startSummary = cloneStats(diffSummaryCache.stats());
 
     @SuppressWarnings("unused")
-    var unused = gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
+    var unused =
+        gApi.changes().id(changeIdentifier).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
 
     assertThat(intraCache.stats()).since(startIntra).hasMissCount(0);
     assertThat(intraCache.stats()).since(startIntra).hasHitCount(0);
@@ -372,64 +388,63 @@
   @Test
   @GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER")
   public void excludeMergeableInChangeInfo() throws Exception {
-    PushOneCommit.Result r = createChange();
-    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+
+    ChangeInfo c = gApi.changes().id(changeIdentifier).get();
     assertThat(c.mergeable).isNull();
   }
 
   @Test
   public void getSubmissionId() throws Exception {
-    PushOneCommit.Result changeResult = createChange();
-    String changeId = changeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
 
-    merge(changeResult);
-    assertThat(gApi.changes().id(changeId).get().submissionId).isNotNull();
+    assertThat(gApi.changes().id(changeIdentifier).get().submissionId).isNull();
+
+    gApi.changes().id(changeIdentifier).current().review(ReviewInput.approve());
+    gApi.changes().id(changeIdentifier).current().submit();
+
+    assertThat(gApi.changes().id(changeIdentifier).get().submissionId).isNotNull();
   }
 
   @Test
   public void setWorkInProgressNotAllowedWithoutPermission() throws Exception {
-    PushOneCommit.Result rwip = createChange();
-    String changeId = rwip.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
 
     requestScopeOperations.setApiUser(user.id());
     AuthException thrown =
-        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setWorkInProgress());
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(changeIdentifier).setWorkInProgress());
     assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
   public void setWorkInProgressAllowedAsAdmin() throws Exception {
-    requestScopeOperations.setApiUser(user.id());
-    String changeId =
-        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
 
     requestScopeOperations.setApiUser(admin.id());
-    gApi.changes().id(changeId).setWorkInProgress();
-    assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
+    assertThat(gApi.changes().id(changeIdentifier).get().workInProgress).isTrue();
   }
 
   @Test
   public void setWorkInProgressAllowedAsProjectOwner() throws Exception {
-    requestScopeOperations.setApiUser(user.id());
-    String changeId =
-        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
+    TestChange change = changeOperations.newChange().owner(user.id()).createAndGet();
 
     com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
         .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
         .update();
     requestScopeOperations.setApiUser(user2.id());
-    gApi.changes().id(changeId).setWorkInProgress();
-    assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
+    gApi.changes().id(change.id()).setWorkInProgress();
+    assertThat(gApi.changes().id(change.id()).get().workInProgress).isTrue();
   }
 
   @Test
   public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
-    ConfigInput input = new ConfigInput();
-    input.workInProgressByDefault = InheritableBoolean.TRUE;
-    gApi.projects().name(project.get()).config(input);
+    projectOperations.project(project).forUpdate().workInProgressByDefault().update();
+
     String changeId =
         gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
     assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
@@ -437,54 +452,52 @@
 
   @Test
   public void setReadyForReviewNotAllowedWithoutPermission() throws Exception {
-    PushOneCommit.Result rready = createChange();
-    String changeId = rready.getChangeId();
-    gApi.changes().id(changeId).setWorkInProgress();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
 
     requestScopeOperations.setApiUser(user.id());
     AuthException thrown =
-        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setReadyForReview());
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(changeIdentifier).setReadyForReview());
     assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
   public void setReadyForReviewAllowedAsAdmin() throws Exception {
-    requestScopeOperations.setApiUser(user.id());
-    String changeId =
-        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
-    gApi.changes().id(changeId).setWorkInProgress();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
 
     requestScopeOperations.setApiUser(admin.id());
-    gApi.changes().id(changeId).setReadyForReview();
-    assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+    gApi.changes().id(changeIdentifier).setReadyForReview();
+    assertThat(gApi.changes().id(changeIdentifier).get().workInProgress).isNull();
   }
 
   @Test
   public void setReadyForReviewAllowedAsProjectOwner() throws Exception {
-    requestScopeOperations.setApiUser(user.id());
-    String changeId =
-        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
-    gApi.changes().id(changeId).setWorkInProgress();
+    TestChange change = changeOperations.newChange().owner(user.id()).createAndGet();
 
-    com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(change.id()).setWorkInProgress();
+
+    TestAccount user2 = accountCreator.user2();
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
         .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
         .update();
     requestScopeOperations.setApiUser(user2.id());
-    gApi.changes().id(changeId).setReadyForReview();
-    assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+    gApi.changes().id(change.id()).setReadyForReview();
+    assertThat(gApi.changes().id(change.id()).get().workInProgress).isNull();
   }
 
   @Test
   public void setReadyForReviewSendsNotificationsForRevertChange() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
-    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    gApi.changes().id(changeIdentifier).current().review(ReviewInput.approve());
+    gApi.changes().id(changeIdentifier).current().submit();
     RevertInput in = new RevertInput();
     in.workInProgress = true;
-    String changeId = gApi.changes().id(r.getChangeId()).revert(in).get().changeId;
+    String changeId = gApi.changes().id(changeIdentifier).revert(in).get().changeId;
     requestScopeOperations.setApiUser(admin.id());
 
     gApi.changes().id(changeId).setReadyForReview();
@@ -496,7 +509,7 @@
     // 3. Change has been successfully merged by Administrator
     // 4. Patch Set 1: Reverted
     List<ChangeMessageInfo> sourceMessages =
-        new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
+        new ArrayList<>(gApi.changes().id(changeIdentifier).get().messages);
     assertThat(sourceMessages).hasSize(4);
     String expectedMessage = String.format("Created a revert of this change as %s", changeId);
     assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
@@ -516,13 +529,12 @@
 
   @Test
   public void pendingReviewers() throws Exception {
-    ConfigInput conf = new ConfigInput();
-    conf.enableReviewerByEmail = InheritableBoolean.TRUE;
-    gApi.projects().name(project.get()).config(conf);
+    projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
 
-    PushOneCommit.Result r = createWorkInProgressChange();
-    String changeId = r.getChangeId();
-    assertThat(gApi.changes().id(changeId).get().pendingReviewers).isEmpty();
+    ChangeIdentifier changeIdentifier =
+        changeOperations.newChange().project(project).owner(admin.id()).create();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
+    assertThat(gApi.changes().id(changeIdentifier).get().pendingReviewers).isEmpty();
 
     // Add some pending reviewers.
     String email1 = name("user1") + "@example.com";
@@ -563,10 +575,10 @@
             .reviewer("byemail2@example.com")
             .reviewer("byemail3@example.com", CC, false)
             .reviewer("byemail4@example.com", CC, false);
-    ReviewResult result = gApi.changes().id(changeId).current().review(in);
+    ReviewResult result = gApi.changes().id(changeIdentifier).current().review(in);
     assertThat(result.changeInfo).isNotNull();
     assertThat(result.reviewers).isNotEmpty();
-    ChangeInfo info = gApi.changes().id(changeId).get();
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     Function<Collection<AccountInfo>, Collection<String>> toEmails =
         ais -> ais.stream().map(ai -> ai.email).collect(toSet());
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
@@ -576,11 +588,11 @@
     assertThat(info.pendingReviewers.get(REMOVED)).isNull();
 
     // Stage some pending reviewer removals.
-    gApi.changes().id(changeId).reviewer(email1).remove();
-    gApi.changes().id(changeId).reviewer(email3).remove();
-    gApi.changes().id(changeId).reviewer("byemail1@example.com").remove();
-    gApi.changes().id(changeId).reviewer("byemail3@example.com").remove();
-    info = gApi.changes().id(changeId).get();
+    gApi.changes().id(changeIdentifier).reviewer(email1).remove();
+    gApi.changes().id(changeIdentifier).reviewer(email3).remove();
+    gApi.changes().id(changeIdentifier).reviewer("byemail1@example.com").remove();
+    gApi.changes().id(changeIdentifier).reviewer("byemail3@example.com").remove();
+    info = gApi.changes().id(changeIdentifier).get();
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
         .containsExactly(email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
@@ -590,8 +602,8 @@
 
     // "Undo" a removal.
     in = ReviewInput.noScore().reviewer(email1);
-    gApi.changes().id(changeId).current().review(in);
-    info = gApi.changes().id(changeId).get();
+    gApi.changes().id(changeIdentifier).current().review(in);
+    info = gApi.changes().id(changeIdentifier).get();
     assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
         .containsExactly(email1, email2, "byemail2@example.com");
     assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
@@ -600,8 +612,8 @@
         .containsExactly(email3, "byemail1@example.com", "byemail3@example.com");
 
     // "Commit" by moving out of WIP.
-    gApi.changes().id(changeId).setReadyForReview();
-    info = gApi.changes().id(changeId).get();
+    gApi.changes().id(changeIdentifier).setReadyForReview();
+    info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.pendingReviewers).isEmpty();
     assertThat(toEmails.apply(info.reviewers.get(REVIEWER)))
         .containsExactly(email1, email2, "byemail2@example.com");
@@ -612,37 +624,36 @@
 
   @Test
   public void toggleWorkInProgressState() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String changeId = r.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
 
     // With message
-    gApi.changes().id(changeId).setWorkInProgress("Needs some refactoring");
+    gApi.changes().id(changeIdentifier).setWorkInProgress("Needs some refactoring");
 
-    ChangeInfo info = gApi.changes().id(changeId).get();
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
 
     assertThat(info.workInProgress).isTrue();
     assertThat(Iterables.getLast(info.messages).message).contains("Needs some refactoring");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
 
-    gApi.changes().id(changeId).setReadyForReview("PTAL");
+    gApi.changes().id(changeIdentifier).setReadyForReview("PTAL");
 
-    info = gApi.changes().id(changeId).get();
+    info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isNull();
     assertThat(Iterables.getLast(info.messages).message).contains("PTAL");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
 
     // No message
-    gApi.changes().id(changeId).setWorkInProgress();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
 
-    info = gApi.changes().id(changeId).get();
+    info = gApi.changes().id(changeIdentifier).get();
 
     assertThat(info.workInProgress).isTrue();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Work In Progress");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
 
-    gApi.changes().id(changeId).setReadyForReview();
+    gApi.changes().id(changeIdentifier).setReadyForReview();
 
-    info = gApi.changes().id(changeId).get();
+    info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isNull();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Ready For Review");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
@@ -650,13 +661,12 @@
 
   @Test
   public void toggleWorkInProgressStateByNonOwnerWithPermission() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String changeId = r.getChangeId();
+    TestChange change = changeOperations.newChange().createAndGet();
     String refactor = "Needs some refactoring";
     String ptal = "PTAL";
 
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
         .add(
             allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
@@ -665,17 +675,17 @@
         .update();
 
     requestScopeOperations.setApiUser(user.id());
-    gApi.changes().id(changeId).setWorkInProgress(refactor);
+    gApi.changes().id(change.id()).setWorkInProgress(refactor);
 
-    ChangeInfo info = gApi.changes().id(changeId).get();
+    ChangeInfo info = gApi.changes().id(change.id()).get();
 
     assertThat(info.workInProgress).isTrue();
     assertThat(Iterables.getLast(info.messages).message).contains(refactor);
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
 
-    gApi.changes().id(changeId).setReadyForReview(ptal);
+    gApi.changes().id(change.id()).setReadyForReview(ptal);
 
-    info = gApi.changes().id(changeId).get();
+    info = gApi.changes().id(change.id()).get();
     assertThat(info.workInProgress).isNull();
     assertThat(Iterables.getLast(info.messages).message).contains(ptal);
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
@@ -683,44 +693,41 @@
 
   @Test
   public void reviewAndStartReview() throws Exception {
-    PushOneCommit.Result r = createWorkInProgressChange();
-    r.assertOkStatus();
-    assertThat(r.getChange().change().isWorkInProgress()).isTrue();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
 
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(false);
-    ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
+    ReviewResult result = gApi.changes().id(changeIdentifier).current().review(in);
     assertThat(result.ready).isTrue();
 
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isNull();
   }
 
   @Test
   public void reviewAndMoveToWorkInProgress() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(r.getChange().change().isWorkInProgress()).isFalse();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
 
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
+    ReviewResult result = gApi.changes().id(changeIdentifier).current().review(in);
     assertThat(result.ready).isNull();
 
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isTrue();
   }
 
   @Test
   public void reviewAndSetWorkInProgressAndAddReviewerAndVote() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(r.getChange().change().isWorkInProgress()).isFalse();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
 
     ReviewInput in =
         ReviewInput.approve()
             .reviewer(user.email())
             .label(LabelId.CODE_REVIEW, 1)
             .setWorkInProgress(true);
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    gApi.changes().id(changeIdentifier).current().review(in);
 
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isTrue();
     assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
         .containsExactly(admin.id().get(), user.id().get());
@@ -730,24 +737,24 @@
 
   @Test
   public void reviewRemoveInactiveReviewer() throws Exception {
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
     ReviewInput in = ReviewInput.approve().reviewer(user.email());
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    gApi.changes().id(changeIdentifier).current().review(in);
 
     accountOperations.account(user.id()).forUpdate().inactive().update();
     in = ReviewInput.noScore().reviewer(Integer.toString(user.id().get()), REMOVED, false);
 
-    gApi.changes().id(r.getChangeId()).current().review(in);
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    gApi.changes().id(changeIdentifier).current().review(in);
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
         .containsExactly(admin.id().get());
   }
 
   @Test
   public void removeReviewerWithoutPermissionsOnChangePostReview_allowed() throws Exception {
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
     ReviewInput in = ReviewInput.approve().reviewer(user.email());
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    gApi.changes().id(changeIdentifier).current().review(in);
     AccountGroup.UUID restrictedGroup =
         groupOperations.newGroup().name("restricted-group").addMember(user.id()).create();
 
@@ -761,18 +768,17 @@
     in = ReviewInput.noScore().reviewer(Integer.toString(user.id().get()), REMOVED, false);
 
     requestScopeOperations.setApiUser(admin.id());
-    gApi.changes().id(r.getChangeId()).current().review(in);
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    gApi.changes().id(changeIdentifier).current().review(in);
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
         .containsExactly(admin.id().get());
   }
 
   @Test
   public void removeReviewerWithoutPermissionsOnChange_allowed() throws Exception {
-
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
     ReviewInput in = ReviewInput.approve().reviewer(user.email());
-    gApi.changes().id(r.getChangeId()).current().review(in);
+    gApi.changes().id(changeIdentifier).current().review(in);
     AccountGroup.UUID restrictedGroup =
         groupOperations.newGroup().name("restricted-group").addMember(user.id()).create();
 
@@ -784,69 +790,61 @@
         .update();
 
     requestScopeOperations.setApiUser(admin.id());
-    gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove();
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    gApi.changes().id(changeIdentifier).reviewer(user.id().toString()).remove();
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
         .containsExactly(admin.id().get());
   }
 
   @Test
   public void reviewWithWorkInProgressAndReadyReturnsError() throws Exception {
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
     ReviewInput in = ReviewInput.noScore();
     in.ready = true;
     in.workInProgress = true;
-    ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
+    ReviewResult result = gApi.changes().id(changeIdentifier).current().review(in);
     assertThat(result.error).isEqualTo(PostReview.ERROR_WIP_READY_MUTUALLY_EXCLUSIVE);
   }
 
   @Test
-  @TestProjectInput(cloneAs = "user1")
   public void reviewWithWorkInProgressChangeOwner() throws Exception {
-    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
 
     requestScopeOperations.setApiUser(user.id());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    gApi.changes().id(r.getChangeId()).current().review(in);
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    gApi.changes().id(changeIdentifier).current().review(in);
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isTrue();
   }
 
   @Test
-  @TestProjectInput(cloneAs = "user1")
   public void reviewWithWithWorkInProgressAdmin() throws Exception {
-    PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
 
     requestScopeOperations.setApiUser(admin.id());
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
-    gApi.changes().id(r.getChangeId()).current().review(in);
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    gApi.changes().id(changeIdentifier).current().review(in);
+    ChangeInfo info = gApi.changes().id(changeIdentifier).get();
     assertThat(info.workInProgress).isTrue();
   }
 
   @Test
   public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     requestScopeOperations.setApiUser(user.id());
     AuthException thrown =
         assertThrows(
-            AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
+            AuthException.class, () -> gApi.changes().id(changeIdentifier).current().review(in));
     assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
   public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception {
-    PushOneCommit.Result r = createChange();
+    TestChange change = changeOperations.newChange().owner(admin.id()).createAndGet();
     ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
         .add(
             allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
@@ -854,50 +852,49 @@
                 .group(REGISTERED_USERS))
         .update();
     requestScopeOperations.setApiUser(user.id());
-    gApi.changes().id(r.getChangeId()).current().review(in);
-    ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+    gApi.changes().id(change.id()).current().review(in);
+    ChangeInfo info = gApi.changes().id(change.id()).get();
     assertThat(info.workInProgress).isTrue();
   }
 
   @Test
   public void reviewWithReadyByNonOwnerReturnsError() throws Exception {
-    PushOneCommit.Result r = createChange();
-    change(r).setWorkInProgress();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
+    gApi.changes().id(changeIdentifier).setWorkInProgress();
 
     ReviewInput in = ReviewInput.noScore().setReady(true);
     requestScopeOperations.setApiUser(user.id());
     AuthException thrown =
         assertThrows(
-            AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
+            AuthException.class, () -> gApi.changes().id(changeIdentifier).current().review(in));
     assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
   }
 
   @Test
   public void getAmbiguous() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    String changeId = r1.getChangeId();
+    TestChange change =
+        changeOperations.newChange().project(project).branch("master").createAndGet();
 
     @SuppressWarnings("unused")
-    var unused = gApi.changes().id(changeId).get();
+    var unused = gApi.changes().id(change.id()).get();
 
     BranchInput b = new BranchInput();
     b.revision = repo().exactRef("HEAD").getObjectId().name();
     gApi.projects().name(project.get()).branch("other").create(b);
 
-    PushOneCommit push2 =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            PushOneCommit.SUBJECT,
-            PushOneCommit.FILE_NAME,
-            PushOneCommit.FILE_CONTENT,
-            changeId);
-    PushOneCommit.Result r2 = push2.to("refs/for/other");
-    assertThat(r2.getChangeId()).isEqualTo(changeId);
+    TestChange otherChange =
+        changeOperations
+            .newChange()
+            .project(project)
+            .branch("other")
+            .commitMessage("Summary line\n\nChange-Id: " + change.changeId())
+            .createAndGet();
+    assertThat(otherChange.changeId()).isEqualTo(change.changeId());
 
     ResourceNotFoundException thrown =
-        assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(changeId).get());
-    assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + changeId);
+        assertThrows(
+            ResourceNotFoundException.class, () -> gApi.changes().id(change.changeId()).get());
+    assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + change.changeId());
   }
 
   @Test
@@ -906,15 +903,12 @@
   }
 
   @Test
-  @TestProjectInput(cloneAs = "user1")
   public void deleteNewChangeAsNormalUser() throws Exception {
-    PushOneCommit.Result changeResult =
-        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-    String changeId = changeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
 
     requestScopeOperations.setApiUser(user.id());
     AuthException thrown =
-        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeIdentifier).delete());
     assertThat(thrown).hasMessageThat().contains("delete not permitted");
   }
 
@@ -925,7 +919,7 @@
         .forUpdate()
         .add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
         .update();
-    deleteChangeAsUser(admin, user);
+    deleteChangeAsUser(project, admin, user);
   }
 
   @Test
@@ -957,7 +951,7 @@
         .forUpdate()
         .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
         .update();
-    deleteChangeAsUser(user, user);
+    deleteChangeAsUser(project, user, user);
   }
 
   @Test
@@ -967,7 +961,7 @@
         .forUpdate()
         .add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(CHANGE_OWNER))
         .update();
-    deleteChangeAsUser(user, user);
+    deleteChangeAsUser(project, user, user);
   }
 
   private void deleteChangeAsUser(
@@ -988,26 +982,24 @@
           .forUpdate()
           .add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(ANONYMOUS_USERS))
           .update();
+      TestChange change =
+          changeOperations.newChange().project(projectName).owner(owner.id()).createAndGet();
       requestScopeOperations.setApiUser(owner.id());
-      ChangeInput in = new ChangeInput();
-      in.project = projectName.get();
-      in.branch = "refs/heads/master";
-      in.subject = "test";
-      ChangeInfo changeInfo = gApi.changes().create(in).get();
-      String changeId = changeInfo.changeId;
-      int id = changeInfo._number;
-      String commit = changeInfo.currentRevision;
 
-      assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id().get());
+      String commit =
+          changeOperations.change(change.id()).currentPatchset().get().commitId().getName();
+
+      assertThat(gApi.changes().id(change.id()).info().owner._accountId)
+          .isEqualTo(owner.id().get());
 
       requestScopeOperations.setApiUser(deleteAs.id());
-      gApi.changes().id(changeId).delete();
+      gApi.changes().id(change.id()).delete();
 
-      assertThat(query(changeId)).isEmpty();
+      assertThat(query(change.id().id())).isEmpty();
 
-      String ref = Change.id(id).toRefPrefix() + "1";
+      String ref = change.numericChangeId().toRefPrefix() + "1";
       eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
-      eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email());
+      eventRecorder.assertChangeDeletedEvents(change.changeId(), deleteAs.email());
     } finally {
       projectOperations
           .project(project)
@@ -1032,12 +1024,11 @@
         .update();
 
     try {
-      PushOneCommit.Result changeResult = createChange();
-      String changeId = changeResult.getChangeId();
+      ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
 
       requestScopeOperations.setApiUser(user.id());
       AuthException thrown =
-          assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
+          assertThrows(AuthException.class, () -> gApi.changes().id(changeIdentifier).delete());
       assertThat(thrown).hasMessageThat().contains("delete not permitted");
     } finally {
       projectOperations
@@ -1051,57 +1042,50 @@
   @Test
   @TestProjectInput(createEmptyCommit = false)
   public void deleteNewChangeForBranchWithoutCommits() throws Exception {
-    PushOneCommit.Result changeResult = createChange();
-    String changeId = changeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
 
-    gApi.changes().id(changeId).delete();
+    gApi.changes().id(changeIdentifier).delete();
 
-    assertThat(query(changeId)).isEmpty();
+    assertThat(query(changeIdentifier.id())).isEmpty();
   }
 
   @Test
-  @TestProjectInput(cloneAs = "user1")
   public void deleteAbandonedChangeAsNormalUser() throws Exception {
-    PushOneCommit.Result changeResult =
-        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-    String changeId = changeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
 
     requestScopeOperations.setApiUser(user.id());
-    gApi.changes().id(changeId).abandon();
+    gApi.changes().id(changeIdentifier).abandon();
 
     AuthException thrown =
-        assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
+        assertThrows(AuthException.class, () -> gApi.changes().id(changeIdentifier).delete());
     assertThat(thrown).hasMessageThat().contains("delete not permitted");
   }
 
   @Test
-  @TestProjectInput(cloneAs = "user1")
   public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
-    PushOneCommit.Result changeResult =
-        pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-    String changeId = changeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(user.id()).create();
 
-    gApi.changes().id(changeId).abandon();
+    gApi.changes().id(changeIdentifier).abandon();
 
-    gApi.changes().id(changeId).delete();
+    gApi.changes().id(changeIdentifier).delete();
 
-    assertThat(query(changeId)).isEmpty();
+    assertThat(query(changeIdentifier.id())).isEmpty();
   }
 
   @Test
   public void deleteMergedChange() throws Exception {
-    PushOneCommit.Result changeResult = createChange();
-    String changeId = changeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
 
-    merge(changeResult);
+    gApi.changes().id(changeIdentifier).current().review(ReviewInput.approve());
+    gApi.changes().id(changeIdentifier).current().submit();
 
     MethodNotAllowedException thrown =
-        assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
+        assertThrows(
+            MethodNotAllowedException.class, () -> gApi.changes().id(changeIdentifier).delete());
     assertThat(thrown).hasMessageThat().contains("delete not permitted");
   }
 
   @Test
-  @TestProjectInput(cloneAs = "user1")
   public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception {
     projectOperations
         .project(project)
@@ -1110,15 +1094,15 @@
         .update();
 
     try {
-      PushOneCommit.Result changeResult =
-          pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
-      String changeId = changeResult.getChangeId();
+      ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
 
-      merge(changeResult);
+      gApi.changes().id(changeIdentifier).current().review(ReviewInput.approve());
+      gApi.changes().id(changeIdentifier).current().submit();
 
       requestScopeOperations.setApiUser(user.id());
       MethodNotAllowedException thrown =
-          assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
+          assertThrows(
+              MethodNotAllowedException.class, () -> gApi.changes().id(changeIdentifier).delete());
       assertThat(thrown).hasMessageThat().contains("delete not permitted");
     } finally {
       projectOperations
@@ -1131,40 +1115,47 @@
 
   @Test
   public void deleteNewChangeWithMergedPatchSet() throws Exception {
-    PushOneCommit.Result changeResult = createChange();
-    String changeId = changeResult.getChangeId();
-    Change.Id id = changeResult.getChange().getId();
+    TestChange change = changeOperations.newChange().project(project).createAndGet();
 
-    merge(changeResult);
-    setChangeStatus(id, Change.Status.NEW);
+    gApi.changes().id(change.id()).current().review(ReviewInput.approve());
+    gApi.changes().id(change.id()).current().submit();
+
+    setChangeStatus(change.numericChangeId(), Change.Status.NEW);
 
     ResourceConflictException thrown =
-        assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).delete());
+        assertThrows(
+            ResourceConflictException.class, () -> gApi.changes().id(change.id()).delete());
     assertThat(thrown)
         .hasMessageThat()
-        .contains(String.format("Cannot delete change %s: patch set 1 is already merged", id));
+        .contains(
+            String.format(
+                "Cannot delete change %s: patch set 1 is already merged",
+                change.numericChangeId()));
   }
 
   @Test
   public void deleteChangeUpdatesIndex() throws Exception {
-    PushOneCommit.Result changeResult = createChange();
-    String changeId = changeResult.getChangeId();
-    Change.Id id = changeResult.getChange().getId();
+    TestChange change = changeOperations.newChange().project(project).createAndGet();
 
     ChangeIndex idx = changeIndexCollection.getSearchIndex();
 
     Optional<ChangeData> result =
-        idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
+        idx.get(
+            change.numericChangeId(),
+            IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
 
     assertThat(result).isPresent();
-    gApi.changes().id(changeId).delete();
-    result = idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
+    gApi.changes().id(change.id()).delete();
+    result =
+        idx.get(
+            change.numericChangeId(),
+            IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
     assertThat(result).isEmpty();
   }
 
   @Test
   public void deleteChangeRemovesDraftComment() throws Exception {
-    PushOneCommit.Result r = createChange();
+    TestChange change = changeOperations.newChange().createAndGet();
 
     requestScopeOperations.setApiUser(user.id());
 
@@ -1173,15 +1164,14 @@
     dri.path = "a.txt";
     dri.line = 1;
 
-    gApi.changes().id(r.getChangeId()).current().createDraft(dri);
-    Change.Id num = r.getChange().getId();
+    gApi.changes().id(change.id()).current().createDraft(dri);
 
-    assertThat(getDraftsCountForChange(num, user.id())).isGreaterThan(0);
+    assertThat(getDraftsCountForChange(change.numericChangeId(), user.id())).isGreaterThan(0);
 
     requestScopeOperations.setApiUser(admin.id());
 
-    gApi.changes().id(r.getChangeId()).delete();
-    assertThat(getDraftsCountForChange(num, user.id())).isEqualTo(0);
+    gApi.changes().id(change.id()).delete();
+    assertThat(getDraftsCountForChange(change.numericChangeId(), user.id())).isEqualTo(0);
   }
 
   @UsedAt(UsedAt.Project.GOOGLE)
@@ -1195,24 +1185,23 @@
 
   @Test
   public void deleteChangeRemovesItsChangeEdit() throws Exception {
-    PushOneCommit.Result result = createChange();
+    TestChange change = changeOperations.newChange().createAndGet();
 
     requestScopeOperations.setApiUser(user.id());
-    String changeId = result.getChangeId();
-    gApi.changes().id(changeId).edit().create();
+    gApi.changes().id(change.id()).edit().create();
     gApi.changes()
-        .id(changeId)
+        .id(change.id())
         .edit()
         .modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
 
     requestScopeOperations.setApiUser(admin.id());
-    String expected = RefNames.refsUsers(user.id()) + "/edit-" + result.getChange().getId() + "/1";
-    try (Repository repo = repoManager.openRepository(project)) {
+    String expected = RefNames.refsUsers(user.id()) + "/edit-" + change.numericChangeId() + "/1";
+    try (Repository repo = repoManager.openRepository(change.project())) {
       assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isNotEmpty();
-      gApi.changes().id(changeId).delete();
+      gApi.changes().id(change.id()).delete();
     }
     // On google infra, repo should be reopened for getting updated refs.
-    try (Repository repo = repoManager.openRepository(project)) {
+    try (Repository repo = repoManager.openRepository(change.project())) {
       assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isEmpty();
     }
   }
@@ -1244,32 +1233,31 @@
 
   @Test
   public void deleteChangeDoesntRemoveOtherChangeEdits() throws Exception {
-    PushOneCommit.Result result = createChange();
-    PushOneCommit.Result irrelevantChangeResult = createChange();
-    requestScopeOperations.setApiUser(admin.id());
-    String changeId = result.getChangeId();
-    String irrelevantChangeId = irrelevantChangeResult.getChangeId();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    ChangeIdentifier irrelevanetChangeIdentifier = changeOperations.newChange().create();
 
-    gApi.changes().id(irrelevantChangeId).edit().create();
+    requestScopeOperations.setApiUser(admin.id());
+
+    gApi.changes().id(irrelevanetChangeIdentifier).edit().create();
     gApi.changes()
-        .id(irrelevantChangeId)
+        .id(irrelevanetChangeIdentifier)
         .edit()
         .modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
 
-    gApi.changes().id(changeId).delete();
+    gApi.changes().id(changeIdentifier).delete();
 
-    assertThat(gApi.changes().id(irrelevantChangeId).edit().get()).isPresent();
+    assertThat(gApi.changes().id(irrelevanetChangeIdentifier).edit().get()).isPresent();
   }
 
   @Test
   public void attentionSetListener_firesOnChange() throws Exception {
-    PushOneCommit.Result r1 = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
     TestAttentionSetListener attentionSetListener = new TestAttentionSetListener();
 
     try (Registration registration =
         extensionRegistry.newRegistration().add(attentionSetListener)) {
 
-      gApi.changes().id(r1.getChangeId()).addReviewer(user.email());
+      gApi.changes().id(changeIdentifier).addReviewer(user.email());
       assertThat(attentionSetListener.firedCount).isEqualTo(1);
       assertThat(attentionSetListener.lastEvent.usersAdded().size()).isEqualTo(1);
       attentionSetListener
@@ -1279,12 +1267,12 @@
 
       // Adding the user with the same reason doesn't fire an event.
       AttentionSetInput addUser = new AttentionSetInput(user.email(), "Reviewer was added");
-      gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
+      gApi.changes().id(changeIdentifier).addToAttentionSet(addUser);
       assertThat(attentionSetListener.firedCount).isEqualTo(1);
 
       // Adding the user with a different reason fires an event.
       addUser = new AttentionSetInput(user.email(), "some reason");
-      gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
+      gApi.changes().id(changeIdentifier).addToAttentionSet(addUser);
       assertThat(attentionSetListener.firedCount).isEqualTo(2);
       assertThat(attentionSetListener.lastEvent.usersAdded().size()).isEqualTo(1);
       attentionSetListener
@@ -1293,7 +1281,7 @@
           .forEach(u -> assertThat(u).isEqualTo(user.id().get()));
 
       // Removing the user fires an event.
-      gApi.changes().id(r1.getChangeId()).attention(user.username()).remove(addUser);
+      gApi.changes().id(changeIdentifier).attention(user.username()).remove(addUser);
       assertThat(attentionSetListener.firedCount).isEqualTo(3);
       assertThat(attentionSetListener.lastEvent.usersAdded()).isEmpty();
       assertThat(attentionSetListener.lastEvent.usersRemoved().size()).isEqualTo(1);
@@ -1415,81 +1403,6 @@
   }
 
   @Test
-  public void pushCommitWithFooterOfOtherUser() throws Exception {
-    // admin pushes commit that references 'user' in a footer
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            PushOneCommit.SUBJECT
-                + "\n\n"
-                + FooterConstants.REVIEWED_BY.getName()
-                + ": "
-                + user.newIdent().toExternalString(),
-            PushOneCommit.FILE_NAME,
-            PushOneCommit.FILE_CONTENT);
-    PushOneCommit.Result result = push.to("refs/for/master");
-    result.assertOkStatus();
-
-    // check that 'user' was added as reviewer
-    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
-    assertThat(reviewers).isNotNull();
-    assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
-    assertThat(change.reviewers.get(CC)).isNull();
-
-    ImmutableList<Message> messages = sender.getMessages();
-    assertThat(messages).hasSize(1);
-    Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(user.getNameEmail());
-    assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
-    assertThat(m.body()).contains("I'd like you to do a code review.");
-    assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertMailReplyTo(m, admin.email());
-  }
-
-  @Test
-  public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
-    // create hidden project that is only visible to administrators
-    Project.NameKey p = projectOperations.newProject().create();
-    projectOperations
-        .project(p)
-        .forUpdate()
-        .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
-        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
-        .update();
-
-    // admin pushes commit that references 'user' in a footer
-    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            repo,
-            PushOneCommit.SUBJECT
-                + "\n\n"
-                + FooterConstants.REVIEWED_BY.getName()
-                + ": "
-                + user.newIdent().toExternalString(),
-            PushOneCommit.FILE_NAME,
-            PushOneCommit.FILE_CONTENT);
-    PushOneCommit.Result result = push.to("refs/for/master");
-    result.assertOkStatus();
-
-    // check that 'user' cannot see the change
-    requestScopeOperations.setApiUser(user.id());
-    assertThrows(
-        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
-
-    // check that 'user' was NOT added as cc ('user' can't see the change)
-    requestScopeOperations.setApiUser(admin.id());
-    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
-    assertThat(change.reviewers.get(REVIEWER)).isNull();
-    assertThat(change.reviewers.get(CC)).isNull();
-    assertThat(sender.getMessages()).isEmpty();
-  }
-
-  @Test
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = projectOperations.newProject().create();
@@ -1501,21 +1414,18 @@
         .update();
 
     // create change
-    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
-    PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
-    PushOneCommit.Result result = push.to("refs/for/master");
-    result.assertOkStatus();
+    ChangeIdentifier changeIdentifier =
+        changeOperations.newChange().project(p).owner(admin.id()).create();
 
     // check the user cannot see the change
     requestScopeOperations.setApiUser(user.id());
-    assertThrows(
-        ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
+    assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(changeIdentifier).get());
 
     // try to add user as reviewer
     requestScopeOperations.setApiUser(admin.id());
     ReviewerInput in = new ReviewerInput();
     in.reviewer = user.email();
-    ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+    ReviewerResult r = gApi.changes().id(changeIdentifier).addReviewer(in);
 
     assertThat(r.input).isEqualTo(user.email());
     assertThat(r.error).contains("does not have permission to see this change");
@@ -1524,14 +1434,14 @@
 
   @Test
   public void addReviewerThatIsInactiveByUsername() throws Exception {
-    PushOneCommit.Result result = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
 
     String username = name("new-user");
     Account.Id id = accountOperations.newAccount().username(username).inactive().create();
 
     ReviewerInput in = new ReviewerInput();
     in.reviewer = username;
-    ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+    ReviewerResult r = gApi.changes().id(changeIdentifier).addReviewer(in);
 
     assertThat(r.input).isEqualTo(in.reviewer);
     assertThat(r.error).isNull();
@@ -1545,14 +1455,14 @@
 
   @Test
   public void addReviewerThatIsInactiveById() throws Exception {
-    PushOneCommit.Result result = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
 
     String username = name("new-user");
     Account.Id id = accountOperations.newAccount().username(username).inactive().create();
 
     ReviewerInput in = new ReviewerInput();
     in.reviewer = Integer.toString(id.get());
-    ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+    ReviewerResult r = gApi.changes().id(changeIdentifier).addReviewer(in);
 
     assertThat(r.input).isEqualTo(in.reviewer);
     assertThat(r.error).isNull();
@@ -1566,17 +1476,16 @@
 
   @Test
   public void addReviewerThatIsInactiveByEmail() throws Exception {
-    ConfigInput conf = new ConfigInput();
-    conf.enableReviewerByEmail = InheritableBoolean.TRUE;
-    gApi.projects().name(project.get()).config(conf);
-    PushOneCommit.Result result = createChange();
+    projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
+
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
     String email = "user@domain.com";
     Account.Id id = accountOperations.newAccount().preferredEmail(email).inactive().create();
 
     ReviewerInput in = new ReviewerInput();
     in.reviewer = email;
     in.state = ReviewerState.CC;
-    ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+    ReviewerResult r = gApi.changes().id(changeIdentifier).addReviewer(in);
 
     assertThat(r.input).isEqualTo(email);
     assertThat(r.error).isNull();
@@ -1590,10 +1499,10 @@
   @UseClockStep
   public void addReviewer() throws Exception {
     testAddReviewerViaPostReview(
-        (changeId, reviewer) -> {
+        (changeIdentifier, reviewer) -> {
           ReviewerInput in = new ReviewerInput();
           in.reviewer = reviewer;
-          gApi.changes().id(changeId).addReviewer(in);
+          gApi.changes().id(changeIdentifier).addReviewer(in);
         });
   }
 
@@ -1601,21 +1510,25 @@
   @UseClockStep
   public void addReviewerViaPostReview() throws Exception {
     testAddReviewerViaPostReview(
-        (changeId, reviewer) -> {
+        (changeIdentifier, reviewer) -> {
           ReviewerInput reviewerInput = new ReviewerInput();
           reviewerInput.reviewer = reviewer;
           ReviewInput reviewInput = new ReviewInput();
           reviewInput.reviewers = ImmutableList.of(reviewerInput);
-          gApi.changes().id(changeId).current().review(reviewInput);
+          gApi.changes().id(changeIdentifier).current().review(reviewInput);
         });
   }
 
   private void testAddReviewerViaPostReview(AddReviewerCaller addReviewer) throws Exception {
-    PushOneCommit.Result r = createChange();
-    ChangeResource rsrc = parseResource(r);
-    Instant oldTs = rsrc.getChange().getLastUpdatedOn();
+    TestChange change =
+        changeOperations
+            .newChange()
+            .owner(admin.id())
+            .commitMessage(PushOneCommit.SUBJECT)
+            .createAndGet();
+    Instant oldTs = change.lastUpdatedOn();
 
-    addReviewer.call(r.getChangeId(), user.email());
+    addReviewer.call(change.id(), user.email());
 
     ImmutableList<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
@@ -1625,7 +1538,7 @@
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
     assertMailReplyTo(m, admin.email());
-    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    ChangeInfo c = gApi.changes().id(change.id()).get();
 
     // Adding a reviewer records that user as reviewer.
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
@@ -1637,30 +1550,31 @@
     assertThat(c.reviewers.get(CC)).isNull();
 
     // Ensure lastUpdatedOn is updated.
-    rsrc = parseResource(r);
-    assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
+    assertThat(changeOperations.change(change.id()).get().lastUpdatedOn()).isNotEqualTo(oldTs);
   }
 
   @Test
   public void postingMessageOnOwnChangeDoesntAddCallerAsReviewer() throws Exception {
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
 
+    requestScopeOperations.setApiUser(admin.id());
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.message = "Foo Bar";
-    gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    gApi.changes().id(changeIdentifier).current().review(reviewInput);
 
-    ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+    ChangeInfo c = gApi.changes().id(changeIdentifier).get();
     assertThat(c.reviewers.get(REVIEWER)).isNull();
     assertThat(c.reviewers.get(CC)).isNull();
   }
 
   @Test
   public void listReviewers() throws Exception {
-    PushOneCommit.Result r = createChange();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
+
     ReviewerInput in = new ReviewerInput();
     in.reviewer = user.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
-    assertThat(gApi.changes().id(r.getChangeId()).reviewers()).hasSize(1);
+    gApi.changes().id(changeIdentifier).addReviewer(in);
+    assertThat(gApi.changes().id(changeIdentifier).reviewers()).hasSize(1);
 
     String username1 = name("user1");
     String email1 = username1 + "@example.com";
@@ -1673,12 +1587,12 @@
             .create();
     in.reviewer = email1;
     in.state = ReviewerState.CC;
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
+    gApi.changes().id(changeIdentifier).addReviewer(in);
     if (server.isUsernameSupported()) {
-      assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username))
+      assertThat(gApi.changes().id(changeIdentifier).reviewers().stream().map(a -> a.username))
           .containsExactly(user.username(), username1);
     }
-    assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a._accountId))
+    assertThat(gApi.changes().id(changeIdentifier).reviewers().stream().map(a -> a._accountId))
         .containsExactly(user.id().get(), user1Id.get());
   }
 
@@ -1859,7 +1773,7 @@
     in.state = REMOVED;
     ReviewerResult reviewerResult = gApi.changes().id(r.getChangeId()).addReviewer(in);
     assertThat(reviewerResult.error)
-        .isEqualTo(MessageFormat.format(ChangeMessages.get().groupRemovalIsNotAllowed, groupName));
+        .isEqualTo(MessageFormat.format(ChangeMessages.groupRemovalIsNotAllowed, groupName));
   }
 
   @Test
@@ -2179,6 +2093,110 @@
   }
 
   @Test
+  public void cannotRemoveCCWithoutRemoveReviewerPermission() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+
+    // Add a cc
+    ReviewerInput reviewerInput = new ReviewerInput();
+    reviewerInput.state = CC;
+    reviewerInput.reviewer = user.id().toString();
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+
+    // Try removing the cc as a user that doesn't have the Remove Reviewer permission:
+    requestScopeOperations.setApiUser(accountCreator.create().id());
+
+    // a) via the Delete Reviewer endpoint:
+    AuthException exception =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(changeId).reviewer(user.id().toString()).remove());
+    assertThat(exception).hasMessageThat().isEqualTo("remove reviewer not permitted");
+
+    // b) via the Post Review endpoint:
+    reviewerInput = new ReviewerInput();
+    reviewerInput.reviewer = user.id().toString();
+    reviewerInput.state = ReviewerState.REMOVED;
+    ReviewInput input = new ReviewInput();
+    input.reviewers = ImmutableList.of(reviewerInput);
+    exception =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(changeId).current().review(input));
+    assertThat(exception).hasMessageThat().isEqualTo("remove reviewer not permitted");
+  }
+
+  @Test
+  public void removeSelfFromCCPossibleWithoutRemoveReviewerPermission() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+
+    // Add a cc
+    ReviewerInput reviewerInput = new ReviewerInput();
+    reviewerInput.state = CC;
+    reviewerInput.reviewer = user.id().toString();
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+    assertThat(gApi.changes().id(changeId).get().reviewers).isNotEmpty();
+
+    // Try removing the cc as that user should work since users can always remove themselves
+    requestScopeOperations.setApiUser(user.id());
+
+    // a) via the Delete Reviewer endpoint:
+    gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
+    assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
+
+    // Add the user back to cc
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+    assertThat(gApi.changes().id(changeId).get().reviewers).isNotEmpty();
+
+    // b) via the Post Review endpoint:
+    reviewerInput = new ReviewerInput();
+    reviewerInput.reviewer = user.id().toString();
+    reviewerInput.state = ReviewerState.REMOVED;
+    ReviewInput input = new ReviewInput();
+    input.reviewers = ImmutableList.of(reviewerInput);
+    gApi.changes().id(changeId).current().review(input);
+    assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
+  }
+
+  @Test
+  public void cannotRemoveSelfFromCCTogetherWithOtherCCWithoutRemoveReviewerPermission()
+      throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+
+    // Add a cc
+    ReviewerInput reviewerInput = new ReviewerInput();
+    reviewerInput.state = CC;
+    reviewerInput.reviewer = user.id().toString();
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+    assertThat(gApi.changes().id(changeId).get().reviewers.get(CC)).hasSize(1);
+
+    // Add another cc
+    TestAccount otherCC = accountCreator.create();
+    reviewerInput = new ReviewerInput();
+    reviewerInput.state = CC;
+    reviewerInput.reviewer = otherCC.id().toString();
+    gApi.changes().id(changeId).addReviewer(reviewerInput);
+    assertThat(gApi.changes().id(changeId).get().reviewers.get(CC)).hasSize(2);
+
+    // Trying to remove themselves as cc and other ccs should not work since the user cannot remove
+    // other users from cc.
+    requestScopeOperations.setApiUser(user.id());
+    ReviewerInput reviewerInputRemoveSelfCC = new ReviewerInput();
+    reviewerInputRemoveSelfCC.reviewer = user.id().toString();
+    reviewerInputRemoveSelfCC.state = ReviewerState.REMOVED;
+    ReviewerInput reviewerInputRemoveOtherCC = new ReviewerInput();
+    reviewerInputRemoveOtherCC.reviewer = otherCC.id().toString();
+    reviewerInputRemoveOtherCC.state = ReviewerState.REMOVED;
+    ReviewInput input = new ReviewInput();
+    input.reviewers = ImmutableList.of(reviewerInputRemoveSelfCC, reviewerInputRemoveOtherCC);
+    AuthException exception =
+        assertThrows(
+            AuthException.class, () -> gApi.changes().id(changeId).current().review(input));
+    assertThat(exception).hasMessageThat().isEqualTo("remove reviewer not permitted");
+  }
+
+  @Test
   public void removeReviewer() throws Exception {
     testRemoveReviewer(true);
   }
@@ -3153,6 +3171,25 @@
   }
 
   @Test
+  public void submitAsNotAllowedWithoutPermission() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = accountCreator.user2().email();
+    requestScopeOperations.setApiUser(user.id());
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(in));
+    assertThat(thrown).hasMessageThat().contains("submit on behalf of other users not permitted");
+  }
+
+  @Test
   public void submitAllowedWithPermission() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -3167,6 +3204,46 @@
   }
 
   @Test
+  public void submitAsAllowedWithPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+        .add(allow(Permission.SUBMIT_AS).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = accountCreator.user2().email();
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(in);
+    assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED);
+  }
+
+  @Test
+  public void submitAsAllowedWithRunAsPermission() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowCapability(GlobalCapability.RUN_AS).group(REGISTERED_USERS))
+        .update();
+
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = accountCreator.user2().email();
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(in);
+    assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED);
+  }
+
+  @Test
   public void submitToSymref() throws Exception {
     // Create symref in the origin repository (testRepo references to a local repository)
     try (RefUpdateContext ctx = openTestRefUpdateContext()) {
@@ -3787,6 +3864,9 @@
 
     change = gApi.changes().id(r.getChangeId()).get();
     assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
+    assertThat(change.labels.get(LabelId.CODE_REVIEW).values).isNotNull();
+    assertThat(change.labels.get(LabelId.CODE_REVIEW).values.keySet())
+        .containsExactly("-2", "-1", " 0", "+1", "+2");
     assertThat(change.permittedLabels.keySet())
         .containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
     assertPermitted(change, LabelId.CODE_REVIEW, 2);
@@ -3797,6 +3877,10 @@
   @Test
   @GerritConfig(name = "change.skipCurrentRulesEvaluationOnClosedChanges", value = "true")
   public void checkLabelsNotUpdatedForMergedChange() throws Exception {
+    if (!prologSubmitRuleUtil.isProjectRulesEnabled()) {
+      // This test is not needed, when labels do not require rule evaluation.
+      return;
+    }
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
     gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
@@ -3833,6 +3917,10 @@
   @Test
   @GerritConfig(name = "change.skipCurrentRulesEvaluationOnClosedChanges", value = "true")
   public void checkLabelsNotUpdatedForAbandonedChange() throws Exception {
+    if (!prologSubmitRuleUtil.isProjectRulesEnabled()) {
+      // This test is not needed, when labels do not require rule evaluation.
+      return;
+    }
     PushOneCommit.Result r = createChange();
     gApi.changes().id(r.getChangeId()).abandon();
 
@@ -3901,6 +3989,7 @@
   }
 
   @Test
+  @GerritConfig(name = "rules.allowNewRules", value = "true")
   public void uploadingRulesPlIsNotAllowed() throws Exception {
     GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
     testRepo.reset("config");
@@ -4144,7 +4233,7 @@
         String.format(
             "%s\n\nFirst Paragraph.\n\nSecond Paragraph\n\nFoo: Bar\nChange-Id: %s\n",
             subject, changeId);
-    changeOperations.newChange().project(project).commitMessage(commitMessage).create();
+    changeOperations.newChange().project(project).commitMessage(commitMessage).createV1();
 
     CommitMessageInfo commitMessageInfo = gApi.changes().id(changeId).getMessage();
     assertThat(commitMessageInfo.subject).isEqualTo(subject);
@@ -4160,7 +4249,7 @@
         String.format(
             "%s\n\nFirst Paragraph.\n\nSecond Paragraph\n\nFoo: Bar\nFoo: Baz\nChange-Id: %s\n",
             subject, changeId);
-    changeOperations.newChange().project(project).commitMessage(commitMessage).create();
+    changeOperations.newChange().project(project).commitMessage(commitMessage).createV1();
 
     CommitMessageInfo commitMessageInfo = gApi.changes().id(changeId).getMessage();
     assertThat(commitMessageInfo.subject).isEqualTo(subject);
@@ -4214,7 +4303,7 @@
     Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
 
     // Create change
-    Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+    Change.Id change = changeOperations.newChange().project(project).owner(testUser).createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -4255,9 +4344,11 @@
 
   @Test
   public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception {
-    ConfigInput configInput = new ConfigInput();
-    configInput.requireChangeId = InheritableBoolean.FALSE;
-    gApi.projects().name(project.get()).config(configInput);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .requireChangeId(InheritableBoolean.FALSE)
+        .update();
 
     PushOneCommit.Result r = createChange();
     r.assertOkStatus();
@@ -4529,6 +4620,149 @@
     assertThrows(AuthException.class, () -> gApi.changes().id(r2.getChangeId()).current().submit());
   }
 
+  @Test
+  public void getWithOwnerDetails() throws Exception {
+    String fullname = "Foo Bar";
+    String email = "foo.bar@example.com";
+    String username = "foobar";
+    Account.Id changeOwnerId =
+        accountOperations
+            .newAccount()
+            .fullname(fullname)
+            .preferredEmail(email)
+            .username(username)
+            .create();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(changeOwnerId).create();
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+
+    assertThat(changeInfo.owner._accountId).isEqualTo(changeOwnerId.get());
+    assertThat(changeInfo.owner.name).isEqualTo(fullname);
+    assertThat(changeInfo.owner.email).isEqualTo(email);
+    if (server.isUsernameSupported()) {
+      assertThat(changeInfo.owner.username).isEqualTo(username);
+    }
+    assertThat(changeInfo.owner.deleted).isNull();
+  }
+
+  @Test
+  public void getWithOwnerDetails_noAccountDataSet() throws Exception {
+    Account.Id changeOwnerId = accountOperations.newAccount().create();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(changeOwnerId).create();
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier.id()).get();
+
+    assertThat(changeInfo.owner._accountId).isEqualTo(changeOwnerId.get());
+    assertThat(changeInfo.owner.name).isNull();
+    assertThat(changeInfo.owner.email).isNull();
+    assertThat(changeInfo.owner.username).isNull();
+    assertThat(changeInfo.owner.deleted).isNull();
+  }
+
+  @Test
+  public void getWithOwnerDetails_accountDeleted() throws Exception {
+    Account.Id changeOwnerId =
+        accountOperations
+            .newAccount()
+            .fullname("Foo Bar")
+            .preferredEmail("foo.bar@example.com")
+            .username("foobar")
+            .create();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(changeOwnerId).create();
+
+    requestScopeOperations.setApiUser(changeOwnerId);
+    gApi.accounts().self().delete();
+
+    requestScopeOperations.setApiUser(admin.id());
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+
+    assertThat(changeInfo.owner._accountId).isEqualTo(changeOwnerId.get());
+    assertThat(changeInfo.owner.name).isNull();
+    assertThat(changeInfo.owner.email).isNull();
+    assertThat(changeInfo.owner.username).isNull();
+    assertThat(changeInfo.owner.deleted).isTrue();
+  }
+
+  @Test
+  public void getWithReviewerDetails() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+
+    String fullname = "Foo Bar";
+    String email = "foo.bar@example.com";
+    String username = "foobar";
+    Account.Id reviewerId =
+        accountOperations
+            .newAccount()
+            .fullname(fullname)
+            .preferredEmail(email)
+            .username(username)
+            .create();
+    gApi.changes().id(changeIdentifier).addReviewer(reviewerId.toString());
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+
+    AccountInfo reviewerAccountInfo =
+        Iterables.getOnlyElement(changeInfo.reviewers.get(ReviewerState.REVIEWER));
+
+    assertThat(reviewerAccountInfo._accountId).isEqualTo(reviewerId.get());
+    assertThat(reviewerAccountInfo.name).isEqualTo(fullname);
+    assertThat(reviewerAccountInfo.email).isEqualTo(email);
+    if (server.isUsernameSupported()) {
+      assertThat(reviewerAccountInfo.username).isEqualTo(username);
+    }
+    assertThat(reviewerAccountInfo.deleted).isNull();
+  }
+
+  @Test
+  public void getWithReviewerDetails_noAccountDataSet() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+
+    Account.Id reviewerId = accountOperations.newAccount().create();
+    gApi.changes().id(changeIdentifier).addReviewer(reviewerId.toString());
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+
+    AccountInfo reviewerAccountInfo =
+        Iterables.getOnlyElement(changeInfo.reviewers.get(ReviewerState.REVIEWER));
+
+    assertThat(reviewerAccountInfo._accountId).isEqualTo(reviewerId.get());
+    assertThat(reviewerAccountInfo.name).isNull();
+    assertThat(reviewerAccountInfo.email).isNull();
+    assertThat(reviewerAccountInfo.username).isNull();
+    assertThat(reviewerAccountInfo.deleted).isNull();
+  }
+
+  @Test
+  public void getWithReviewerDetails_accountDeleted() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+
+    Account.Id reviewerId =
+        accountOperations
+            .newAccount()
+            .fullname("Foo Bar")
+            .preferredEmail("foo.bar@example.com")
+            .username("foobar")
+            .create();
+    gApi.changes().id(changeIdentifier).addReviewer(reviewerId.toString());
+
+    requestScopeOperations.setApiUser(reviewerId);
+    gApi.accounts().self().delete();
+
+    requestScopeOperations.setApiUser(admin.id());
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+
+    AccountInfo reviewerAccountInfo =
+        Iterables.getOnlyElement(changeInfo.reviewers.get(ReviewerState.REVIEWER));
+
+    assertThat(reviewerAccountInfo._accountId).isEqualTo(reviewerId.get());
+    assertThat(reviewerAccountInfo.name).isNull();
+    assertThat(reviewerAccountInfo.email).isNull();
+    assertThat(reviewerAccountInfo.username).isNull();
+    assertThat(reviewerAccountInfo.deleted).isTrue();
+  }
+
   private void submittableAfterLosingPermissions(String label) throws Exception {
     String codeReviewLabel = LabelId.CODE_REVIEW;
     AccountGroup.UUID registered = REGISTERED_USERS;
@@ -4881,14 +5115,7 @@
   public void ccNonExistentAccountByEmailThenRemoveByDelete() throws Exception {
     // Create a project that allows reviewers by email.
     Project.NameKey project = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .updateProject(
-              b ->
-                  b.setBooleanConfig(
-                      BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.TRUE));
-      u.save();
-    }
+    projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
 
     // Create a change
     TestRepository<?> testRepo = cloneProject(project, admin);
@@ -4927,14 +5154,7 @@
   public void ccNonExistentAccountByEmailThenRemoveByPostReview() throws Exception {
     // Create a project that allows reviewers by email.
     Project.NameKey project = projectOperations.newProject().create();
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .updateProject(
-              b ->
-                  b.setBooleanConfig(
-                      BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.TRUE));
-      u.save();
-    }
+    projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
 
     // Create a change
     TestRepository<?> testRepo = cloneProject(project, admin);
@@ -5170,7 +5390,7 @@
 
   @FunctionalInterface
   private interface AddReviewerCaller {
-    void call(String changeId, String reviewer) throws RestApiException;
+    void call(ChangeIdentifier changeIdentifier, String reviewer) throws RestApiException;
   }
 
   public static class TestAttentionSetListenerModule extends AbstractModule {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java b/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java
index 710ae54..7889753 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java
@@ -21,7 +21,6 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -32,6 +31,7 @@
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -130,8 +130,11 @@
     assertThat(currentRevision.commit.parents.get(0).commit)
         .isEqualTo(currentMaster.getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
     assertThat(currentRevision.conflicts.ours).isEqualTo(targetBranch.name());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(sourceBranch.name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
 
     // Verify the message that has been posted on the change.
@@ -178,7 +181,7 @@
     createBranch(BranchNameKey.create(project, branch));
 
     // Create a change for master branch
-    Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+    Change.Id change = changeOperations.newChange().project(project).owner(testUser).createV1();
 
     // Push a commit to dev branch
     createChange("refs/heads/dev");
@@ -231,11 +234,29 @@
         assertThrows(
             ResourceConflictException.class,
             () -> gApi.changes().id(changeId).createMergePatchSet(in));
-    assertThat(thrown).hasMessageThat().isEqualTo("merge conflict(s):\n" + fileName);
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                """
+                merge conflict(s):
+                * %s
+                """,
+                fileName));
   }
 
   @Test
-  public void createMergePatchSet_ConflictAllowed() throws Exception {
+  public void createMergePatchSetWithConflictAllowed() throws Exception {
+    testCreateMergePatchSetWithConflictAllowed(/* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void createMergePatchSetWithConflictAllowedDiff3() throws Exception {
+    testCreateMergePatchSetWithConflictAllowed(/* useDiff3= */ true);
+  }
+
+  private void testCreateMergePatchSetWithConflictAllowed(boolean useDiff3) throws Exception {
     RevCommit initialHead = projectOperations.project(project).getHead("master");
     createBranch(BranchNameKey.create(project, "dev"));
 
@@ -295,8 +316,11 @@
     assertThat(currentRevision.commit.parents.get(0).commit)
         .isEqualTo(currentMaster.getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
     assertThat(currentRevision.conflicts.ours).isEqualTo(targetBranch.name());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(sourceBranch.name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isTrue();
 
     // Verify that the file content in the created patch set is correct.
@@ -305,22 +329,25 @@
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String sourceSha1 = abbreviateName(changeA.getCommit(), 6);
-    String targetSha1 = abbreviateName(currentMaster.getCommit(), 6);
     assertThat(fileContent)
         .isEqualTo(
             "<<<<<<< TARGET BRANCH ("
-                + targetSha1
+                + currentMaster.getCommit().getName()
                 + " "
                 + targetSubject
                 + ")\n"
                 + targetContent
                 + "\n"
+                + (useDiff3
+                    ? String.format(
+                        "||||||| BASE          (%s %s)\n",
+                        initialHead.getName(), initialHead.getShortMessage())
+                    : "")
                 + "=======\n"
                 + sourceContent
                 + "\n"
                 + ">>>>>>> SOURCE BRANCH ("
-                + sourceSha1
+                + changeA.getCommit().getName()
                 + " "
                 + sourceSubject
                 + ")\n");
@@ -427,8 +454,11 @@
     assertThat(currentRevision.commit.parents.get(0).commit)
         .isNotEqualTo(currentMaster.getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
     assertThat(currentRevision.conflicts.ours).isEqualTo(parent);
     assertThat(currentRevision.conflicts.theirs).isEqualTo(sourceBranch.name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
   }
 
@@ -531,8 +561,11 @@
     RevisionInfo currentRevision = changeInfo.getCurrentRevision();
     assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(baseChangeCommit);
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
     assertThat(currentRevision.conflicts.ours).isEqualTo(baseChangeCommit);
     assertThat(currentRevision.conflicts.theirs).isEqualTo(sourceBranch.name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/EvaluateChangeQueryExpressionIT.java b/javatests/com/google/gerrit/acceptance/api/change/EvaluateChangeQueryExpressionIT.java
new file mode 100644
index 0000000..3fe15ff
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/EvaluateChangeQueryExpressionIT.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.TestExtensions.TestSubmitRule;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import com.google.gerrit.extensions.common.EvaluateChangeQueryExpressionResultInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.index.query.MatchResult;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeIsOperandFactory;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+/**
+ * Integration tests for the {@link
+ * com.google.gerrit.server.restapi.change.EvaluateChangeQueryExpression} REST endpoint.
+ */
+public class EvaluateChangeQueryExpressionIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void missingExpression() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(changeIdentifier)
+                    .evaluateChangeQueryExpression()
+                    .withExpression(null)
+                    .get());
+    assertThat(exception).hasMessageThat().isEqualTo("expression is required");
+  }
+
+  @Test
+  public void emptyExpression() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(changeIdentifier)
+                    .evaluateChangeQueryExpression()
+                    .withExpression("")
+                    .get());
+    assertThat(exception).hasMessageThat().isEqualTo("expression is required");
+  }
+
+  @Test
+  public void nonParseableExpression() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(changeIdentifier)
+                    .evaluateChangeQueryExpression()
+                    .withExpression("[INVALID]")
+                    .get());
+    assertThat(exception).hasMessageThat().contains("invalid query expression:");
+  }
+
+  @Test
+  public void unsupportedOperator() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () ->
+                gApi.changes()
+                    .id(changeIdentifier)
+                    .evaluateChangeQueryExpression()
+                    .withExpression("foo:bar")
+                    .get());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo("invalid query expression: Unsupported operator foo:bar");
+  }
+
+  @Test
+  public void singleAtomExpressionThatMatches() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    EvaluateChangeQueryExpressionResultInfo info =
+        gApi.changes()
+            .id(changeIdentifier)
+            .evaluateChangeQueryExpression()
+            .withExpression("is:open")
+            .get();
+    assertThat(info.status).isTrue();
+    assertThat(info.passingAtoms).containsExactly("is:open");
+    assertThat(info.failingAtoms).isEmpty();
+    assertThat(info.atomExplanations).isNull();
+  }
+
+  @Test
+  public void singleAtomExpressionThatDoesNotMatch() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    EvaluateChangeQueryExpressionResultInfo info =
+        gApi.changes()
+            .id(changeIdentifier)
+            .evaluateChangeQueryExpression()
+            .withExpression("is:closed")
+            .get();
+    assertThat(info.status).isFalse();
+    assertThat(info.passingAtoms).isEmpty();
+    assertThat(info.failingAtoms).containsExactly("is:closed");
+    assertThat(info.atomExplanations).isNull();
+  }
+
+  @Test
+  public void multiAtomExpressionThatMatches() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    changeOperations.change(changeIdentifier).newVote().codeReviewApproval().create();
+    EvaluateChangeQueryExpressionResultInfo info =
+        gApi.changes()
+            .id(changeIdentifier)
+            .evaluateChangeQueryExpression()
+            .withExpression("is:open label:Code-Review+2")
+            .get();
+    assertThat(info.status).isTrue();
+    assertThat(info.passingAtoms).containsExactly("is:open", "label:Code-Review+2");
+    assertThat(info.failingAtoms).isEmpty();
+    assertThat(info.atomExplanations).isNull();
+  }
+
+  @Test
+  public void multiAtomExpressionThatDoesNotMatch() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    EvaluateChangeQueryExpressionResultInfo info =
+        gApi.changes()
+            .id(changeIdentifier)
+            .evaluateChangeQueryExpression()
+            .withExpression("is:closed label:Code-Review+2")
+            .get();
+    assertThat(info.status).isFalse();
+    assertThat(info.passingAtoms).isEmpty();
+    assertThat(info.failingAtoms).containsExactly("is:closed", "label:Code-Review+2");
+    assertThat(info.atomExplanations).isNull();
+
+    changeOperations.change(changeIdentifier).newVote().codeReviewApproval().create();
+    info =
+        gApi.changes()
+            .id(changeIdentifier)
+            .evaluateChangeQueryExpression()
+            .withExpression("is:closed label:Code-Review+2")
+            .get();
+    assertThat(info.status).isFalse();
+    assertThat(info.passingAtoms).containsExactly("label:Code-Review+2");
+    assertThat(info.failingAtoms).containsExactly("is:closed");
+    assertThat(info.atomExplanations).isNull();
+  }
+
+  @Test
+  public void withAtomExplanation() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+
+    // Register an "is:foo" predicate that never matches and has an atom explanation.
+    try (Registration registration =
+        extensionRegistry
+            .newRegistration()
+            .add(
+                new ChangeIsOperandFactory() {
+                  @Override
+                  public Predicate<ChangeData> create(ChangeQueryBuilder builder)
+                      throws QueryParseException {
+                    return new OperatorPredicate<>("is", "foo") {
+                      @Override
+                      public boolean isMatchable() {
+                        return true;
+                      }
+
+                      @Override
+                      public Matchable<ChangeData> asMatchable() {
+                        return new Matchable<>() {
+                          @Override
+                          public boolean match(ChangeData object) {
+                            return false;
+                          }
+
+                          @Override
+                          public MatchResult matchResult(ChangeData changeData) {
+                            return new MatchResult(match(changeData), "this atom never matches");
+                          }
+
+                          @Override
+                          public int getCost() {
+                            return 0;
+                          }
+                        };
+                      }
+                    };
+                  }
+                },
+                "foo")) {
+      String atom = String.format("is:foo_%s", ExtensionRegistry.PLUGIN_NAME);
+      EvaluateChangeQueryExpressionResultInfo info =
+          gApi.changes()
+              .id(changeIdentifier)
+              .evaluateChangeQueryExpression()
+              .withExpression(atom)
+              .get();
+      assertThat(info.status).isFalse();
+      assertThat(info.passingAtoms).isEmpty();
+      assertThat(info.failingAtoms).containsExactly(atom);
+      assertThat(info.atomExplanations).containsExactly(atom, "this atom never matches");
+
+      info =
+          gApi.changes()
+              .id(changeIdentifier)
+              .evaluateChangeQueryExpression()
+              .withExpression("is:open " + atom)
+              .get();
+      assertThat(info.status).isFalse();
+      assertThat(info.passingAtoms).containsExactly("is:open");
+      assertThat(info.failingAtoms).containsExactly(atom);
+      assertThat(info.atomExplanations).containsExactly(atom, "this atom never matches");
+    }
+  }
+
+  @Test
+  public void evaluatngIsSubmittableInvokesSubmitRulesOnce() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    changeOperations.change(changeIdentifier).newVote().codeReviewApproval().create();
+
+    TestSubmitRule testSubmitRule = new TestSubmitRule();
+    try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
+      EvaluateChangeQueryExpressionResultInfo info =
+          gApi.changes()
+              .id(changeIdentifier)
+              .evaluateChangeQueryExpression()
+              .withExpression("is:submittable")
+              .get();
+      assertThat(info.status).isTrue();
+      assertThat(info.passingAtoms).containsExactly("is:submittable");
+      assertThat(info.failingAtoms).isEmpty();
+      assertThat(info.atomExplanations).isNull();
+    }
+    assertThat(testSubmitRule.count()).isEqualTo(1);
+  }
+
+  @Test
+  public void evaluatingIsSubmittableUsingTheIndexDoesntInvokeSubmitRules() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    changeOperations.change(changeIdentifier).newVote().codeReviewApproval().create();
+
+    TestSubmitRule testSubmitRule = new TestSubmitRule();
+    try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
+      EvaluateChangeQueryExpressionResultInfo info =
+          gApi.changes()
+              .id(changeIdentifier)
+              .evaluateChangeQueryExpression()
+              .withExpression("is:submittable")
+              .useIndex()
+              .get();
+      assertThat(info.status).isTrue();
+      assertThat(info.passingAtoms).containsExactly("is:submittable");
+      assertThat(info.failingAtoms).isEmpty();
+      assertThat(info.atomExplanations).isNull();
+    }
+    assertThat(testSubmitRule.count()).isEqualTo(0);
+  }
+
+  @Test
+  public void
+      evaluatingExpressionThatDoesntRequireCheckingTheChangeSubmittabilityDoesntInvokesSubmitRules()
+          throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    changeOperations.change(changeIdentifier).newVote().codeReviewApproval().create();
+
+    TestSubmitRule testSubmitRule = new TestSubmitRule();
+    try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
+      EvaluateChangeQueryExpressionResultInfo info =
+          gApi.changes()
+              .id(changeIdentifier)
+              .evaluateChangeQueryExpression()
+              .withExpression("is:open")
+              .get();
+      assertThat(info.status).isTrue();
+      assertThat(info.passingAtoms).containsExactly("is:open");
+      assertThat(info.failingAtoms).isEmpty();
+      assertThat(info.atomExplanations).isNull();
+    }
+    assertThat(testSubmitRule.count()).isEqualTo(0);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index ec29bf9..2fa4166 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -21,7 +21,6 @@
 import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static java.util.stream.Collectors.toList;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
@@ -39,6 +38,7 @@
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestExtensions.TestSubmitRule;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -50,14 +50,12 @@
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -67,7 +65,6 @@
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.events.CommentAddedListener;
 import com.google.gerrit.extensions.events.ReviewerAddedListener;
@@ -79,12 +76,9 @@
 import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.OnPostReview;
 import com.google.gerrit.server.restapi.change.PostReview;
-import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.update.CommentsRejectedException;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.TestCommentHelper;
@@ -93,7 +87,6 @@
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
@@ -369,7 +362,7 @@
   }
 
   @Test
-  @GerritConfig(name = "change.maxComments", value = "7")
+  @GerritConfig(name = "change.maxComments", value = "5")
   public void restrictNumberOfComments() throws Exception {
     when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
 
@@ -379,17 +372,12 @@
     commentInput.line = 1;
     commentInput.message = "foo";
     commentInput.path = filePath;
-    RobotCommentInput robotCommentInput =
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(filePath);
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.comments = ImmutableMap.of(filePath, ImmutableList.of(commentInput));
-    reviewInput.robotComments = ImmutableMap.of(filePath, ImmutableList.of(robotCommentInput));
     gApi.changes().id(r.getChangeId()).current().review(reviewInput);
-    // Counting change messages plus comments we now have 4.
-
-    // reviewInput still has both a user and a robot comment (and deduplication is false). We also
-    // create a draft, and there's the change message, so that in total there would be 8 comments.
-    // The limit is set to 7, so this verifies that all new comments are considered.
+    // reviewInput still has both a user comment (and deduplication is false). We also
+    // create a draft, and there's the change message, so that in total there would be 6 comments.
+    // The limit is set to 5, so this verifies that all new comments are considered.
     DraftInput draftInline = testCommentHelper.newDraft(filePath, Side.REVISION, 1, "a draft");
     testCommentHelper.addDraft(r.getChangeId(), r.getPatchSetId().getId(), draftInline);
     reviewInput.drafts = DraftHandling.PUBLISH;
@@ -401,10 +389,9 @@
             () -> gApi.changes().id(r.getChangeId()).current().review(reviewInput));
     assertThat(exception)
         .hasMessageThat()
-        .contains("Exceeding maximum number of comments: 4 (existing) + 4 (new) > 7");
+        .contains("Exceeding maximum number of comments: 3 (existing) + 3 (new) > 5");
 
     assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1);
-    assertThat(getRobotComments(r.getChangeId())).hasSize(1);
   }
 
   @Test
@@ -818,7 +805,7 @@
       gApi.changes().id(r.getChangeId()).current().review(input);
     }
 
-    assertThat(testSubmitRule.count).isEqualTo(1);
+    assertThat(testSubmitRule.count()).isEqualTo(1);
   }
 
   @Test
@@ -832,7 +819,7 @@
       gApi.changes().id(r.getChangeId()).current().review(input);
     }
 
-    assertThat(testSubmitRule.count).isEqualTo(1);
+    assertThat(testSubmitRule.count()).isEqualTo(1);
   }
 
   @Test
@@ -1199,12 +1186,6 @@
     }
   }
 
-  private List<RobotCommentInfo> getRobotComments(String changeId) throws RestApiException {
-    return gApi.changes().id(changeId).robotComments().values().stream()
-        .flatMap(Collection::stream)
-        .collect(toList());
-  }
-
   private static CommentInput newComment(String path) {
     return TestCommentHelper.populate(new CommentInput(), path, PostReviewIT.COMMENT_TEXT);
   }
@@ -1275,24 +1256,6 @@
     }
   }
 
-  private static class TestSubmitRule implements SubmitRule {
-    int count;
-
-    @Override
-    public Optional<SubmitRecord> evaluate(ChangeData changeData) {
-      if (!isAsyncCallForSendingReviewCommentsEmail()) {
-        count++;
-      }
-      return Optional.empty();
-    }
-
-    private boolean isAsyncCallForSendingReviewCommentsEmail() {
-      return Arrays.stream(Thread.currentThread().getStackTrace())
-          .map(StackTraceElement::getClassName)
-          .anyMatch(className -> EmailReviewComments.class.getName().equals(className));
-    }
-  }
-
   private static class TestReviewerAddedListener implements ReviewerAddedListener {
     List<ReviewerAddedListener.Event> receivedEvents = new ArrayList<>();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java
index a6c3c27..121f5f1 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesFilterPermissionBackendIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.permissions.DefaultPermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -73,6 +74,16 @@
     }
 
     @Override
+    public WithUser user(CurrentUser user, ImpersonationPermissionMode permissionMode) {
+      return new TestPermissionWithUser(defaultPermissionBackend.user(user, permissionMode));
+    }
+
+    @Override
+    public WithUser exactUser(CurrentUser user) {
+      return new TestPermissionWithUser(defaultPermissionBackend.exactUser(user));
+    }
+
+    @Override
     public WithUser absentUser(Account.Id id) {
       return new TestPermissionWithUser(defaultPermissionBackend.absentUser(id));
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
index 30f500e..9d6331d 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
@@ -382,7 +382,7 @@
         accountOperations.newAccount().preferredEmail(reviewerEmail).create();
 
     // Create the change.
-    Change.Id changeId = changeOperations.newChange().owner(nonVisibleOwner).create();
+    Change.Id changeId = changeOperations.newChange().owner(nonVisibleOwner).createV1();
 
     // Add a review.
     requestScopeOperations.setApiUser(nonVisibleReviewer);
@@ -449,7 +449,8 @@
         accountOperations.newAccount().preferredEmail(reviewerEmail).create();
 
     // Create the change.
-    Change.Id changeId = changeOperations.newChange().owner(nonVisibleOwner).create();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).owner(nonVisibleOwner).createV1();
 
     // Add a review.
     requestScopeOperations.setApiUser(nonVisibleReviewer);
@@ -503,7 +504,7 @@
             .create();
 
     // Create the change.
-    Change.Id changeId = changeOperations.newChange().owner(owner).create();
+    Change.Id changeId = changeOperations.newChange().owner(owner).createV1();
 
     // Add a review.
     requestScopeOperations.setApiUser(reviewer);
@@ -567,7 +568,7 @@
             .create();
 
     // Create the change.
-    Change.Id changeId = changeOperations.newChange().owner(owner).create();
+    Change.Id changeId = changeOperations.newChange().owner(owner).createV1();
 
     // Add a review.
     requestScopeOperations.setApiUser(reviewer);
@@ -622,7 +623,7 @@
             .create();
 
     // Create the change.
-    Change.Id changeId = changeOperations.newChange().owner(owner).create();
+    Change.Id changeId = changeOperations.newChange().owner(owner).createV1();
 
     // Add a review.
     requestScopeOperations.setApiUser(reviewer);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
index 68d5a75..2669b31 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
@@ -86,7 +86,7 @@
   @Test
   public void cannotRebaseOnBehalfOfUploaderWithAllowConflicts() throws Exception {
     Account.Id uploader = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(uploader).create();
+    Change.Id changeId = changeOperations.newChange().owner(uploader).createV1();
     RebaseInput rebaseInput = new RebaseInput();
     rebaseInput.onBehalfOfUploader = true;
     rebaseInput.allowConflicts = true;
@@ -102,7 +102,7 @@
   @Test
   public void cannotRebaseOnBehalfOfUploaderWithCommitterEmail() throws Exception {
     Account.Id uploader = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(uploader).create();
+    Change.Id changeId = changeOperations.newChange().owner(uploader).createV1();
     RebaseInput rebaseInput = new RebaseInput();
     rebaseInput.onBehalfOfUploader = true;
     rebaseInput.committerEmail = "admin@example.com";
@@ -140,13 +140,13 @@
         groupOperations.newGroup().name("cannot-upload").addMember(rebaser).create();
     blockPermission(Permission.PUSH, cannotUploadGroup);
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     // Create a chain of changes for being rebased, each change with a different uploader.
     Account.Id uploader1 =
         accountOperations.newAccount().preferredEmail("uploader1@example.com").create();
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader1).create();
+        changeOperations.newChange().project(project).owner(uploader1).createV1();
 
     Account.Id uploader2 =
         accountOperations.newAccount().preferredEmail("uploader2@example.com").create();
@@ -157,7 +157,7 @@
             .childOf()
             .change(changeToBeRebased1)
             .owner(uploader2)
-            .create();
+            .createV1();
 
     Account.Id uploader3 =
         accountOperations.newAccount().preferredEmail("uploader3@example.com").create();
@@ -168,7 +168,7 @@
             .childOf()
             .change(changeToBeRebased2)
             .owner(uploader3)
-            .create();
+            .createV1();
 
     Account.Id uploader4 =
         accountOperations.newAccount().preferredEmail("uploader4@example.com").create();
@@ -179,7 +179,7 @@
             .childOf()
             .change(changeToBeRebased3)
             .owner(uploader4)
-            .create();
+            .createV1();
 
     // Block rebase and submit permission for the uploaders. For rebase on behalf of the uploader
     // only
@@ -231,7 +231,7 @@
     String uploaderEmailOne = "uploader1@example.com";
     Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmailOne).create();
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     Change.Id changeToBeRebased2 =
         changeOperations
@@ -240,7 +240,7 @@
             .childOf()
             .change(changeToBeRebased1)
             .owner(uploader)
-            .create();
+            .createV1();
 
     Change.Id changeToBeRebased3 =
         changeOperations
@@ -249,7 +249,7 @@
             .childOf()
             .change(changeToBeRebased2)
             .owner(uploader)
-            .create();
+            .createV1();
 
     // Change preferred email for the uploader
     String uploaderEmailTwo = "uploader2@example.com";
@@ -257,7 +257,7 @@
 
     // Create, approve and submit the change that will be the new base for the chain that will be
     // rebased
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
     gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
 
@@ -301,13 +301,13 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     // Create a chain of changes for being rebased, each change with a different uploader.
     Account.Id uploader1 =
         accountOperations.newAccount().preferredEmail("uploader1@example.com").create();
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader1).create();
+        changeOperations.newChange().project(project).owner(uploader1).createV1();
 
     Account.Id uploader2 =
         accountOperations.newAccount().preferredEmail("uploader2@example.com").create();
@@ -318,7 +318,7 @@
             .childOf()
             .change(changeToBeRebased1)
             .owner(uploader2)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -336,7 +336,7 @@
 
     // Create and submit another change so that we can rebase the chain once again.
     requestScopeOperations.setApiUser(approver);
-    Change.Id changeToBeTheNewBase2 = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase2 = changeOperations.newChange().project(project).createV1();
     gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
 
@@ -351,9 +351,14 @@
   @Test
   public void nonChangeOwnerWithoutSubmitAndRebasePermissionCannotRebaseChainOnBehalfOfUploader()
       throws Exception {
-    Change.Id changeToBeRebased1 = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeRebased1 = changeOperations.newChange().project(project).createV1();
     Change.Id changeToBeRebased2 =
-        changeOperations.newChange().project(project).childOf().change(changeToBeRebased1).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .childOf()
+            .change(changeToBeRebased1)
+            .createV1();
 
     blockPermissionForAllUsers(Permission.REBASE);
     blockPermissionForAllUsers(Permission.SUBMIT);
@@ -402,10 +407,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -413,7 +418,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -447,10 +452,10 @@
         accountOperations.newAccount().preferredEmail("uploader@example.com").create();
     Account.Id approver = admin.id();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -458,7 +463,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -483,10 +488,10 @@
         accountOperations.newAccount().preferredEmail("uploader@example.com").create();
     Account.Id approver = admin.id();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -494,7 +499,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -533,10 +538,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -544,7 +549,7 @@
             .owner(changeOwner)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Create a second patch set for the second change in the chain that will be rebased so that the
     // uploader is different to the change owner.
@@ -592,10 +597,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -603,7 +608,7 @@
             .owner(changeOwner)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Create a second patch set for the second change in the chain that will be rebased so that the
     // uploader is different to the change owner.
@@ -655,10 +660,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).author(author).create();
+        changeOperations.newChange().project(project).owner(uploader).author(author).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -667,7 +672,7 @@
             .author(author)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -714,10 +719,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).author(author).create();
+        changeOperations.newChange().project(project).owner(uploader).author(author).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -726,7 +731,7 @@
             .author(author)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -769,10 +774,15 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).committer(committer).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .owner(uploader)
+            .committer(committer)
+            .createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -781,7 +791,7 @@
             .committer(committer)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -819,7 +829,7 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
         changeOperations
@@ -827,7 +837,7 @@
             .project(project)
             .owner(uploader)
             .authorIdent(serverIdent.get())
-            .create();
+            .createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -836,7 +846,7 @@
             .authorIdent(serverIdent.get())
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -889,7 +899,7 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
         changeOperations
@@ -897,7 +907,7 @@
             .project(project)
             .owner(uploader)
             .authorIdent(serverIdent.get())
-            .create();
+            .createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -906,7 +916,7 @@
             .authorIdent(serverIdent.get())
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -959,10 +969,10 @@
         groupOperations.newGroup().name("cannot-upload").addMember(rebaser).create();
     blockPermission(Permission.PUSH, cannotUploadGroup);
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -970,7 +980,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain so that the chain is
     // rebasable.
@@ -994,10 +1004,10 @@
     Account.Id changeOwner = accountOperations.newAccount().create();
     Account.Id approver = admin.id();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -1005,7 +1015,7 @@
             .owner(changeOwner)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1035,10 +1045,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     Change.Id changeToBeRebased2 =
         changeOperations
@@ -1047,7 +1057,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1094,11 +1104,11 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Account.Id uploader1 = accountOperations.newAccount().create();
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader1).create();
+        changeOperations.newChange().project(project).owner(uploader1).createV1();
 
     Account.Id uploader2 = accountOperations.newAccount().create();
     Change.Id changeToBeRebased2 =
@@ -1108,7 +1118,7 @@
             .owner(uploader2)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1173,10 +1183,10 @@
     Account.Id rebaser = accountOperations.newAccount().create();
 
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().owner(uploader).project(project).create();
+        changeOperations.newChange().owner(uploader).project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -1184,7 +1194,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1210,7 +1220,7 @@
     // Create and submit another change so that we can rebase the chain once again.
     requestScopeOperations.setApiUser(approver);
     Change.Id changeToBeTheNewBase2 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
 
@@ -1251,10 +1261,10 @@
     Account.Id rebaser = accountOperations.newAccount().create();
 
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().owner(uploader).project(project).create();
+        changeOperations.newChange().owner(uploader).project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -1262,7 +1272,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1299,10 +1309,10 @@
     Account.Id approver = admin.id();
     Account.Id rebaser = accountOperations.newAccount().create();
 
-    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+    Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeToBeRebased1 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased2 =
         changeOperations
             .newChange()
@@ -1310,7 +1320,7 @@
             .owner(uploader)
             .childOf()
             .change(changeToBeRebased1)
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the chain that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1332,7 +1342,7 @@
     // Create and submit another change so that we can rebase the change once again.
     requestScopeOperations.setApiUser(approver);
     Change.Id changeToBeTheNewBase2 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index e1a1f7c..78e5060 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.gerrit.acceptance.TestExtensions.TestCommitValidationListener;
 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.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE;
@@ -24,7 +23,6 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
@@ -39,6 +37,7 @@
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestExtensions.TestCommitValidationListener;
 import com.google.gerrit.acceptance.TestMetricMaker;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -127,6 +126,8 @@
 
     @Test
     public void rebaseChange() throws Exception {
+      RevCommit initialHead = projectOperations.project(project).getHead("master");
+
       // Create two changes both with the same parent
       PushOneCommit.Result r = createChange();
       testRepo.reset("HEAD~1");
@@ -145,13 +146,115 @@
       rebaseCall.call(r2.getChangeId());
 
       verifyRebaseForChange(
-          r2.getChange().getId(), commitThatIsBeingRebased.name(), r.getCommit().name(), true, 2);
+          r2.getChange().getId(),
+          initialHead,
+          commitThatIsBeingRebased.name(),
+          r.getCommit().name(),
+          "recursive",
+          true,
+          2);
 
       // Rebasing the second change again should fail
       verifyChangeIsUpToDate(r2);
     }
 
     @Test
+    public void rebaseChangeOntoTargetBranchWhenParentChangeDeletedHasBeenDeleted()
+        throws Exception {
+      // Create a chain with 2 changes
+      Change.Id changeId1 =
+          changeOperations
+              .newChange()
+              .project(project)
+              .file("a.txt")
+              .content("A content")
+              .createV1();
+      Change.Id changeId2 =
+          changeOperations
+              .newChange()
+              .project(project)
+              .childOf()
+              .change(changeId1)
+              .file("b.txt")
+              .content("B content")
+              .createV1();
+
+      ObjectId change1PatchSetCommit =
+          changeOperations.change(changeId1).currentPatchset().get().commitId();
+
+      // Delete the first change
+      gApi.changes().id(project.get(), changeId1.get()).delete();
+
+      String newBase = projectOperations.project(project).getHead("refs/heads/master").name();
+      String commitThatIsBeingRebased =
+          gApi.changes().id(project.get(), changeId2.get()).get().currentRevision;
+
+      rebaseCall.call(changeId2.toString());
+
+      verifyRebaseForChange(
+          changeId2,
+          change1PatchSetCommit,
+          commitThatIsBeingRebased,
+          newBase,
+          "recursive",
+          /* shouldHaveApproval= */ false,
+          2);
+    }
+
+    @Test
+    public void rebaseChangeOntoOtherChangeWhenParentChangeDeletedHasBeenDeleted()
+        throws Exception {
+      // Create a chain with 2 changes
+      Change.Id changeId1 =
+          changeOperations
+              .newChange()
+              .project(project)
+              .file("a.txt")
+              .content("A content")
+              .createV1();
+      Change.Id changeId2 =
+          changeOperations
+              .newChange()
+              .project(project)
+              .childOf()
+              .change(changeId1)
+              .file("b.txt")
+              .content("B content")
+              .createV1();
+
+      ObjectId change1PatchSetCommit =
+          changeOperations.change(changeId1).currentPatchset().get().commitId();
+
+      // Delete the first change
+      gApi.changes().id(project.get(), changeId1.get()).delete();
+
+      Change.Id changeIdOther =
+          changeOperations
+              .newChange()
+              .project(project)
+              .file("c.txt")
+              .content("C content")
+              .createV1();
+
+      String newBase = gApi.changes().id(project.get(), changeIdOther.get()).get().currentRevision;
+      String commitThatIsBeingRebased =
+          gApi.changes().id(project.get(), changeId2.get()).get().currentRevision;
+
+      RebaseInput rebaseInput = new RebaseInput();
+      rebaseInput.base = changeIdOther.toString();
+      rebaseCallWithInput.call(changeId2.toString(), rebaseInput);
+
+      verifyRebaseForChange(
+          changeId2,
+          change1PatchSetCommit,
+          commitThatIsBeingRebased,
+          newBase,
+          "recursive",
+          /* shouldHaveApproval= */ false,
+          2);
+    }
+
+    @Test
     public void rebaseMerge() throws Exception {
       // Create a new project for this test so that we can configure a copy condition without
       // affecting any other tests. Copy Code-Review approvals if change kind is
@@ -184,7 +287,7 @@
               .branch("master")
               .file(file1)
               .content("base content")
-              .create();
+              .createV1();
       approveAndSubmit(initialChange);
 
       // Create another branch
@@ -202,7 +305,9 @@
               .branch("master")
               .file(file1)
               .content("master content")
-              .create();
+              .createV1();
+      ObjectId baseChangeCommit =
+          changeOperations.change(baseChangeInMaster).currentPatchset().get().commitId();
       approveAndSubmit(baseChangeInMaster);
 
       // Create a change in the other branch and that touches file1 and creates file2.
@@ -215,7 +320,7 @@
               .content("other content")
               .file(file2)
               .content("content")
-              .create();
+              .createV1();
       approveAndSubmit(changeInOtherBranch);
 
       // Create a merge change with a conflict resolution for file1. file2 has the same content as
@@ -233,7 +338,7 @@
               .content("merged content")
               .file(file2)
               .content("content")
-              .create();
+              .createV1();
       String commitThatIsBeingRebased = getCurrentRevision(mergeChangeId);
 
       // Create a change in master onto which the merge change can be rebased. This change touches
@@ -245,7 +350,7 @@
               .branch("master")
               .file(file3)
               .content("other content")
-              .create();
+              .createV1();
       approveAndSubmit(newBaseChangeInMaster);
 
       // Add an approval whose score should be copied on rebase.
@@ -256,9 +361,11 @@
 
       verifyRebaseForChange(
           mergeChangeId,
+          baseChangeCommit,
           commitThatIsBeingRebased,
           ImmutableList.of(
               getCurrentRevision(newBaseChangeInMaster), getCurrentRevision(changeInOtherBranch)),
+          "recursive",
           /* shouldHaveApproval= */ true,
           /* shouldHaveConflicts,= */ false,
           /* expectedNumRevisions= */ 2);
@@ -285,7 +392,7 @@
               .branch("master")
               .file(file1)
               .content("base content")
-              .create();
+              .createV1();
       approveAndSubmit(initialChange);
 
       // Create another branch
@@ -303,7 +410,7 @@
               .branch("master")
               .file(file1)
               .content("master content")
-              .create();
+              .createV1();
       approveAndSubmit(baseChangeInMaster);
 
       // Create a change in the other branch and that touches file1 and creates file2.
@@ -316,7 +423,7 @@
               .content("other content")
               .file(file2)
               .content("content")
-              .create();
+              .createV1();
       approveAndSubmit(changeInOtherBranch);
 
       // Create a merge change with a conflict resolution for file1. file2 has the same content as
@@ -334,7 +441,7 @@
               .content("merged content")
               .file(file2)
               .content("content")
-              .create();
+              .createV1();
 
       // Create a change in master onto which the merge change can be rebased. This change touches
       // file1 again so that there is a conflict on rebase.
@@ -345,7 +452,7 @@
               .branch("master")
               .file(file1)
               .content("conflicting content")
-              .create();
+              .createV1();
       approveAndSubmit(newBaseChangeInMaster);
 
       // Try to rebase the merge change
@@ -356,15 +463,28 @@
           .hasMessageThat()
           .isEqualTo(
               String.format(
-                  "Change %s could not be rebased due to a conflict during merge.\n"
-                      + "\n"
-                      + "merge conflict(s):\n"
-                      + "%s",
+                  """
+                  Change %s could not be rebased due to a conflict during merge.
+
+                  merge conflict(s):
+                  * %s
+                  """,
                   mergeChangeId, file1));
     }
 
     @Test
-    public void rebaseMergeWithConflict_conflictsAllowed() throws Exception {
+    public void rebaseMergeWithConflictWithConflictsAllowed() throws Exception {
+      testRebaseMergeWithConflictWithConflictsAllowed(/* useDiff3= */ false);
+    }
+
+    @Test
+    @GerritConfig(name = "change.diff3ConflictView", value = "true")
+    public void rebaseMergeWithConflictWithConflictsAllowedUsingDiff3() throws Exception {
+      testRebaseMergeWithConflictWithConflictsAllowed(/* useDiff3= */ true);
+    }
+
+    private void testRebaseMergeWithConflictWithConflictsAllowed(boolean useDiff3)
+        throws Exception {
       // Create a new project for this test so that we can configure a copy condition without
       // affecting any other tests. Copy Code-Review approvals if change kind is
       // MERGE_FIRST_PARENT_UPDATE. MERGE_FIRST_PARENT_UPDATE is the change kind when a merge commit
@@ -394,7 +514,7 @@
               .branch("master")
               .file(file)
               .content("base content")
-              .create();
+              .createV1();
       approveAndSubmit(initialChange);
 
       // Create another branch
@@ -405,14 +525,19 @@
       gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
 
       // Create a change in master that touches the file.
+      String baseChangeSubject = "base change";
+      String baseChangeBaseContent = "base content";
       Change.Id baseChangeInMaster =
           changeOperations
               .newChange()
               .project(project)
               .branch("master")
+              .commitMessage(baseChangeSubject)
               .file(file)
-              .content("master content")
-              .create();
+              .content(baseChangeBaseContent)
+              .createV1();
+      ObjectId baseChangeCommit =
+          changeOperations.change(baseChangeInMaster).currentPatchset().get().commitId();
       approveAndSubmit(baseChangeInMaster);
 
       // Create a change in the other branch and that also touches the file.
@@ -423,7 +548,7 @@
               .branch(branchName)
               .file(file)
               .content("other content")
-              .create();
+              .createV1();
       approveAndSubmit(changeInOtherBranch);
 
       // Create a merge change with a conflict resolution.
@@ -441,9 +566,8 @@
               .tipOfBranch(branchName)
               .file(file)
               .content(mergeContent)
-              .create();
+              .createV1();
       String commitThatIsBeingRebased = getCurrentRevision(mergeChangeId);
-      String mergeSha1 = abbreviateName(ObjectId.fromString(commitThatIsBeingRebased), 6);
 
       // Create a change in master onto which the merge change can be rebased. This change touches
       // the file again so that there is a conflict on rebase.
@@ -457,7 +581,7 @@
               .commitMessage(newBaseCommitMessage)
               .file(file)
               .content(newBaseContent)
-              .create();
+              .createV1();
       approveAndSubmit(newBaseChangeInMaster);
 
       // Add an approval whose score should NOT be copied on rebase (since there is a conflict the
@@ -479,28 +603,34 @@
       String baseCommit = getCurrentRevision(newBaseChangeInMaster);
       verifyRebaseForChange(
           mergeChangeId,
+          baseChangeCommit,
           commitThatIsBeingRebased,
           ImmutableList.of(baseCommit, getCurrentRevision(changeInOtherBranch)),
+          "recursive",
           /* shouldHaveApproval= */ false,
           /* shouldHaveConflicts,= */ true,
           /* expectedNumRevisions= */ 2);
 
       // Verify the file contents.
-      String baseSha1 = abbreviateName(ObjectId.fromString(baseCommit), 6);
       assertThat(getFileContent(mergeChangeId, file))
           .isEqualTo(
               "<<<<<<< PATCH SET ("
-                  + mergeSha1
+                  + commitThatIsBeingRebased
                   + " "
                   + mergeCommitMessage
                   + ")\n"
                   + mergeContent
                   + "\n"
+                  + (useDiff3
+                      ? String.format(
+                          "||||||| BASE      (%s %s)\n%s\n",
+                          baseChangeCommit.getName(), baseChangeSubject, baseChangeBaseContent)
+                      : "")
                   + "=======\n"
                   + newBaseContent
                   + "\n"
                   + ">>>>>>> BASE      ("
-                  + baseSha1
+                  + baseCommit
                   + " "
                   + newBaseCommitMessage
                   + ")\n");
@@ -545,7 +675,7 @@
               .branch("master")
               .file(file)
               .content("base content")
-              .create();
+              .createV1();
       approveAndSubmit(initialChange);
 
       // Create another branch
@@ -563,7 +693,9 @@
               .branch("master")
               .file(file)
               .content("master content")
-              .create();
+              .createV1();
+      ObjectId baseChangeCommit =
+          changeOperations.change(baseChangeInMaster).currentPatchset().get().commitId();
       approveAndSubmit(baseChangeInMaster);
 
       // Create a change in the other branch and that also touches the file.
@@ -574,7 +706,7 @@
               .branch(branchName)
               .file(file)
               .content("other content")
-              .create();
+              .createV1();
       approveAndSubmit(changeInOtherBranch);
 
       // Create a merge change with a conflict resolution for the file.
@@ -590,7 +722,7 @@
               .tipOfBranch(branchName)
               .file(file)
               .content(mergeContent)
-              .create();
+              .createV1();
       String commitThatIsBeingRebased = getCurrentRevision(mergeChangeId);
 
       // Create a change in master onto which the merge change can be rebased.  This change touches
@@ -603,7 +735,7 @@
               .branch("master")
               .file(file)
               .content(newBaseContent)
-              .create();
+              .createV1();
       approveAndSubmit(newBaseChangeInMaster);
 
       // Rebase the merge change with setting a merge strategy
@@ -613,9 +745,11 @@
 
       verifyRebaseForChange(
           mergeChangeId,
+          baseChangeCommit,
           commitThatIsBeingRebased,
           ImmutableList.of(
               getCurrentRevision(newBaseChangeInMaster), getCurrentRevision(changeInOtherBranch)),
+          strategy,
           /* shouldHaveApproval= */ false,
           /* shouldHaveConflicts,= */ false,
           /* expectedNumRevisions= */ 2);
@@ -783,8 +917,8 @@
       Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
 
       // Create two changes both with the same parent
-      Change.Id c1 = changeOperations.newChange().project(project).owner(testUser).create();
-      Change.Id c2 = changeOperations.newChange().project(project).owner(testUser).create();
+      Change.Id c1 = changeOperations.newChange().project(project).owner(testUser).createV1();
+      Change.Id c2 = changeOperations.newChange().project(project).owner(testUser).createV1();
 
       // Approve and submit the first change
       gApi.changes().id(c1.get()).current().review(ReviewInput.approve());
@@ -1179,6 +1313,8 @@
 
     @Test
     public void rebaseChangeWhenChecksRefExists() throws Exception {
+      RevCommit initialHead = projectOperations.project(project).getHead("master");
+
       // Create two changes both with the same parent
       PushOneCommit.Result r = createChange();
       testRepo.reset("HEAD~1");
@@ -1202,8 +1338,10 @@
 
       verifyRebaseForChange(
           r2.getChange().getId(),
+          initialHead,
           r2.getCommit().name(),
           r.getCommit().name(),
+          "recursive",
           /* shouldHaveApproval= */ false,
           /* expectedNumRevisions= */ 2);
     }
@@ -1227,25 +1365,31 @@
 
     protected void verifyRebaseForChange(
         Change.Id changeId,
+        ObjectId base,
         String commitThatIsBeingRebased,
         Change.Id baseChangeId,
+        String strategy,
         boolean shouldHaveApproval)
         throws RestApiException {
       verifyRebaseForChange(
-          changeId, commitThatIsBeingRebased, baseChangeId, shouldHaveApproval, 2);
+          changeId, base, commitThatIsBeingRebased, baseChangeId, strategy, shouldHaveApproval, 2);
     }
 
     protected void verifyRebaseForChange(
         Change.Id changeId,
+        ObjectId base,
         String commitThatIsBeingRebased,
         Change.Id baseChangeId,
+        String strategy,
         boolean shouldHaveApproval,
         int expectedNumRevisions)
         throws RestApiException {
       verifyRebaseForChange(
           changeId,
+          base,
           commitThatIsBeingRebased,
           ImmutableList.of(getCurrentRevision(baseChangeId)),
+          strategy,
           shouldHaveApproval,
           /* shouldHaveConflicts,= */ false,
           expectedNumRevisions);
@@ -1253,15 +1397,19 @@
 
     protected void verifyRebaseForChange(
         Change.Id changeId,
+        ObjectId base,
         String commitThatIsBeingRebased,
         String parentCommit,
+        String strategy,
         boolean shouldHaveApproval,
         int expectedNumRevisions)
         throws RestApiException {
       verifyRebaseForChange(
           changeId,
+          base,
           commitThatIsBeingRebased,
           ImmutableList.of(parentCommit),
+          strategy,
           shouldHaveApproval, /* shouldHaveConflicts,= */
           false,
           expectedNumRevisions);
@@ -1269,8 +1417,10 @@
 
     protected void verifyRebaseForChange(
         Change.Id changeId,
+        ObjectId base,
         String commitThatIsBeingRebased,
         List<String> parentCommits,
+        String strategy,
         boolean shouldHaveApproval,
         boolean shouldHaveConflicts,
         int expectedNumRevisions)
@@ -1284,8 +1434,11 @@
 
       // check conflicts info
       assertThat(r.conflicts).isNotNull();
+      assertThat(r.conflicts.base).isEqualTo(base.getName());
       assertThat(r.conflicts.ours).isEqualTo(commitThatIsBeingRebased);
       assertThat(r.conflicts.theirs).isEqualTo(parentCommits.get(0));
+      assertThat(r.conflicts.mergeStrategy).isEqualTo(strategy);
+      assertThat(r.conflicts.noBaseReason).isNull();
       assertThat(r.conflicts.containsConflicts).isEqualTo(shouldHaveConflicts);
 
       // ...and the parent should be correct
@@ -1376,6 +1529,8 @@
       String baseContent = "base content";
       String expectedContent = strategy.equals("theirs") ? baseContent : patchSetContent;
 
+      RevCommit initialHead = projectOperations.project(project).getHead("master");
+
       PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
       gApi.changes()
           .id(r1.getChangeId())
@@ -1425,8 +1580,11 @@
       RevisionInfo currentRevision = changeInfo.getCurrentRevision();
       assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(base.name());
       assertThat(currentRevision.conflicts).isNotNull();
+      assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
       assertThat(currentRevision.conflicts.ours).isEqualTo(patchSet.name());
       assertThat(currentRevision.conflicts.theirs).isEqualTo(base.name());
+      assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+      assertThat(currentRevision.conflicts.noBaseReason).isNull();
       assertThat(currentRevision.conflicts.containsConflicts).isFalse();
 
       // Verify that the file content in the created patch set is correct.
@@ -1455,12 +1613,24 @@
     }
 
     @Test
-    public void rebaseWithConflict_conflictsAllowed() throws Exception {
+    public void rebaseWithConflictWithCnflictsAllowed() throws Exception {
+      testRebaseWithConflictWithConflictsAllowed(/* useDiff3= */ false);
+    }
+
+    @Test
+    @GerritConfig(name = "change.diff3ConflictView", value = "true")
+    public void rebaseWithConflictWithConflictsAllowedUsingDiff3() throws Exception {
+      testRebaseWithConflictWithConflictsAllowed(/* useDiff3= */ true);
+    }
+
+    private void testRebaseWithConflictWithConflictsAllowed(boolean useDiff3) throws Exception {
       String patchSetSubject = "patch set change";
       String patchSetContent = "patch set content";
       String baseSubject = "base change";
       String baseContent = "base content";
 
+      RevCommit initialHead = projectOperations.project(project).getHead("master");
+
       PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
       gApi.changes()
           .id(r1.getChangeId())
@@ -1510,8 +1680,11 @@
       RevisionInfo currentRevision = changeInfo.getCurrentRevision();
       assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(base.name());
       assertThat(currentRevision.conflicts).isNotNull();
+      assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
       assertThat(currentRevision.conflicts.ours).isEqualTo(patchSet.name());
       assertThat(currentRevision.conflicts.theirs).isEqualTo(base.name());
+      assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+      assertThat(currentRevision.conflicts.noBaseReason).isNull();
       assertThat(currentRevision.conflicts.containsConflicts).isTrue();
 
       // Verify that the file content in the created patch set is correct.
@@ -1521,22 +1694,25 @@
       ByteArrayOutputStream os = new ByteArrayOutputStream();
       bin.writeTo(os);
       String fileContent = new String(os.toByteArray(), UTF_8);
-      String patchSetSha1 = abbreviateName(patchSet, 6);
-      String baseSha1 = abbreviateName(base, 6);
       assertThat(fileContent)
           .isEqualTo(
               "<<<<<<< PATCH SET ("
-                  + patchSetSha1
+                  + patchSet.getName()
                   + " "
                   + patchSetSubject
                   + ")\n"
                   + patchSetContent
                   + "\n"
+                  + (useDiff3
+                      ? String.format(
+                          "||||||| BASE      (%s %s)\n",
+                          initialHead.getName(), initialHead.getShortMessage())
+                      : "")
                   + "=======\n"
                   + baseContent
                   + "\n"
                   + ">>>>>>> BASE      ("
-                  + baseSha1
+                  + base.getName()
                   + " "
                   + baseSubject
                   + ")\n");
@@ -1578,8 +1754,12 @@
           .hasMessageThat()
           .isEqualTo(
               String.format(
-                  "Change %s could not be rebased due to a conflict during merge.\n\n"
-                      + "merge conflict(s):\n%s",
+                  """
+                  Change %s could not be rebased due to a conflict during merge.
+
+                  merge conflict(s):
+                  * %s
+                  """,
                   r2.getChange().getId(), PushOneCommit.FILE_NAME));
     }
 
@@ -1593,14 +1773,14 @@
               "add merge=union to gitattributes",
               ".gitattributes",
               "*.txt merge=union");
-      PushOneCommit.Result unusedResult = pushAttributes.to("refs/heads/master");
+      PushOneCommit.Result r1 = pushAttributes.to("refs/heads/master");
 
-      PushOneCommit.Result r1 = createChange();
+      PushOneCommit.Result r2 = createChange();
       gApi.changes()
-          .id(r1.getChangeId())
-          .revision(r1.getCommit().name())
+          .id(r2.getChangeId())
+          .revision(r2.getCommit().name())
           .review(ReviewInput.approve());
-      gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+      gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).submit();
 
       PushOneCommit push =
           pushFactory.create(
@@ -1610,11 +1790,11 @@
               PushOneCommit.FILE_NAME,
               "other content",
               "I3bf2c82554e83abc759154e85db94c7ebb079c70");
-      PushOneCommit.Result r2 = push.to("refs/for/master");
-      r2.assertOkStatus();
-      String changeId = r2.getChangeId();
-      RevCommit patchSet = r2.getCommit();
-      RevCommit base = r1.getCommit();
+      PushOneCommit.Result r3 = push.to("refs/for/master");
+      r3.assertOkStatus();
+      String changeId = r3.getChangeId();
+      RevCommit patchSet = r3.getCommit();
+      RevCommit base = r2.getCommit();
       RebaseInput rebaseInput = new RebaseInput();
       rebaseInput.strategy = "recursive";
       ChangeInfo changeInfo =
@@ -1625,8 +1805,11 @@
       RevisionInfo currentRevision =
           gApi.changes().id(changeId).get(CURRENT_REVISION).getCurrentRevision();
       assertThat(currentRevision.conflicts).isNotNull();
+      assertThat(currentRevision.conflicts.base).isEqualTo(r1.getCommit().name());
       assertThat(currentRevision.conflicts.ours).isEqualTo(patchSet.name());
       assertThat(currentRevision.conflicts.theirs).isEqualTo(base.name());
+      assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+      assertThat(currentRevision.conflicts.noBaseReason).isNull();
       assertThat(currentRevision.conflicts.containsConflicts).isFalse();
 
       // Verify that the file content in the created patch set is correct.
@@ -1690,8 +1873,8 @@
 
     @Test
     public void rebaseActionEnabledIfChangeCanBeRebased() throws Exception {
-      Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
-      Change.Id changeToBeRebased = changeOperations.newChange().project(project).create();
+      Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
+      Change.Id changeToBeRebased = changeOperations.newChange().project(project).createV1();
 
       // Change cannot be rebased since its parent commit is the same commit as the HEAD of the
       // destination branch.
@@ -1716,9 +1899,9 @@
 
     @Test
     public void rebaseActionEnabledIfChangeHasAParentChange() throws Exception {
-      Change.Id change1 = changeOperations.newChange().project(project).create();
+      Change.Id change1 = changeOperations.newChange().project(project).createV1();
       Change.Id change2 =
-          changeOperations.newChange().project(project).childOf().change(change1).create();
+          changeOperations.newChange().project(project).childOf().change(change1).createV1();
 
       // change1 cannot be rebased since its parent commit is the same commit as the HEAD of the
       // destination branch.
@@ -1911,9 +2094,13 @@
       //         *r5
       PushOneCommit.Result r = createChange();
       testRepo.reset("HEAD~1");
+      RevCommit head = projectOperations.project(project).getHead("master");
       PushOneCommit.Result r2 = createChange();
+      String r2PatchSet1 = getCurrentRevision(r2.getChange().getId());
       PushOneCommit.Result r3 = createChange();
+      String r3PatchSet1 = getCurrentRevision(r3.getChange().getId());
       PushOneCommit.Result r4 = createChange();
+      String r4PatchSet1 = getCurrentRevision(r4.getChange().getId());
       PushOneCommit.Result r5 = createChange();
 
       // Approve and submit the first change
@@ -1931,11 +2118,27 @@
 
       // Only r2, r3 and r4 are rebased.
       verifyRebaseForChange(
-          r2.getChange().getId(), r2.getCommit().name(), r.getCommit().name(), true, 2);
+          r2.getChange().getId(),
+          head,
+          r2.getCommit().name(),
+          r.getCommit().name(),
+          "recursive",
+          true,
+          2);
       verifyRebaseForChange(
-          r3.getChange().getId(), r3.getCommit().name(), r2.getChange().getId(), true);
+          r3.getChange().getId(),
+          ObjectId.fromString(r2PatchSet1),
+          r3.getCommit().name(),
+          r2.getChange().getId(),
+          "recursive",
+          true);
       verifyRebaseForChange(
-          r4.getChange().getId(), r4.getCommit().name(), r3.getChange().getId(), false);
+          r4.getChange().getId(),
+          ObjectId.fromString(r3PatchSet1),
+          r4.getCommit().name(),
+          r3.getChange().getId(),
+          "recursive",
+          false);
 
       verifyChangeIsUpToDate(r2);
       verifyChangeIsUpToDate(r3);
@@ -1955,7 +2158,12 @@
           gApi.changes().id(r5.getChangeId()).rebaseChain(), false, r2, r3, r4, r5);
 
       verifyRebaseForChange(
-          r5.getChange().getId(), r5.getCommit().name(), r4.getChange().getId(), false);
+          r5.getChange().getId(),
+          ObjectId.fromString(r4PatchSet1),
+          r5.getCommit().name(),
+          r4.getChange().getId(),
+          "recursive",
+          false);
     }
 
     @Test
@@ -1971,7 +2179,7 @@
               .branch("master")
               .file(file1)
               .content("base content")
-              .create();
+              .createV1();
       approveAndSubmit(initialChange);
 
       // Create another branch
@@ -1989,7 +2197,7 @@
               .branch("master")
               .file(file1)
               .content("master content")
-              .create();
+              .createV1();
       approveAndSubmit(baseChangeInMaster);
 
       // Create a change in the other branch and that also touches file1.
@@ -2000,9 +2208,11 @@
               .branch(branchName)
               .file(file1)
               .content("other content")
-              .create();
+              .createV1();
       approveAndSubmit(changeInOtherBranch);
 
+      RevCommit head = projectOperations.project(project).getHead("master");
+
       // Create a merge change with a conflict resolution.
       Change.Id mergeChangeId =
           changeOperations
@@ -2015,7 +2225,7 @@
               .tipOfBranch(branchName)
               .file(file1)
               .content("merged content")
-              .create();
+              .createV1();
       String mergeCommitThatIsBeingRebased = getCurrentRevision(mergeChangeId);
 
       // Create a follow up change.
@@ -2028,7 +2238,7 @@
               .change(mergeChangeId)
               .file(file1)
               .content("modified content")
-              .create();
+              .createV1();
       String followUpCommitThatIsBeingRebased = getCurrentRevision(followUpChangeId);
 
       // Create another change in the other branch so that we can create another merge
@@ -2039,7 +2249,7 @@
               .branch(branchName)
               .file(file1)
               .content("yet another content")
-              .create();
+              .createV1();
       approveAndSubmit(anotherChangeInOtherBranch);
 
       // Create a second merge change with a conflict resolution.
@@ -2056,7 +2266,7 @@
               .tipOfBranch(branchName)
               .file(file1)
               .content("another merged content")
-              .create();
+              .createV1();
       String followUpMergeCommitThatIsBeingRebased = getCurrentRevision(followUpMergeChangeId);
 
       // Create a change in master onto which the chain can be rebased. This change touches an
@@ -2068,7 +2278,7 @@
               .branch("master")
               .file(file2)
               .content("other content")
-              .create();
+              .createV1();
       approveAndSubmit(newBaseChangeInMaster);
 
       // Rebase the chain
@@ -2079,24 +2289,30 @@
 
       verifyRebaseForChange(
           mergeChangeId,
+          head,
           mergeCommitThatIsBeingRebased,
           ImmutableList.of(
               getCurrentRevision(newBaseChangeInMaster), getCurrentRevision(changeInOtherBranch)),
+          "recursive",
           /* shouldHaveApproval= */ false,
           /* shouldHaveConflicts,= */ false,
           /* expectedNumRevisions= */ 2);
       verifyRebaseForChange(
           followUpChangeId,
+          ObjectId.fromString(mergeCommitThatIsBeingRebased),
           followUpCommitThatIsBeingRebased,
           ImmutableList.of(getCurrentRevision(mergeChangeId)),
+          "recursive",
           /* shouldHaveApproval= */ false,
           /* shouldHaveConflicts,= */ false,
           /* expectedNumRevisions= */ 2);
       verifyRebaseForChange(
           followUpMergeChangeId,
+          ObjectId.fromString(followUpCommitThatIsBeingRebased),
           followUpMergeCommitThatIsBeingRebased,
           ImmutableList.of(
               getCurrentRevision(followUpChangeId), getCurrentRevision(anotherChangeInOtherBranch)),
+          "recursive",
           /* shouldHaveApproval= */ false,
           /* shouldHaveConflicts,= */ false,
           /* expectedNumRevisions= */ 2);
@@ -2126,8 +2342,11 @@
       //       * r4
       PushOneCommit.Result r = createChange();
       testRepo.reset("HEAD~1");
+      RevCommit head = projectOperations.project(project).getHead("master");
       PushOneCommit.Result r2 = createChange();
+      String r2PatchSet1 = getCurrentRevision(r2.getChange().getId());
       PushOneCommit.Result r3 = createChange("original patch-set", file, oldContent);
+      String r3PatchSet1 = getCurrentRevision(r3.getChange().getId());
       PushOneCommit.Result r4 = createChange();
       gApi.changes()
           .id(r3.getChangeId())
@@ -2145,10 +2364,28 @@
       rebaseCall.call(r4.getChangeId());
 
       verifyRebaseForChange(
-          r2.getChange().getId(), r2.getCommit().name(), r.getCommit().name(), false, 2);
-      verifyRebaseForChange(r3.getChange().getId(), r3PatchSet2, r2.getChange().getId(), false, 3);
+          r2.getChange().getId(),
+          head,
+          r2.getCommit().name(),
+          r.getCommit().name(),
+          "recursive",
+          false,
+          2);
       verifyRebaseForChange(
-          r4.getChange().getId(), r4.getCommit().name(), r3.getChange().getId(), false);
+          r3.getChange().getId(),
+          ObjectId.fromString(r2PatchSet1),
+          r3PatchSet2,
+          r2.getChange().getId(),
+          "recursive",
+          false,
+          3);
+      verifyRebaseForChange(
+          r4.getChange().getId(),
+          ObjectId.fromString(r3PatchSet1),
+          r4.getCommit().name(),
+          r3.getChange().getId(),
+          "recursive",
+          false);
 
       assertThat(gApi.changes().id(r3.getChangeId()).current().file(file).content().asString())
           .isEqualTo(newContent);
@@ -2172,8 +2409,11 @@
       //         *r5
       PushOneCommit.Result r = createChange();
       PushOneCommit.Result r2 = createChange();
+      String r2PatchSet1 = getCurrentRevision(r2.getChange().getId());
       PushOneCommit.Result r3 = createChange();
+      String r3PatchSet1 = getCurrentRevision(r3.getChange().getId());
       PushOneCommit.Result r4 = createChange();
+      String r4PatchSet1 = getCurrentRevision(r4.getChange().getId());
       PushOneCommit.Result r5 = createChange();
 
       // Approve and submit the first change
@@ -2201,9 +2441,19 @@
 
       // Only r3 and r4 are rebased.
       verifyRebaseForChange(
-          r3.getChange().getId(), r3.getCommit().name(), r2.getChange().getId(), true);
+          r3.getChange().getId(),
+          ObjectId.fromString(r2PatchSet1),
+          r3.getCommit().name(),
+          r2.getChange().getId(),
+          "recursive",
+          true);
       verifyRebaseForChange(
-          r4.getChange().getId(), r4.getCommit().name(), r3.getChange().getId(), false);
+          r4.getChange().getId(),
+          ObjectId.fromString(r3PatchSet1),
+          r4.getCommit().name(),
+          r3.getChange().getId(),
+          "recursive",
+          false);
 
       verifyChangeIsUpToDate(r2);
       verifyChangeIsUpToDate(r3);
@@ -2223,7 +2473,12 @@
           gApi.changes().id(r5.getChangeId()).rebaseChain(), false, r3, r4, r5);
 
       verifyRebaseForChange(
-          r5.getChange().getId(), r5.getCommit().name(), r4.getChange().getId(), false);
+          r5.getChange().getId(),
+          ObjectId.fromString(r4PatchSet1),
+          r5.getCommit().name(),
+          r4.getChange().getId(),
+          "recursive",
+          false);
     }
 
     @Test
@@ -2255,18 +2510,34 @@
           .hasMessageThat()
           .isEqualTo(
               String.format(
-                  "Change %s could not be rebased due to a conflict during merge.\n\n"
-                      + "merge conflict(s):\n%s",
+                  """
+                  Change %s could not be rebased due to a conflict during merge.
+
+                  merge conflict(s):
+                  * %s
+                  """,
                   r2.getChange().getId(), PushOneCommit.FILE_NAME));
     }
 
     @Test
-    public void rebaseChainWithConflicts_conflictsAllowed() throws Exception {
+    public void rebaseChainWithConflictsConflictsAllowed() throws Exception {
+      testRebaseChainWithConflictsConflictsAllowed(/* useDiff3= */ false);
+    }
+
+    @Test
+    @GerritConfig(name = "change.diff3ConflictView", value = "true")
+    public void rebaseChainWithConflictsConflictsAllowedUsingDiff3() throws Exception {
+      testRebaseChainWithConflictsConflictsAllowed(/* useDiff3= */ true);
+    }
+
+    private void testRebaseChainWithConflictsConflictsAllowed(boolean useDiff3) throws Exception {
       String patchSetSubject = "patch set change";
       String patchSetContent = "patch set content";
       String baseSubject = "base change";
       String baseContent = "base content";
 
+      RevCommit initialHead = projectOperations.project(project).getHead("master");
+
       PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
       gApi.changes()
           .id(r1.getChangeId())
@@ -2311,8 +2582,11 @@
         RevisionInfo parentChangeCurrentRevision = parentChangeInfo.getCurrentRevision();
         assertThat(parentChangeCurrentRevision.commit.parents.get(0).commit).isEqualTo(base.name());
         assertThat(parentChangeCurrentRevision.conflicts).isNotNull();
+        assertThat(parentChangeCurrentRevision.conflicts.base).isEqualTo(initialHead.name());
         assertThat(parentChangeCurrentRevision.conflicts.ours).isEqualTo(parentPatchSet.name());
         assertThat(parentChangeCurrentRevision.conflicts.theirs).isEqualTo(base.name());
+        assertThat(parentChangeCurrentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+        assertThat(parentChangeCurrentRevision.conflicts.noBaseReason).isNull();
         assertThat(parentChangeCurrentRevision.conflicts.containsConflicts).isTrue();
 
         ChangeInfo childChangeInfo = rebaseChainInfo.rebasedChanges.get(1);
@@ -2324,9 +2598,12 @@
         assertThat(childChangeCurrentRevision.commit.parents.get(0).commit)
             .isEqualTo(parentChangeCurrentRevision.commit.commit);
         assertThat(childChangeCurrentRevision.conflicts).isNotNull();
+        assertThat(childChangeCurrentRevision.conflicts.base).isEqualTo(parentPatchSet.name());
         assertThat(childChangeCurrentRevision.conflicts.ours).isEqualTo(childPatchSet.name());
         assertThat(childChangeCurrentRevision.conflicts.theirs)
             .isEqualTo(parentChangeCurrentRevision.commit.commit);
+        assertThat(childChangeCurrentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+        assertThat(childChangeCurrentRevision.conflicts.noBaseReason).isNull();
         assertThat(childChangeCurrentRevision.conflicts.containsConflicts).isTrue();
       }
       assertThat(wipStateChangedListener.invoked).isTrue();
@@ -2348,22 +2625,25 @@
       ByteArrayOutputStream os = new ByteArrayOutputStream();
       bin.writeTo(os);
       String fileContent = new String(os.toByteArray(), UTF_8);
-      String patchSetSha1 = abbreviateName(parentPatchSet, 6);
-      String baseSha1 = abbreviateName(base, 6);
       assertThat(fileContent)
           .isEqualTo(
               "<<<<<<< PATCH SET ("
-                  + patchSetSha1
+                  + parentPatchSet.getName()
                   + " "
                   + patchSetSubject
                   + ")\n"
                   + patchSetContent
                   + "\n"
+                  + (useDiff3
+                      ? String.format(
+                          "||||||| BASE      (%s %s)\n",
+                          initialHead.getName(), initialHead.getShortMessage())
+                      : "")
                   + "=======\n"
                   + baseContent
                   + "\n"
                   + ">>>>>>> BASE      ("
-                  + baseSha1
+                  + base.getName()
                   + " "
                   + baseSubject
                   + ")\n");
@@ -2408,16 +2688,16 @@
 
     @Test
     public void rebaseChainActionEnabled() throws Exception {
-      Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+      Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).createV1();
 
-      Change.Id changeToBeRebased1 = changeOperations.newChange().project(project).create();
+      Change.Id changeToBeRebased1 = changeOperations.newChange().project(project).createV1();
       Change.Id changeToBeRebased2 =
           changeOperations
               .newChange()
               .project(project)
               .childOf()
               .change(changeToBeRebased1)
-              .create();
+              .createV1();
 
       // Approve and submit the change that will be the new base for the chain so that the chain is
       // rebasable.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index c6d74d1..c07521f 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -89,7 +89,7 @@
   @Test
   public void cannotRebaseOnBehalfOfUploaderWithAllowConflicts() throws Exception {
     Account.Id uploader = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(uploader).create();
+    Change.Id changeId = changeOperations.newChange().owner(uploader).createV1();
     RebaseInput rebaseInput = new RebaseInput();
     rebaseInput.onBehalfOfUploader = true;
     rebaseInput.allowConflicts = true;
@@ -124,9 +124,9 @@
     // Create two changes both with the same parent.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -170,9 +170,9 @@
     // Create two changes both with the same parent.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -201,7 +201,7 @@
   @Test
   public void cannotRebaseNonCurrentPatchSetOnBehalfOfUploader() throws Exception {
     Account.Id uploader = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(uploader).create();
+    Change.Id changeId = changeOperations.newChange().owner(uploader).createV1();
     changeOperations.change(changeId).newPatchset().create();
     RebaseInput rebaseInput = new RebaseInput();
     rebaseInput.onBehalfOfUploader = true;
@@ -275,9 +275,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(changeOwner);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
 
     // Create a second patch set for the change that will be rebased so that the uploader is
     // different to the change owner. This is to verify that being change owner doesn't matter for
@@ -360,9 +360,9 @@
 
     // Create two changes both with the same parent
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
@@ -399,9 +399,9 @@
     // Create two changes both with the same parent.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -425,7 +425,7 @@
     // Create and submit another change so that we can rebase the change once again.
     requestScopeOperations.setApiUser(approver);
     Change.Id changeToBeTheNewBase2 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
 
@@ -444,7 +444,7 @@
     // Create and submit another change so that we can rebase the change once again.
     requestScopeOperations.setApiUser(approver);
     Change.Id changeToBeTheNewBase3 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     gApi.changes().id(changeToBeTheNewBase3.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase3.get()).current().submit();
 
@@ -465,7 +465,7 @@
   @Test
   public void nonChangeOwnerWithoutSubmitAndRebasePermissionCannotRebaseOnBehalfOfUploader()
       throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
 
     blockPermissionForAllUsers(Permission.REBASE);
     blockPermissionForAllUsers(Permission.SUBMIT);
@@ -516,9 +516,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -556,9 +556,9 @@
     // rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -591,9 +591,9 @@
     // rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -636,9 +636,9 @@
     // rebased.
     requestScopeOperations.setApiUser(changeOwner);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
 
     // Create a second patch set for the change that will be rebased so that the uploader is
     // different to the change owner.
@@ -694,9 +694,9 @@
     // rebased.
     requestScopeOperations.setApiUser(changeOwner);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
 
     // Create a second patch set for the change that will be rebased so that the uploader is
     // different to the change owner.
@@ -752,9 +752,9 @@
     // rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).author(author).create();
+        changeOperations.newChange().project(project).owner(uploader).author(author).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -798,9 +798,9 @@
     // rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).author(author).create();
+        changeOperations.newChange().project(project).owner(uploader).author(author).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -847,9 +847,14 @@
     // rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).committer(committer).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .owner(uploader)
+            .committer(committer)
+            .createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -884,14 +889,14 @@
     // change that will be rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
         changeOperations
             .newChange()
             .project(project)
             .owner(uploader)
             .authorIdent(serverIdent.get())
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -940,14 +945,14 @@
     // change that will be rebased.
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
         changeOperations
             .newChange()
             .project(project)
             .owner(uploader)
             .authorIdent(serverIdent.get())
-            .create();
+            .createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1003,9 +1008,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1032,9 +1037,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(changeOwner);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(changeOwner).create();
+        changeOperations.newChange().project(project).owner(changeOwner).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1067,9 +1072,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1126,9 +1131,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1152,7 +1157,7 @@
     // Create and submit another change so that we can rebase the change once again.
     requestScopeOperations.setApiUser(approver);
     Change.Id changeToBeTheNewBase2 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
 
@@ -1207,9 +1212,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1283,9 +1288,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1332,9 +1337,9 @@
     // Create two changes both with the same parent
     requestScopeOperations.setApiUser(uploader);
     Change.Id changeToBeTheNewBase =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     Change.Id changeToBeRebased =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
 
     // Approve and submit the change that will be the new base for the change that will be rebased.
     requestScopeOperations.setApiUser(approver);
@@ -1356,7 +1361,7 @@
     // Create and submit another change so that we can rebase the change once again.
     requestScopeOperations.setApiUser(approver);
     Change.Id changeToBeTheNewBase2 =
-        changeOperations.newChange().project(project).owner(uploader).create();
+        changeOperations.newChange().project(project).owner(uploader).createV1();
     gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve());
     gApi.changes().id(changeToBeTheNewBase2.get()).current().submit();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 6d8cc3d..b3b56db 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -25,10 +25,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AbstractDaemonTest.ProjectConfigUpdate;
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestExtensions.TestCommitValidationListener;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -47,6 +49,7 @@
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.common.RevertSubmissionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -208,8 +211,12 @@
 
     assertThat(revertChange.getCurrentRevision().conflicts).isNotNull();
     assertThat(revertChange.getCurrentRevision().conflicts.containsConflicts).isFalse();
+    assertThat(revertChange.getCurrentRevision().conflicts.base).isNull();
     assertThat(revertChange.getCurrentRevision().conflicts.ours).isNull();
     assertThat(revertChange.getCurrentRevision().conflicts.theirs).isNull();
+    assertThat(revertChange.getCurrentRevision().conflicts.mergeStrategy).isNull();
+    assertThat(revertChange.getCurrentRevision().conflicts.noBaseReason)
+        .isEqualTo(NoMergeBaseReason.NO_MERGE_PERFORMED);
   }
 
   @Test
@@ -1016,8 +1023,12 @@
     assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts).isNotNull();
     assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts.containsConflicts)
         .isFalse();
+    assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts.base).isNull();
     assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts.ours).isNull();
     assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts.theirs).isNull();
+    assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts.mergeStrategy).isNull();
+    assertThat(revertChanges.get(0).get().getCurrentRevision().conflicts.noBaseReason)
+        .isEqualTo(NoMergeBaseReason.NO_MERGE_PERFORMED);
   }
 
   @Test
@@ -1614,6 +1625,227 @@
     assertThat(firstRevertChanges.get(3).get().subject).isEqualTo("Revert \"Revert^934\"");
   }
 
+  @Test
+  public void revertChangeWithBugAndIssueFooters_andDefaultMessageSet() throws Exception {
+    // Create a change with bug and issue footers
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Change with bug and issue\n" + "Bug: 12345\n" + "Issue: 67890",
+            "a.txt",
+            "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // Revert the change
+    RevertInput revertInput = new RevertInput();
+    revertInput.message = "Reverting this change because it broke tests";
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+
+    // Check that the revert commit message contains the footers
+    String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+    assertThat(commitMessage).contains("Bug: 12345");
+    assertThat(commitMessage).contains("Issue: 67890");
+  }
+
+  @Test
+  public void revertChangeWithBugAndIssueFooters_andNoDefaultMessageSet() throws Exception {
+    // Create a change with bug and issue footers
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Change with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+            "a.txt",
+            "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // Revert the change
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get();
+
+    // Check that the revert commit message contains the footers
+    String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+    assertThat(commitMessage).contains("Bug: 12345");
+    assertThat(commitMessage).contains("Issue: 67890");
+  }
+
+  @Test
+  public void revertSubmissionWithBugAndIssueFooters() throws Exception {
+    String topic = "revert-with-footers";
+
+    // Create change 1
+    PushOneCommit.Result r1 =
+        createChange(
+            "Change 1 with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+            "a.txt",
+            "content1");
+    gApi.changes().id(r1.getChangeId()).topic(topic);
+
+    // Create change 2
+    PushOneCommit.Result r2 =
+        createChange(
+            "Change 2 with bug and issue\n" + "\n" + "Bug: 54321\n" + "Issue: 98765",
+            "b.txt",
+            "content2");
+    gApi.changes().id(r2.getChangeId()).topic(topic);
+
+    // Approve and submit
+    approve(r1.getChangeId());
+    approve(r2.getChangeId());
+    gApi.changes().id(r2.getChangeId()).current().submit();
+
+    // Revert submission
+    List<ChangeInfo> revertChanges =
+        gApi.changes().id(r1.getChangeId()).revertSubmission().revertChanges;
+    assertThat(revertChanges).hasSize(2);
+
+    // The order of revertChanges is not guaranteed, so find the reverts by original change number.
+    ChangeInfo revert1 =
+        revertChanges.stream()
+            .filter(c -> c.revertOf == r1.getChange().change().getChangeId())
+            .findFirst()
+            .get();
+    ChangeInfo revert2 =
+        revertChanges.stream()
+            .filter(c -> c.revertOf == r2.getChange().change().getChangeId())
+            .findFirst()
+            .get();
+
+    // Check revert of change 1
+    String commitMessage1 = gApi.changes().id(revert1.id).current().commit(false).message;
+    assertThat(commitMessage1).contains("Bug: 12345");
+    assertThat(commitMessage1).contains("Issue: 67890");
+    assertThat(commitMessage1).doesNotContain("Bug: 54321");
+    assertThat(commitMessage1).doesNotContain("Issue: 98765");
+
+    // Check revert of change 2
+    String commitMessage2 = gApi.changes().id(revert2.id).current().commit(false).message;
+    assertThat(commitMessage2).contains("Bug: 54321");
+    assertThat(commitMessage2).contains("Issue: 98765");
+    assertThat(commitMessage2).doesNotContain("Bug: 12345");
+    assertThat(commitMessage2).doesNotContain("Issue: 67890");
+  }
+
+  @Test
+  public void revertChangeWithExistingFooters() throws Exception {
+    // Create a change with bug and issue footers
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Change with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+            "a.txt",
+            "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // Revert the change with a message that already contains the footers
+    RevertInput revertInput = new RevertInput();
+    revertInput.message = "Reverting this change\n\nBug: 12345\nIssue: 67890";
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+
+    // Check that the footers are not duplicated
+    String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+    String expectedMessage =
+        String.format(
+            """
+            Reverting this change
+
+            Bug: 12345
+            Issue: 67890
+            Change-Id: %s
+            """,
+            revertChange.changeId);
+    assertThat(commitMessage).isEqualTo(expectedMessage);
+  }
+
+  @Test
+  public void revertChangeWithSkipPresubmitFooter() throws Exception {
+    // Create a change with bug and issue footers
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Change with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+            "a.txt",
+            "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // Revert the change
+    RevertInput revertInput = new RevertInput();
+    revertInput.message = "Reverting this change\n\nSkip-Presubmit: true";
+    ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+
+    // Check that the revert commit message contains all footers and no extra newline
+    String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+    String expectedMessage =
+        String.format(
+            """
+            Reverting this change
+
+            Skip-Presubmit: true
+            Bug: 12345
+            Issue: 67890
+            Change-Id: %s
+            """,
+            revertChange.changeId);
+    assertThat(commitMessage).isEqualTo(expectedMessage);
+  }
+
+  @Test
+  public void revertSubmissionWithExistingFooters() throws Exception {
+    // Create a change with bug and issue footers
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Change with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+            "a.txt",
+            "content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+    gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+    // Revert the submission with a message that already contains the footers
+    List<ChangeInfo> revertChanges =
+        gApi.changes().id(r.getChangeId()).revertSubmission().revertChanges;
+    assertThat(revertChanges).hasSize(1);
+    ChangeInfo revertChange = revertChanges.get(0);
+
+    // Check that the footers are not duplicated
+    String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+    String expectedMessage =
+        String.format(
+            """
+            Revert "Change with bug and issue"
+
+            This reverts commit %s.
+
+            Bug: 12345
+            Issue: 67890
+            Change-Id: %s
+            """,
+            r.getCommit().name(), revertChange.changeId);
+    assertThat(commitMessage).isEqualTo(expectedMessage);
+  }
+
   @Override
   protected PushOneCommit.Result createChange() throws Exception {
     return createChange("refs/for/master");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 6dea5be..a86a16d 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -357,7 +357,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -387,11 +387,11 @@
             .project(project)
             .file(existingFile)
             .content("content")
-            .create();
+            .createV1();
     vote(admin, prep.toString(), 2, 1);
     gApi.changes().id(prep.get()).current().submit();
 
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -415,7 +415,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -433,7 +433,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -488,7 +488,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -508,7 +508,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -532,7 +532,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -549,7 +549,7 @@
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     vote(admin, changeId.toString(), 2, 1);
     vote(user, changeId.toString(), -2, -1);
 
@@ -601,9 +601,9 @@
   public void stickyIfFilesUnchanged_magicFilesAreIgnored() throws Exception {
     updateCodeReviewLabel(b -> b.setCopyCondition("has:unchanged-files"));
 
-    Change.Id parent1ChangeId = changeOperations.newChange().project(project).create();
-    Change.Id parent2ChangeId = changeOperations.newChange().project(project).create();
-    Change.Id dummyParentChangeId = changeOperations.newChange().project(project).create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id dummyParentChangeId = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
         changeOperations
             .newChange()
@@ -612,7 +612,7 @@
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     // The change is for a merge commit. It doesn't touch any files, but contains /COMMIT_MSG and
     // /MERGE_LIST as magic files.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
index a29244f..822807e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
@@ -26,6 +26,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -35,27 +36,29 @@
 import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
-import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.server.account.ServiceUserClassifier;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
+import java.util.Map;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
@@ -136,7 +139,7 @@
             .newChange()
             .commitMessage("Hello Earth, from planet Mars")
             .project(project)
-            .create();
+            .createV1();
     // The punctuation and capitalisation is ignored.
     assertMatching("message:\"earth from planet\"", c1);
     // The punctuation and capitalisation is ignored.
@@ -145,7 +148,7 @@
 
   @Test
   public void distinctVoters_sameUserVotesOnDifferentLabels_fails() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(admin.id());
     approve(c1.toString());
     assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MAX,count>1\"", c1);
@@ -160,7 +163,7 @@
 
   @Test
   public void distinctVoters_distinctUsersOnDifferentLabels_passes() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(admin.id());
     approve(c1.toString());
     requestScopeOperations.setApiUser(user.id());
@@ -173,7 +176,7 @@
 
   @Test
   public void distinctVoters_onlyMaxVotesRespected() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(c1.toString())
@@ -189,7 +192,7 @@
 
   @Test
   public void distinctVoters_onlyMinVotesRespected() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(c1.toString())
@@ -205,7 +208,7 @@
 
   @Test
   public void distinctVoters_onlyExactValueRespected() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(c1.toString())
@@ -221,7 +224,7 @@
 
   @Test
   public void distinctVoters_valueIsOptional() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(c1.toString())
@@ -235,7 +238,7 @@
 
   @Test
   public void distinctVoters_moreThanTwoLabels() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(c1.toString())
@@ -249,7 +252,7 @@
 
   @Test
   public void distinctVoters_moreThanTwoLabels_moreThanTwoUsers() throws Exception {
-    Change.Id c1 = changeOperations.newChange().project(project).create();
+    Change.Id c1 = changeOperations.newChange().project(project).createV1();
     requestScopeOperations.setApiUser(user.id());
     gApi.changes()
         .id(c1.toString())
@@ -344,15 +347,15 @@
   public void hasSubmoduleUpdate_withWrongArgs_throws() {
     assertError(
         "has:submodule-update,base=xyz",
-        changeOperations.newChange().project(project).create(),
+        changeOperations.newChange().project(project).createV1(),
         "failed to parse the parent number xyz: For input string: \"xyz\"");
     assertError(
         "has:submodule-update,base=1,arg=foo",
-        changeOperations.newChange().project(project).create(),
+        changeOperations.newChange().project(project).createV1(),
         "wrong number of arguments for the has:submodule-update operator");
     assertError(
         "has:submodule-update,base",
-        changeOperations.newChange().project(project).create(),
+        changeOperations.newChange().project(project).createV1(),
         "unexpected base value format");
   }
 
@@ -413,7 +416,12 @@
             .create();
 
     Change.Id changeId =
-        changeOperations.newChange().author(authorId).committer(committerId).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .author(authorId)
+            .committer(committerId)
+            .createV1();
     ChangeInfo changeInfo = gApi.changes().id(changeId.get()).get();
     assertAuthor(changeInfo, "authoremail@example.com");
 
@@ -442,7 +450,12 @@
             .create();
 
     Change.Id changeId =
-        changeOperations.newChange().author(authorId).committer(committerId).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .author(authorId)
+            .committer(committerId)
+            .createV1();
     ChangeInfo changeInfo = gApi.changes().id(changeId.get()).get();
     assertCommitter(changeInfo, "committeremail@example.com");
 
@@ -489,6 +502,125 @@
   }
 
   @Test
+  public void nonAuthorLabelVote_match() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, user);
+    PushOneCommit.Result r1 =
+        pushFactory
+            .create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
+            .to("refs/for/master");
+
+    Change.Id cId = r1.getChange().getId();
+
+    ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
+
+    // Assert on author
+    assertAuthor(changeInfo, user.email());
+
+    // Vote from admin (a.k.a. non author) matches
+    requestScopeOperations.setApiUser(admin.id());
+    approve(cId.toString());
+    assertMatching("label:Code-Review=+2,user=non_author", cId);
+    // Also make sure magic label votes and > operator work
+    assertMatching("label:Code-Review=MAX,user=non_author", cId);
+    assertMatching("label:Code-Review>+1,user=non_author", cId);
+  }
+
+  @Test
+  @Sandboxed
+  public void nonAuthorLabelVote_voteFromAuthor_doesNotMatch() throws Exception {
+    Account.Id authorId =
+        accountOperations
+            .newAccount()
+            .fullname("author")
+            .preferredEmail("authoremail@example.com")
+            .create();
+    Account.Id committerId =
+        accountOperations
+            .newAccount()
+            .fullname("committer")
+            .preferredEmail("committeremail@example.com")
+            .create();
+
+    TestChange change =
+        changeOperations
+            .newChange()
+            .project(project)
+            .author(authorId)
+            .committer(committerId)
+            .createAndGet();
+    ChangeInfo changeInfo = gApi.changes().id(change.id()).get();
+    assertAuthor(changeInfo, "authoremail@example.com");
+
+    allowLabelPermission(
+        codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
+
+    // Vote from author does not match
+    requestScopeOperations.setApiUser(authorId);
+    approve(change.id().id());
+    assertNotMatching("label:Code-Review=+2,user=non_author", change.numericChangeId());
+  }
+
+  @Test
+  public void nonCommitterLabelVote_voteFromCommitter_doesNotMatch() throws Exception {
+    Account.Id authorId =
+        accountOperations
+            .newAccount()
+            .fullname("author")
+            .preferredEmail("authoremail@example.com")
+            .create();
+    Account.Id committerId =
+        accountOperations
+            .newAccount()
+            .fullname("committer")
+            .preferredEmail("committeremail@example.com")
+            .create();
+
+    TestChange change =
+        changeOperations
+            .newChange()
+            .project(project)
+            .author(authorId)
+            .committer(committerId)
+            .createAndGet();
+    ChangeInfo changeInfo = gApi.changes().id(change.id()).get();
+    assertCommitter(changeInfo, "committeremail@example.com");
+
+    allowLabelPermission(
+        codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
+
+    // Vote from committer does not match
+    requestScopeOperations.setApiUser(committerId);
+    approve(change.id().id());
+    assertNotMatching("label:Code-Review=+2,user=non_committer", change.numericChangeId());
+  }
+
+  @Test
+  public void nonCommitterLabelVote_match() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, user);
+    PushOneCommit.Result r1 =
+        pushFactory
+            .create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
+            .to("refs/for/master");
+
+    Change.Id cId = r1.getChange().getId();
+
+    ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
+
+    // Assert on committer
+    assertAuthor(changeInfo, user.email());
+
+    // Vote from admin (a.k.a. non committer) matches
+    requestScopeOperations.setApiUser(admin.id());
+    approve(cId.toString());
+    assertMatching("label:Code-Review=+2,user=non_committer", cId);
+    // Also make sure magic label votes and > operator work
+    assertMatching("label:Code-Review=MAX,user=non_committer", cId);
+    assertMatching("label:Code-Review>+1,user=non_committer", cId);
+  }
+
+  @Test
   public void label_requireVoteFromHumanReviewers() throws Exception {
     projectOperations
         .project(project)
@@ -505,7 +637,7 @@
     gApi.groups().id(ServiceUserClassifier.SERVICE_USERS).addMembers(serviceUser.toString());
 
     Change.Id changeApprovedByAllReviewers =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(project, changeApprovedByAllReviewers, reviewer1, reviewer2, reviewer3);
     addReviews(
         project,
@@ -516,12 +648,12 @@
         reviewer3);
 
     Change.Id changeApprovedBySomeReviewers =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(project, changeApprovedBySomeReviewers, reviewer1, reviewer2, reviewer3);
     addReviews(project, changeApprovedBySomeReviewers, ReviewInput.approve(), reviewer1, reviewer2);
 
     Change.Id changeRecommendedByAllReviewers =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(project, changeRecommendedByAllReviewers, reviewer1, reviewer2, reviewer3);
     addReviews(
         project,
@@ -532,17 +664,17 @@
         reviewer3);
 
     Change.Id changeRecommendedBySomeReviewers =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(project, changeRecommendedBySomeReviewers, reviewer1, reviewer2, reviewer3);
     addReviews(
         project, changeRecommendedBySomeReviewers, ReviewInput.recommend(), reviewer1, reviewer2);
 
     Change.Id changeNoVotesByReviewers =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(project, changeNoVotesByReviewers, reviewer1, reviewer2, reviewer3);
 
     Change.Id changeWithoutReviewers =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
 
     requestScopeOperations.setApiUser(user.id());
 
@@ -670,18 +802,9 @@
             changeNoVotesByReviewers));
 
     // when reviewers by email are present changes do not match, unless the expected value is 0
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig cfg = projectConfigFactory.create(project);
-      cfg.load(md);
-      cfg.updateProject(
-          update ->
-              update.setBooleanConfig(
-                  BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.TRUE));
-      cfg.commit(md);
-    }
-    projectCache.evictAndReindex(project);
+    projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
     Change.Id changeRecommendedByAllReviewersWithReviewersByEmail =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(
         project,
         changeRecommendedByAllReviewersWithReviewersByEmail,
@@ -700,7 +823,7 @@
         changeRecommendedByAllReviewersWithReviewersByEmail,
         "email-without-account@example.com");
     Change.Id changeNoVotesByReviewersWithReviewersByEmail =
-        changeOperations.newChange().project(project).owner(owner).create();
+        changeOperations.newChange().project(project).owner(owner).createV1();
     addReviewers(
         project, changeNoVotesByReviewersWithReviewersByEmail, reviewer1, reviewer2, reviewer3);
     addReviewer(
@@ -834,6 +957,64 @@
             changeNoVotesByReviewers));
   }
 
+  @Test
+  public void hasUnresolvedComments() throws Exception {
+    TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, admin);
+    PushOneCommit.Result r =
+        pushFactory
+            .create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
+            .to("refs/for/master");
+
+    assertNotMatching("has:unresolved", r.getChange().getId());
+    assertMatching("-has:unresolved", r.getChange().getId());
+
+    ReviewInput reviewInput = new ReviewInput();
+    ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
+    commentInput.line = 1;
+    commentInput.message = "inline";
+    commentInput.unresolved = true;
+    reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(commentInput));
+    gApi.changes().id(project.get(), r.getChange().getId().get()).current().review(reviewInput);
+    CommentInfo commentInfo =
+        Iterables.getOnlyElement(
+            gApi.changes()
+                .id(project.get(), r.getChange().getId().get())
+                .current()
+                .commentsAsList());
+
+    assertMatching("has:unresolved", r.getChange().getId());
+    assertNotMatching("-has:unresolved", r.getChange().getId());
+
+    // Rename the file in the change to check that the change still matches if the commented file
+    // path is no longer present in the current patch set.
+    gApi.changes().id(project.get(), r.getChange().getId().get()).edit().create();
+    gApi.changes()
+        .id(project.get(), r.getChange().getId().get())
+        .edit()
+        .renameFile("file.txt", "renamed-file.txt");
+    gApi.changes()
+        .id(project.get(), r.getChange().getId().get())
+        .edit()
+        .publish(new PublishChangeEditInput());
+    Map<String, FileInfo> files =
+        gApi.changes().id(project.get(), r.getChange().getId().get()).current().files("1");
+    assertThat(files.keySet()).containsExactly(Patch.COMMIT_MSG, "renamed-file.txt");
+    assertThat(files.get("renamed-file.txt").oldPath).isEqualTo("file.txt");
+    assertMatching("has:unresolved", r.getChange().getId());
+    assertNotMatching("-has:unresolved", r.getChange().getId());
+
+    // Resolve the comment.
+    reviewInput = new ReviewInput();
+    commentInput = new ReviewInput.CommentInput();
+    commentInput.inReplyTo = commentInfo.id;
+    commentInput.message = "done";
+    commentInput.unresolved = false;
+    reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(commentInput));
+    gApi.changes().id(project.get(), r.getChange().getId().get()).current().review(reviewInput);
+    assertNotMatching("has:unresolved", r.getChange().getId());
+    assertMatching("-has:unresolved", r.getChange().getId());
+  }
+
   private void addReviewers(Project.NameKey project, Change.Id changeId, Account.Id... reviewers)
       throws Exception {
     for (Account.Id reviewer : reviewers) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
index da89c9a..86c905f 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
@@ -86,7 +86,7 @@
             .project(project)
             .file("file")
             .content("content\naa\nsF\naa\naaa\nsomething\nfoo\nbla\ndeletedEnd")
-            .create();
+            .createV1();
 
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
@@ -133,7 +133,7 @@
             .project(project)
             .file("file")
             .content("line1\nline2\nline3\nline4")
-            .create();
+            .createV1();
 
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
     changeOperations
@@ -151,10 +151,10 @@
     // create 2 unrelated changes and rebase on top of them. Those rebases should be ignored.
     // The changes add files.
     Change.Id unrelated =
-        changeOperations.newChange().project(project).file("a").content("a").create();
+        changeOperations.newChange().project(project).file("a").content("a").createV1();
     gApi.changes().id(unrelated.get()).current().review(ReviewInput.approve());
     gApi.changes().id(unrelated.get()).current().submit();
-    unrelated = changeOperations.newChange().project(project).file("z").content("z").create();
+    unrelated = changeOperations.newChange().project(project).file("z").content("z").createV1();
     gApi.changes().id(unrelated.get()).current().review(ReviewInput.approve());
     gApi.changes().id(unrelated.get()).current().submit();
 
@@ -180,11 +180,12 @@
   @Test
   public void diffChangeMessageOnSubmitWithStickyVote_ignoreDiffFromRebaseRenames()
       throws Exception {
-    Change.Id setup = changeOperations.newChange().project(project).file("a").content("a").create();
+    Change.Id setup =
+        changeOperations.newChange().project(project).file("a").content("a").createV1();
     gApi.changes().id(setup.get()).current().review(ReviewInput.approve());
     gApi.changes().id(setup.get()).current().submit();
 
-    setup = changeOperations.newChange().project(project).file("z").content("z").create();
+    setup = changeOperations.newChange().project(project).file("z").content("z").createV1();
     gApi.changes().id(setup.get()).current().review(ReviewInput.approve());
     gApi.changes().id(setup.get()).current().submit();
 
@@ -195,7 +196,7 @@
             .project(project)
             .file("file")
             .content("line1\nline2\nline3\nline4")
-            .create();
+            .createV1();
 
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
     changeOperations
@@ -213,10 +214,10 @@
     // create 2 unrelated changes and rebase on top of them. Those rebases should be ignored.
     // The changes rename files.
     Change.Id unrelated =
-        changeOperations.newChange().project(project).file("a").renameTo("aa").create();
+        changeOperations.newChange().project(project).file("a").renameTo("aa").createV1();
     gApi.changes().id(unrelated.get()).current().review(ReviewInput.approve());
     gApi.changes().id(unrelated.get()).current().submit();
-    unrelated = changeOperations.newChange().project(project).file("z").renameTo("zz").create();
+    unrelated = changeOperations.newChange().project(project).file("z").renameTo("zz").createV1();
     gApi.changes().id(unrelated.get()).current().review(ReviewInput.approve());
     gApi.changes().id(unrelated.get()).current().submit();
 
@@ -242,11 +243,12 @@
   @Test
   public void diffChangeMessageOnSubmitWithStickyVote_ignoreDiffFromRebaseDeletions()
       throws Exception {
-    Change.Id setup = changeOperations.newChange().project(project).file("a").content("a").create();
+    Change.Id setup =
+        changeOperations.newChange().project(project).file("a").content("a").createV1();
     gApi.changes().id(setup.get()).current().review(ReviewInput.approve());
     gApi.changes().id(setup.get()).current().submit();
 
-    setup = changeOperations.newChange().project(project).file("z").content("z").create();
+    setup = changeOperations.newChange().project(project).file("z").content("z").createV1();
     gApi.changes().id(setup.get()).current().review(ReviewInput.approve());
     gApi.changes().id(setup.get()).current().submit();
 
@@ -257,7 +259,7 @@
             .project(project)
             .file("file")
             .content("line1\nline2\nline3\nline4")
-            .create();
+            .createV1();
 
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
     changeOperations
@@ -273,10 +275,11 @@
     testRepo.reset(initial);
     // create 2 unrelated changes and rebase on top of them. Those rebases should be ignored.
     // The changes delete files.
-    Change.Id unrelated = changeOperations.newChange().project(project).file("a").delete().create();
+    Change.Id unrelated =
+        changeOperations.newChange().project(project).file("a").delete().createV1();
     gApi.changes().id(unrelated.get()).current().review(ReviewInput.approve());
     gApi.changes().id(unrelated.get()).current().submit();
-    unrelated = changeOperations.newChange().project(project).file("z").delete().create();
+    unrelated = changeOperations.newChange().project(project).file("z").delete().createV1();
     gApi.changes().id(unrelated.get()).current().review(ReviewInput.approve());
     gApi.changes().id(unrelated.get()).current().submit();
 
@@ -308,7 +311,7 @@
             .project(project)
             .file("file")
             .content("content\naa\nbb\ncc\ndd\nee\nff\nTODELETE1\nTODELETE2\ngg\nend")
-            .create();
+            .createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     changeOperations
@@ -350,7 +353,7 @@
   @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "10k")
   public void autoGeneratedPostSubmitDiffIsPartOfTheCommentSizeLimit() throws Exception {
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
     String content = new String(new char[800]).replace("\0", "a");
     changeOperations.change(changeId).newPatchset().file("file").content(content).create();
@@ -382,7 +385,7 @@
   @Test
   public void postSubmitDiffCannotBeTooBig() throws Exception {
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     // max post submit diff size is 300k
@@ -404,7 +407,7 @@
   @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "10k")
   public void postSubmitDiffCannotBeTooBigWithLargeComments() throws Exception {
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     // unrelated comment taking up most of the space, making post submit diff shorter.
@@ -432,7 +435,7 @@
 
   @Test
   public void diffChangeMessageOnSubmitWithStickyVote_addedFile() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     changeOperations
@@ -458,7 +461,7 @@
 
   @Test
   public void diffChangeMessageOnSubmitWithStickyVote_addedMultipleFiles() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     changeOperations
@@ -502,7 +505,7 @@
             .project(project)
             .file("file")
             .content("content\nmore content\nlast content")
-            .create();
+            .createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     changeOperations.change(changeId).newPatchset().file("file").delete().create();
@@ -529,7 +532,7 @@
             .project(project)
             .file("file")
             .content("content\nmoreContent")
-            .create();
+            .createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     changeOperations.change(changeId).newPatchset().file("file").renameTo("new_file").create();
@@ -556,7 +559,7 @@
             .project(project)
             .file("file")
             .content("content\nmoreContent")
-            .create();
+            .createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     changeOperations.change(changeId).newPatchset().file("file").renameTo("new_file").create();
@@ -572,7 +575,7 @@
 
   @Test
   public void diffChangeMessageOnSubmitWithStickyVote_approvedPatchset() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     changeOperations.change(changeId).newPatchset().create();
 
     // approve patch-set 2
@@ -590,7 +593,7 @@
 
   @Test
   public void diffChangeMessageOnSubmitWithStickyVote_noChanges() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
 
     // no file changed
diff --git a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
index fe3cb00..60a8204 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
@@ -52,7 +52,9 @@
                 .GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE,
             ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
             ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE,
-            ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+            ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE,
+            ExperimentFeaturesConstants.ASYNC_SUBMIT_REQUIREMENTS,
+            ExperimentFeaturesConstants.UI_FEATURE_GET_AI_PROMPT)
         .inOrder();
 
     // "GerritBackendFeature__check_implicit_merges_on_merge",
@@ -81,6 +83,8 @@
     assertThat(
             experiments.get(ExperimentFeaturesConstants.ALLOW_FIX_SUGGESTIONS_IN_COMMENTS).enabled)
         .isFalse();
+    assertThat(experiments.get(ExperimentFeaturesConstants.UI_FEATURE_GET_AI_PROMPT).enabled)
+        .isTrue();
     assertThat(
             experiments.get(
                     ExperimentFeaturesConstants
@@ -106,7 +110,8 @@
                 .GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE,
             ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
             ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE,
-            ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+            ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE,
+            ExperimentFeaturesConstants.UI_FEATURE_GET_AI_PROMPT)
         .inOrder();
     for (ExperimentInfo experimentInfo : experiments.values()) {
       assertThat(experimentInfo.enabled).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/api/flow/BUILD b/javatests/com/google/gerrit/acceptance/api/flow/BUILD
new file mode 100644
index 0000000..24aea20
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/flow/BUILD
@@ -0,0 +1,23 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "api_flow",
+    labels = ["api"],
+    deps = [
+        ":flow-test-util",
+    ],
+)
+
+java_library(
+    name = "flow-test-util",
+    testonly = True,
+    srcs = glob(["FlowTestUtil.java"]),
+    deps = [
+        "//java/com/google/gerrit/acceptance:lib",
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/server",
+        "//lib:guava",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/api/flow/CreateFlowIT.java b/javatests/com/google/gerrit/acceptance/api/flow/CreateFlowIT.java
new file mode 100644
index 0000000..c135b8a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/flow/CreateFlowIT.java
@@ -0,0 +1,332 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.flow;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowInputWithInvalidCondition;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowInputWithMultipleStages;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowInputWithNStages;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowInputWithOneStage;
+import static com.google.gerrit.extensions.common.testing.FlowInfoSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestExtensions;
+import com.google.gerrit.acceptance.TestExtensions.TestFlowService;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.flow.FlowKey;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.gerrit.server.restapi.flow.CreateFlow;
+import com.google.inject.Inject;
+import java.time.Instant;
+import org.junit.Test;
+
+/**
+ * Integration tests for the {@link com.google.gerrit.server.restapi.flow.CreateFlow} REST endpoint.
+ */
+public class CreateFlowIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void createFlowIfNoFlowServiceIsBound_methodNotAllowed() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    MethodNotAllowedException exception =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () ->
+                gApi.changes()
+                    .id(changeIdentifier)
+                    .createFlow(createTestFlowInputWithOneStage(accountCreator, changeIdentifier)));
+    assertThat(exception).hasMessageThat().isEqualTo("No FlowService bound.");
+  }
+
+  @Test
+  public void createFlowWithoutStages_badRequest() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      flowInput.stageExpressions = null;
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("at least one stage expression is required");
+
+      flowInput.stageExpressions = ImmutableList.of();
+      exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("at least one stage expression is required");
+    }
+  }
+
+  @Test
+  public void createFlowWithoutConditionInStageExpression_badRequest() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      Iterables.getOnlyElement(flowInput.stageExpressions).condition = null;
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("condition in stage expression is required");
+
+      Iterables.getOnlyElement(flowInput.stageExpressions).condition = "";
+      exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("condition in stage expression is required");
+    }
+  }
+
+  @Test
+  public void cannotCreateFlowWithoutActionOnLastStageExpression() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      Iterables.getOnlyElement(flowInput.stageExpressions).action = null;
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo("the last stage expression is required to have an action");
+    }
+  }
+
+  @Test
+  public void createFlowWithStageExpressionsThatDontHaveAnAction() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      Instant beforeInstant = Instant.now();
+      FlowInput flowInput = createTestFlowInputWithNStages(accountCreator, changeIdentifier, 3);
+      flowInput.stageExpressions.get(0).action = null;
+      FlowInfo flowInfo = gApi.changes().id(changeIdentifier).createFlow(flowInput);
+      assertFlowInfoForNewlyCreatedFlow(flowInfo, flowInput, admin, beforeInstant);
+
+      flowInput.stageExpressions.get(1).action = null;
+      flowInfo = gApi.changes().id(changeIdentifier).createFlow(flowInput);
+      assertFlowInfoForNewlyCreatedFlow(flowInfo, flowInput, admin, beforeInstant);
+    }
+  }
+
+  @Test
+  public void createFlowWithoutNameInAction_badRequest() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      Iterables.getOnlyElement(flowInput.stageExpressions).action.name = null;
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("name in action is required");
+
+      Iterables.getOnlyElement(flowInput.stageExpressions).action.name = "";
+      exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("name in action is required");
+    }
+  }
+
+  @Test
+  public void createFlowWithSingleStage() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      Instant beforeInstant = Instant.now();
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, change.id());
+      FlowInfo flowInfo = gApi.changes().id(change.id()).createFlow(flowInput);
+      assertFlowInfoForNewlyCreatedFlow(flowInfo, flowInput, admin, beforeInstant);
+      assertThat(
+              flowService.getFlow(
+                  FlowKey.create(change.project(), change.numericChangeId(), flowInfo.uuid)))
+          .isPresent();
+    }
+  }
+
+  @Test
+  public void createFlowWithMultipleStage() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      Instant beforeInstant = Instant.now();
+      FlowInput flowInput = createTestFlowInputWithMultipleStages(accountCreator, change.id());
+      FlowInfo flowInfo = gApi.changes().id(change.id()).createFlow(flowInput);
+      assertFlowInfoForNewlyCreatedFlow(flowInfo, flowInput, admin, beforeInstant);
+      assertThat(
+              flowService.getFlow(
+                  FlowKey.create(change.project(), change.numericChangeId(), flowInfo.uuid)))
+          .isPresent();
+    }
+  }
+
+  @Test
+  public void createFlowWithoutParametersInAction() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      Instant beforeInstant = Instant.now();
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, change.id());
+      Iterables.getOnlyElement(flowInput.stageExpressions).action.parameters = null;
+      FlowInfo flowInfo = gApi.changes().id(change.id()).createFlow(flowInput);
+      assertFlowInfoForNewlyCreatedFlow(flowInfo, flowInput, admin, beforeInstant);
+      assertThat(
+              flowService.getFlow(
+                  FlowKey.create(change.project(), change.numericChangeId(), flowInfo.uuid)))
+          .isPresent();
+
+      Iterables.getOnlyElement(flowInput.stageExpressions).action.parameters = ImmutableList.of();
+      flowInfo = gApi.changes().id(change.id()).createFlow(flowInput);
+      assertFlowInfoForNewlyCreatedFlow(flowInfo, flowInput, admin, beforeInstant);
+      assertThat(
+              flowService.getFlow(
+                  FlowKey.create(change.project(), change.numericChangeId(), flowInfo.uuid)))
+          .isPresent();
+    }
+  }
+
+  @Test
+  public void createFlowWithInvalidCondtition_badRequest() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInput flowInput =
+          createTestFlowInputWithInvalidCondition(accountCreator, changeIdentifier);
+      assertThrows(
+          BadRequestException.class,
+          () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+    }
+  }
+
+  @Test
+  public void createFlow_authenticationRequired() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    requestScopeOperations.setApiUserAnonymous();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    testFlowService.rejectFlowCreation();
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      AuthException exception =
+          assertThrows(
+              AuthException.class, () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception).hasMessageThat().isEqualTo("Authentication required");
+    }
+  }
+
+  @Test
+  public void createFlow_permissionDenied() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    testFlowService.rejectFlowCreation();
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      assertThrows(
+          AuthException.class, () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+    }
+  }
+
+  @Test
+  public void numberOfFlowsPerChangeIsLimitedByDefault() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      for (int i = 1; i <= CreateFlow.DEFAULT_MAX_FLOWS_PER_CHANGE; i++) {
+        gApi.changes().id(changeIdentifier).createFlow(flowInput);
+      }
+
+      ResourceConflictException exception =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Too many flows (max %s flow allowed per change)",
+                  CreateFlow.DEFAULT_MAX_FLOWS_PER_CHANGE));
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "flows.maxPerChange", value = "50")
+  public void numberOfFlowsPerChangeIsLimitedByConfiguration() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      for (int i = 1; i <= 50; i++) {
+        gApi.changes().id(changeIdentifier).createFlow(flowInput);
+      }
+
+      ResourceConflictException exception =
+          assertThrows(
+              ResourceConflictException.class,
+              () -> gApi.changes().id(changeIdentifier).createFlow(flowInput));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(String.format("Too many flows (max %s flow allowed per change)", 50));
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "flows.maxPerChange", value = "0")
+  public void numberOfFlowsPerChangeIsUnlimited() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInput flowInput = createTestFlowInputWithOneStage(accountCreator, changeIdentifier);
+      for (int i = 1; i <= CreateFlow.DEFAULT_MAX_FLOWS_PER_CHANGE + 10; i++) {
+        gApi.changes().id(changeIdentifier).createFlow(flowInput);
+      }
+    }
+  }
+
+  private static void assertFlowInfoForNewlyCreatedFlow(
+      FlowInfo flowInfo, FlowInput flowInput, TestAccount owner, Instant beforeInstant) {
+    assertThat(flowInfo).matches(flowInput);
+    assertThat(flowInfo).hasOwnerThat().hasAccountIdThat().isEqualTo(owner.id());
+    assertThat(flowInfo).hasCreatedThat().isAtLeast(beforeInstant);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/flow/DeleteFlowIT.java b/javatests/com/google/gerrit/acceptance/api/flow/DeleteFlowIT.java
new file mode 100644
index 0000000..da5e005
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/flow/DeleteFlowIT.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.flow;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreationWithOneStage;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.TestExtensions;
+import com.google.gerrit.acceptance.TestExtensions.TestFlowService;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+/**
+ * Integration tests for the {@link com.google.gerrit.server.restapi.flow.DeleteFlow} REST endpoint.
+ */
+public class DeleteFlowIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void deleteFlow() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreationWithOneStage(accountCreator, change);
+    Flow flow = flowService.createFlow(flowCreation);
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      gApi.changes().id(change.id()).flow(flow.key().uuid()).delete();
+      assertThat(flowService.getFlow(flow.key())).isEmpty();
+    }
+  }
+
+  @Test
+  public void deleteFlow_authenticationRequired() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    requestScopeOperations.setApiUserAnonymous();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreationWithOneStage(accountCreator, change);
+    Flow flow = flowService.createFlow(flowCreation);
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      AuthException exception =
+          assertThrows(
+              AuthException.class,
+              () -> gApi.changes().id(change.id()).flow(flow.key().uuid()).delete());
+      assertThat(exception).hasMessageThat().isEqualTo("Authentication required");
+    }
+  }
+
+  @Test
+  public void deleteFlow_permissionDenied() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreationWithOneStage(accountCreator, change);
+    Flow flow = testFlowService.createFlow(flowCreation);
+    testFlowService.rejectFlowDeletion();
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      assertThrows(
+          AuthException.class,
+          () -> gApi.changes().id(change.id()).flow(flow.key().uuid()).delete());
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/flow/FlowTestUtil.java b/javatests/com/google/gerrit/acceptance/api/flow/FlowTestUtil.java
new file mode 100644
index 0000000..827f2d4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/flow/FlowTestUtil.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.flow;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.TestExtensions.TestFlowService;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import com.google.gerrit.extensions.common.FlowActionInfo;
+import com.google.gerrit.extensions.common.FlowExpressionInfo;
+import com.google.gerrit.extensions.common.FlowInput;
+import com.google.gerrit.server.flow.FlowAction;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowExpression;
+
+/**
+ * Methods to create and assert flow entities that are shared between the different flow integration
+ * tests.
+ */
+public class FlowTestUtil {
+  /** Creates a {@link FlowInput} with arbitrary test data that contains one stage. */
+  public static FlowInput createTestFlowInputWithOneStage(
+      AccountCreator accountCreator, ChangeIdentifier changeIdentifier) throws Exception {
+    return createTestFlowInput(accountCreator, changeIdentifier, 1);
+  }
+
+  /** Creates a {@link FlowInput} with arbitrary test data that contains multiple stages. */
+  public static FlowInput createTestFlowInputWithMultipleStages(
+      AccountCreator accountCreator, ChangeIdentifier changeIdentifier) throws Exception {
+    return createTestFlowInput(accountCreator, changeIdentifier, 3);
+  }
+
+  /** Creates a {@link FlowInput} with arbitrary test data that contains n stages. */
+  public static FlowInput createTestFlowInputWithNStages(
+      AccountCreator accountCreator, ChangeIdentifier changeIdentifier, int numberOfStages)
+      throws Exception {
+    return createTestFlowInput(accountCreator, changeIdentifier, numberOfStages);
+  }
+
+  /** Creates a {@link FlowInput} with arbitrary test data that contains an invalid condition. */
+  public static FlowInput createTestFlowInputWithInvalidCondition(
+      AccountCreator accountCreator, ChangeIdentifier changeIdentifier) throws Exception {
+    FlowInput flowInput = createTestFlowInput(accountCreator, changeIdentifier, 1);
+    flowInput.stageExpressions.get(0).condition = TestFlowService.INVALID_CONDITION;
+    return flowInput;
+  }
+
+  /** Creates a {@link FlowInput} with arbitrary test data and as many stages as specified. */
+  private static FlowInput createTestFlowInput(
+      AccountCreator accountCreator, ChangeIdentifier changeIdentifier, int numberOfStages)
+      throws Exception {
+    FlowInput flowInput = new FlowInput();
+
+    ImmutableList.Builder<FlowExpressionInfo> stageExpressionsBuilder = ImmutableList.builder();
+    for (int i = 0; i < numberOfStages; i++) {
+      FlowExpressionInfo flowExpressionInfo = new FlowExpressionInfo();
+      flowExpressionInfo.condition =
+          String.format("com.google.gerrit[change:%s label:Verified+%s]", changeIdentifier.id(), i);
+
+      FlowActionInfo flowActionInfo = new FlowActionInfo();
+      flowActionInfo.name = "AddReviewer";
+      flowActionInfo.parameters =
+          ImmutableList.of(accountCreator.createValid("reviewer" + i).email());
+      flowExpressionInfo.action = flowActionInfo;
+
+      stageExpressionsBuilder.add(flowExpressionInfo);
+    }
+
+    flowInput.stageExpressions = stageExpressionsBuilder.build();
+
+    return flowInput;
+  }
+
+  /** Creates a {@link FlowCreation} with arbitrary test data that contains one stage. */
+  public static FlowCreation createTestFlowCreationWithOneStage(
+      AccountCreator accountCreator, TestChange change) throws Exception {
+    return createTestFlowCreation(accountCreator, change, 1);
+  }
+
+  /** Creates a {@link FlowCreation} with arbitrary test data that contains multiple stages. */
+  public static FlowCreation createTestFlowCreationWithMultipleStages(
+      AccountCreator accountCreator, TestChange change) throws Exception {
+    return createTestFlowCreation(accountCreator, change, 3);
+  }
+
+  /** Creates a {@link FlowCreation} with arbitrary test data and as many stages as specified. */
+  public static FlowCreation createTestFlowCreation(
+      AccountCreator accountCreator, TestChange change, int numberOfStages) throws Exception {
+    FlowCreation.Builder flowCreationBuilder =
+        FlowCreation.builder()
+            .projectName(change.project())
+            .changeId(change.numericChangeId())
+            .ownerId(accountCreator.createValid("owner").id());
+
+    for (int i = 0; i < numberOfStages; i++) {
+      flowCreationBuilder.addStageExpression(
+          FlowExpression.builder()
+              .condition(
+                  String.format("com.google.gerrit[change:%s label:Verified+%s]", change.id(), i))
+              .action(
+                  FlowAction.builder()
+                      .name("AddReviewer")
+                      .addParameter(accountCreator.createValid("reviewer" + i).email())
+                      .build())
+              .build());
+    }
+
+    return flowCreationBuilder.build();
+  }
+
+  /**
+   * Private constructor to prevent instantiation of this class.
+   *
+   * <p>This class contains only static methods and hence never needs to be instantiated.
+   */
+  private FlowTestUtil() {}
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/flow/GetFlowIT.java b/javatests/com/google/gerrit/acceptance/api/flow/GetFlowIT.java
new file mode 100644
index 0000000..4be805e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/flow/GetFlowIT.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.flow;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreation;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreationWithMultipleStages;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreationWithOneStage;
+import static com.google.gerrit.extensions.common.testing.FlowInfoSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.TestExtensions;
+import com.google.gerrit.acceptance.TestExtensions.TestFlowService;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.gerrit.server.flow.FlowStageEvaluationStatus.State;
+import com.google.inject.Inject;
+import java.util.Optional;
+import org.junit.Test;
+
+/**
+ * Integration tests for the {@link com.google.gerrit.server.restapi.flow.GetFlow} REST endpoint.
+ */
+public class GetFlowIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void getFlowIfNoFlowServiceIsBound_methodNotAllowed() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    MethodNotAllowedException exception =
+        assertThrows(
+            MethodNotAllowedException.class,
+            () -> gApi.changes().id(changeIdentifier).flow("flow-uuid"));
+    assertThat(exception).hasMessageThat().isEqualTo("No FlowService bound.");
+  }
+
+  @Test
+  public void getNonExistingFlow_notFound() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      ResourceNotFoundException exception =
+          assertThrows(
+              ResourceNotFoundException.class,
+              () -> gApi.changes().id(changeIdentifier).flow("non-existing-flow-uuid"));
+      assertThat(exception).hasMessageThat().isEqualTo("Flow non-existing-flow-uuid not found.");
+    }
+  }
+
+  @Test
+  public void getFlowWithSingleStage_notYetEvaluated() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreationWithOneStage(accountCreator, change);
+    Flow flow = flowService.createFlow(flowCreation);
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInfo flowInfo = gApi.changes().id(change.id()).flow(flow.key().uuid()).get();
+      assertThat(flowInfo).matches(flow);
+    }
+  }
+
+  @Test
+  public void getFlowWithSingleStage_evaluated() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreationWithOneStage(accountCreator, change);
+    Flow flow = testFlowService.createFlow(flowCreation);
+    flow =
+        testFlowService.evaluate(
+            flow.key(), ImmutableList.of(State.DONE), ImmutableList.of(Optional.of("done")));
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInfo flowInfo = gApi.changes().id(change.id()).flow(flow.key().uuid()).get();
+      assertThat(flowInfo).matches(flow);
+    }
+  }
+
+  @Test
+  public void getFlowWithMultipleStages_notYetEvaluated() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreationWithMultipleStages(accountCreator, change);
+    Flow flow = flowService.createFlow(flowCreation);
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      FlowInfo flowInfo = gApi.changes().id(change.id()).flow(flow.key().uuid()).get();
+      assertThat(flowInfo).matches(flow);
+    }
+  }
+
+  @Test
+  public void getFlowWithMultipleStages_evaluated() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    FlowCreation flowCreation = createTestFlowCreation(accountCreator, change, 3);
+    Flow flow = testFlowService.createFlow(flowCreation);
+    flow =
+        testFlowService.evaluate(
+            flow.key(),
+            ImmutableList.of(State.DONE, State.FAILED, State.TERMINATED),
+            ImmutableList.of(
+                Optional.empty(),
+                Optional.of("error"),
+                Optional.of("terminated because previous stage failed")));
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      FlowInfo flowInfo = gApi.changes().id(change.id()).flow(flow.key().uuid()).get();
+      assertThat(flowInfo).matches(flow);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/flow/ListFlowsIT.java b/javatests/com/google/gerrit/acceptance/api/flow/ListFlowsIT.java
new file mode 100644
index 0000000..2ae01e8
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/flow/ListFlowsIT.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.flow;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreation;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreationWithMultipleStages;
+import static com.google.gerrit.acceptance.api.flow.FlowTestUtil.createTestFlowCreationWithOneStage;
+import static com.google.gerrit.extensions.common.testing.FlowInfoSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.TestExtensions;
+import com.google.gerrit.acceptance.TestExtensions.TestFlowService;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import com.google.gerrit.extensions.common.FlowInfo;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.gerrit.server.flow.FlowStageEvaluationStatus.State;
+import com.google.inject.Inject;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import org.junit.Test;
+
+/**
+ * Integration tests for the {@link com.google.gerrit.server.restapi.flow.ListFlows} REST endpoint.
+ */
+public class ListFlowsIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void listFlowsIfNoFlowServiceIsBound_methodNotAllowed() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    MethodNotAllowedException exception =
+        assertThrows(
+            MethodNotAllowedException.class, () -> gApi.changes().id(changeIdentifier).flows());
+    assertThat(exception).hasMessageThat().isEqualTo("No FlowService bound.");
+  }
+
+  @Test
+  public void listFlow_noFlowsExist() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      List<FlowInfo> flows = gApi.changes().id(changeIdentifier).flows();
+      assertThat(flows).isEmpty();
+    }
+  }
+
+  @Test
+  public void listFlows() throws Exception {
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestFlowService testFlowService = new TestExtensions.TestFlowService();
+    Flow flow1 =
+        testFlowService.createFlow(createTestFlowCreationWithOneStage(accountCreator, change));
+    Flow flow2 =
+        testFlowService.createFlow(createTestFlowCreationWithOneStage(accountCreator, change));
+    flow2 =
+        testFlowService.evaluate(
+            flow2.key(), ImmutableList.of(State.DONE), ImmutableList.of(Optional.of("done")));
+    Flow flow3 =
+        testFlowService.createFlow(
+            createTestFlowCreationWithMultipleStages(accountCreator, change));
+    Flow flow4 = testFlowService.createFlow(createTestFlowCreation(accountCreator, change, 3));
+    flow4 =
+        testFlowService.evaluate(
+            flow4.key(),
+            ImmutableList.of(State.DONE, State.FAILED, State.TERMINATED),
+            ImmutableList.of(
+                Optional.empty(),
+                Optional.of("error"),
+                Optional.of("terminated because previous stage failed")));
+    try (Registration registration = extensionRegistry.newRegistration().set(testFlowService)) {
+      List<FlowInfo> flows = gApi.changes().id(change.id()).flows();
+      ImmutableMap<String, FlowInfo> flowInfosByUuid =
+          flows.stream().collect(toImmutableMap(flowInfo -> flowInfo.uuid, Function.identity()));
+      assertThat(flowInfosByUuid.keySet())
+          .containsExactly(
+              flow1.key().uuid(), flow2.key().uuid(), flow3.key().uuid(), flow4.key().uuid());
+      assertThat(flowInfosByUuid.get(flow1.key().uuid())).matches(flow1);
+      assertThat(flowInfosByUuid.get(flow2.key().uuid())).matches(flow2);
+      assertThat(flowInfosByUuid.get(flow3.key().uuid())).matches(flow3);
+      assertThat(flowInfosByUuid.get(flow4.key().uuid())).matches(flow4);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
index 72b3b39..3558e61 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
@@ -16,6 +16,7 @@
 
 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.block;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -524,6 +525,34 @@
   }
 
   @Test
+  public void adminCanCreateAccessChangeEvenIfReadAccessToRefsMetaConfigIsBlocked()
+      throws Exception {
+    projectOperations
+        .project(newProjectName)
+        .forUpdate()
+        .add(block(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(admin.id());
+
+    @SuppressWarnings("unused")
+    var unused = pApi().branch("refs/heads/master").get();
+
+    ProjectAccessInput accessInput = newProjectAccessInput();
+
+    AccessSectionInfo accessSection = newAccessSectionInfo();
+
+    PermissionInfo read = newPermissionInfo();
+    PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.DENY, false);
+    read.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+    accessSection.permissions.put(Permission.READ, read);
+    accessInput.add.put(REFS_HEADS, accessSection);
+
+    // This shouldn't fail with AuthException saying "refs/meta/config not visible error".
+    pApi().accessChange(accessInput);
+  }
+
+  @Test
   public void removePermission() throws Exception {
     // Add initial permission set
     ProjectAccessInput accessInput = newProjectAccessInput();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index fb6259b..d58f51a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -15,42 +15,44 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 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.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
-import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.truth.Correspondence;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.TestChange;
+import com.google.gerrit.acceptance.testsuite.change.TestPatchset;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -80,13 +82,18 @@
 
   @Test
   public void getCommitInfo() throws Exception {
-    Result result = createChange();
-    String commitId = result.getCommit().getId().name();
-    CommitInfo info = gApi.projects().name(project.get()).commit(commitId).get();
-    assertThat(info.commit).isEqualTo(commitId);
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestPatchset currentPatchSet = changeOperations.change(change.id()).currentPatchset().get();
+
+    CommitInfo info =
+        gApi.projects()
+            .name(change.project().get())
+            .commit(currentPatchSet.commitId().name())
+            .get();
+    assertThat(info.commit).isEqualTo(currentPatchSet.commitId().name());
     assertThat(info.parents.stream().map(c -> c.commit).collect(toList()))
-        .containsExactly(result.getCommit().getParent(0).name());
-    assertThat(info.subject).isEqualTo(result.getCommit().getShortMessage());
+        .containsExactly(currentPatchSet.commit().getParent(0).name());
+    assertThat(info.subject).isEqualTo(currentPatchSet.commit().getShortMessage());
     assertPerson(info.author, admin);
     assertPerson(info.committer, admin);
     assertThat(info.webLinks).isNull();
@@ -94,90 +101,138 @@
 
   @Test
   public void includedInOpenChange() throws Exception {
-    Result result = createChange();
-    assertThat(getIncludedIn(result.getCommit().getId()).branches).isEmpty();
-    assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty();
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestPatchset currentPatchSet = changeOperations.change(change.id()).currentPatchset().get();
+
+    IncludedInInfo includedIn = getIncludedIn(change.project(), currentPatchSet.commitId());
+    assertThat(includedIn.branches).isEmpty();
+    assertThat(includedIn.tags).isEmpty();
   }
 
   @Test
   public void includedInMergedChange() throws Exception {
-    Result result = createChange();
-    gApi.changes()
-        .id(result.getChangeId())
-        .revision(result.getCommit().name())
-        .review(ReviewInput.approve());
-    gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestPatchset currentPatchSet = changeOperations.change(change.id()).currentPatchset().get();
+    changeOperations.change(change.id()).newVote().codeReviewApproval().create();
+    gApi.changes().id(change.id()).current().submit();
 
-    assertThat(getIncludedIn(result.getCommit().getId()).branches).containsExactly("master");
-    assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty();
+    IncludedInInfo includedIn = getIncludedIn(change.project(), currentPatchSet.commitId());
+    assertThat(includedIn.branches).containsExactly("master");
+    assertThat(includedIn.tags).isEmpty();
+
+    createBranch(BranchNameKey.create(change.project(), "test-branch"));
 
     projectOperations
         .project(project)
         .forUpdate()
         .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(adminGroupUuid()))
         .update();
-    gApi.projects().name(result.getChange().project().get()).tag("test-tag").create(new TagInput());
+    gApi.projects().name(change.project().get()).tag("test-tag").create(new TagInput());
 
-    assertThat(getIncludedIn(result.getCommit().getId()).tags).containsExactly("test-tag");
-
-    createBranch(BranchNameKey.create(project, "test-branch"));
-
-    assertThat(getIncludedIn(result.getCommit().getId()).branches)
-        .containsExactly("master", "test-branch");
+    includedIn = getIncludedIn(change.project(), currentPatchSet.commitId());
+    assertThat(includedIn.branches).containsExactly("master", "test-branch");
+    assertThat(includedIn.tags).containsExactly("test-tag");
   }
 
   @Test
   public void includedInMergedChange_filtersOutNonVisibleBranches() throws Exception {
-    Result baseChange = createAndSubmitChange("refs/for/master");
+    TestChange change = changeOperations.newChange().createAndGet();
+    TestPatchset currentPatchSet = changeOperations.change(change.id()).currentPatchset().get();
+    changeOperations.change(change.id()).newVote().codeReviewApproval().create();
+    gApi.changes().id(change.id()).current().submit();
 
-    createBranch(BranchNameKey.create(project, "test-branch-1"));
-    createBranch(BranchNameKey.create(project, "test-branch-2"));
-    RevCommit changeCommit = createAndSubmitChange("refs/for/test-branch-1").getCommit();
-    // Reset repo back to the original state - otherwise all changes in tests have testChange as a
-    // parent.
-    testRepo.reset(changeCommit.getParent(0));
-    createAndSubmitChange("refs/for/test-branch-2");
+    createBranch(BranchNameKey.create(change.project(), "branch-1"));
+    createBranch(BranchNameKey.create(change.project(), "branch-2"));
 
-    assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
-        .containsExactly("master", "test-branch-1", "test-branch-2");
+    // Use a non-admin user, since admins can always see all branches.
+    requestScopeOperations.setApiUser(user.id());
+
+    assertThat(getIncludedIn(change.project(), currentPatchSet.commitId()).branches)
+        .containsExactly("master", "branch-1", "branch-2");
 
     projectOperations
-        .project(project)
+        .project(change.project())
         .forUpdate()
-        .add(block(Permission.READ).ref("refs/heads/test-branch-1").group(REGISTERED_USERS))
+        .add(block(Permission.READ).ref(RefNames.REFS_HEADS + "branch-1").group(REGISTERED_USERS))
         .update();
 
-    assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
-        .containsExactly("master", "test-branch-2");
+    assertThat(getIncludedIn(change.project(), currentPatchSet.commitId()).branches)
+        .containsExactly("master", "branch-2");
   }
 
   @Test
   public void includedInMergedChange_filtersOutNonVisibleTags() throws Exception {
-    String tagBase = "tag_base";
-    String tagBranch1 = "tag_1";
+    Project.NameKey project = projectOperations.newProject().create();
 
-    Result baseChange = createAndSubmitChange("refs/for/master");
-    createLightWeightTag(tagBase);
-    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags).containsExactly(tagBase);
+    // Create a change in master and submit it.
+    TestChange change =
+        changeOperations.newChange().project(project).branch("master").createAndGet();
+    TestPatchset currentPatchSet = changeOperations.change(change.id()).currentPatchset().get();
+    changeOperations.change(change.id()).newVote().codeReviewApproval().create();
+    gApi.changes().id(change.id()).current().submit();
 
-    createBranch(BranchNameKey.create(project, "test-branch-1"));
-    createAndSubmitChange("refs/for/test-branch-1");
-    createLightWeightTag(tagBranch1);
-    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags)
-        .containsExactly(tagBase, tagBranch1);
+    // Create a tag on the commit in master.
+    TagInput tagInput = new TagInput();
+    tagInput.ref = "tag-on-commit-in-master";
+    tagInput.revision = currentPatchSet.commitId().name();
+    TagInfo tagOnCommitInMaster =
+        gApi.projects().name(project.get()).tag(tagInput.ref).create(tagInput).get();
 
+    // Verify that the tag is returned when retrieving the refs that include the commit in the
+    // master branch.
+    assertThat(getIncludedIn(project, currentPatchSet.commit()).tags)
+        .containsExactly(tagName(tagOnCommitInMaster));
+
+    // Create another branch.
+    String otherBranch = "other";
+    createBranch(BranchNameKey.create(project, otherBranch));
+
+    // Create a change in the other branch and submit it. The base of this change is the change that
+    // we submitted to the master branch (since the new branch was created based on the master
+    // branch).
+    ChangeIdentifier followUpChange =
+        changeOperations.newChange().project(project).branch(otherBranch).create();
+    TestPatchset currentPatchSetFollowUpChange =
+        changeOperations.change(followUpChange).currentPatchset().get();
+    changeOperations.change(followUpChange).newVote().codeReviewApproval().create();
+    gApi.changes().id(followUpChange).current().submit();
+
+    // Create a tag on the commit in the other branch. This is a follow-up commit to the commit in
+    // the master branch. This means the created tag contains both commits, the commit in the other
+    // branch and its base, the commit in the master branch.
+    tagInput = new TagInput();
+    tagInput.ref = "tag-on-commit-in-other";
+    tagInput.revision = currentPatchSetFollowUpChange.commitId().name();
+    TagInfo tagOnCommitInOther =
+        gApi.projects().name(project.get()).tag(tagInput.ref).create(tagInput).get();
+
+    // Use a non-admin user, since admins can always see all tags.
+    requestScopeOperations.setApiUser(user.id());
+
+    // Verify that both tags are returned when retrieving the refs that include the commit in the
+    // master branch.
+    assertThat(getIncludedIn(project, currentPatchSet.commit()).tags)
+        .containsExactly(tagName(tagOnCommitInMaster), tagName(tagOnCommitInOther));
+
+    // Block read permissions on the other branch. Tags are only visible if they point to a commit
+    // that is contained in a visible branch. By blocking read permissions on the other branch the
+    // tag that points to the commit in the other branch becomes non-visible.
     projectOperations
         .project(project)
         .forUpdate()
-        // Tag permissions are controlled by read permissions on branches. Blocking read permission
-        // on test-branch-1 so that tagBranch1 becomes non-visible
-        .add(block(Permission.READ).ref("refs/heads/test-branch-1").group(REGISTERED_USERS))
+        .add(block(Permission.READ).ref(RefNames.REFS_HEADS + otherBranch).group(REGISTERED_USERS))
         .update();
-    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags).containsExactly(tagBase);
+
+    // Verify that the tag for the commit in the other branch is no longer returned when retrieving
+    // the refs that include the commit in the master branch (because that tag is not visible).
+    assertThat(getIncludedIn(project, currentPatchSet.commit()).tags)
+        .containsExactly(tagName(tagOnCommitInMaster));
   }
 
   @Test
   public void cherryPickWithoutConflicts() throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+
     String destBranch = "foo";
     createBranch(BranchNameKey.create(project, destBranch));
 
@@ -204,8 +259,11 @@
             .getCurrentRevision();
     assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(head.name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.getName());
     assertThat(currentRevision.conflicts.ours).isEqualTo(head.getName());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(r.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
   }
 
@@ -261,7 +319,7 @@
             .file("file")
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -316,7 +374,17 @@
 
   @Test
   public void cherryPickWithAllowConflicts() throws Exception {
-    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+    testCherryPickWithAllowConflicts(/* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void cherryPickWithAllowConflictsUsingDiff3() throws Exception {
+    testCherryPickWithAllowConflicts(/* useDiff3= */ true);
+  }
+
+  private void testCherryPickWithAllowConflicts(boolean useDiff3) throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Create a branch and push a commit to it (by-passing review)
     String destBranch = "foo";
@@ -331,7 +399,7 @@
     push.to("refs/heads/" + destBranch);
 
     // Create a change on master with a commit that conflicts with the commit on the other branch.
-    testRepo.reset(initial);
+    testRepo.reset(initialHead);
     String changeContent = "another content";
     push =
         pushFactory.create(
@@ -382,8 +450,11 @@
             .getCurrentRevision();
     assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(head.name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.getName());
     assertThat(currentRevision.conflicts.ours).isEqualTo(head.getName());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(r.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isTrue();
 
     // Verify that the file content in the cherry-pick change is correct.
@@ -397,26 +468,39 @@
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String destSha1 = abbreviateName(projectOperations.project(project).getHead(destBranch), 6);
-    String changeSha1 = abbreviateName(r.getCommit(), 6);
     assertThat(fileContent)
         .isEqualTo(
             "<<<<<<< HEAD   ("
-                + destSha1
+                + projectOperations.project(project).getHead(destBranch).getName()
                 + " test commit)\n"
                 + destContent
                 + "\n"
+                + (useDiff3
+                    ? String.format(
+                        "||||||| BASE   (%s %s)\n",
+                        initialHead.getName(), initialHead.getShortMessage())
+                    : "")
                 + "=======\n"
                 + changeContent
                 + "\n"
                 + ">>>>>>> CHANGE ("
-                + changeSha1
+                + r.getCommit().getName()
                 + " test commit)\n");
   }
 
   @Test
   public void cherryPickToExistingChangeWithAllowConflicts() throws Exception {
-    String tip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
+    testCherryPickToExistingChangeWithAllowConflicts(/* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void cherryPickToExistingChangeWithAllowConflictsUsingDiff3() throws Exception {
+    testCherryPickToExistingChangeWithAllowConflicts(/* useDiff3= */ true);
+  }
+
+  private void testCherryPickToExistingChangeWithAllowConflicts(boolean useDiff3) throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     String destBranch = "foo";
     createBranch(BranchNameKey.create(project, destBranch));
@@ -424,7 +508,7 @@
     PushOneCommit.Result existingChange =
         createChange(testRepo, destBranch, SUBJECT, FILE_NAME, destContent, null);
 
-    testRepo.reset(tip);
+    testRepo.reset(initialHead);
     String changeContent = "another content";
     PushOneCommit.Result srcChange =
         createChange(testRepo, "master", SUBJECT, FILE_NAME, changeContent, null);
@@ -473,8 +557,11 @@
     assertThat(currentRevision.commit.parents.get(0).commit)
         .isEqualTo(existingChange.getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.name());
     assertThat(currentRevision.conflicts.ours).isEqualTo(existingChange.getCommit().name());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(srcChange.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isTrue();
 
     // Verify that the file content in the cherry-pick change is correct.
@@ -488,20 +575,23 @@
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String destSha1 = abbreviateName(existingChange.getCommit(), 6);
-    String changeSha1 = abbreviateName(srcChange.getCommit(), 6);
     assertThat(fileContent)
         .isEqualTo(
             "<<<<<<< HEAD   ("
-                + destSha1
+                + existingChange.getCommit().getName()
                 + " test commit)\n"
                 + destContent
                 + "\n"
+                + (useDiff3
+                    ? String.format(
+                        "||||||| BASE   (%s %s)\n",
+                        initialHead.getName(), initialHead.getShortMessage())
+                    : "")
                 + "=======\n"
                 + changeContent
                 + "\n"
                 + ">>>>>>> CHANGE ("
-                + changeSha1
+                + srcChange.getCommit().getName()
                 + " test commit)\n");
   }
 
@@ -839,8 +929,8 @@
                 in.committerEmail, admin.id()));
   }
 
-  private IncludedInInfo getIncludedIn(ObjectId id) throws Exception {
-    return gApi.projects().name(project.get()).commit(id.name()).includedIn();
+  private IncludedInInfo getIncludedIn(Project.NameKey projectName, ObjectId id) throws Exception {
+    return gApi.projects().name(projectName.get()).commit(id.name()).includedIn();
   }
 
   private static void assertPerson(GitPerson actual, TestAccount expected) {
@@ -848,15 +938,9 @@
     assertThat(actual.name).isEqualTo(expected.fullName());
   }
 
-  private Result createAndSubmitChange(String branch) throws Exception {
-    Result r = createChange(branch);
-    approve(r.getChangeId());
-    gApi.changes().id(r.getChangeId()).current().submit();
-    return r;
-  }
-
-  private void createLightWeightTag(String tagName) throws Exception {
-    pushHead(testRepo, RefNames.REFS_TAGS + tagName, false, false);
+  private static String tagName(TagInfo tagInfo) {
+    assertThat(tagInfo.ref).startsWith(RefNames.REFS_TAGS);
+    return tagInfo.ref.substring(RefNames.REFS_TAGS.length());
   }
 
   private static Correspondence<RelatedChangeAndCommitInfo, Integer> hasId() {
diff --git a/javatests/com/google/gerrit/acceptance/api/project/LabelsReviewIT.java b/javatests/com/google/gerrit/acceptance/api/project/LabelsReviewIT.java
index 8e64325..bf7aef4 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/LabelsReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/LabelsReviewIT.java
@@ -15,15 +15,19 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.BatchLabelInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -82,4 +86,93 @@
         .asList()
         .containsExactly("+1 Looks Good", "0 Don't Know", "-1 Looks Bad");
   }
+
+  @Test
+  public void createLabelsChangeWithoutFunction() throws Exception {
+    Project.NameKey testProject = projectOperations.newProject().create();
+    LabelDefinitionInput fooInput = new LabelDefinitionInput();
+    fooInput.name = "Foo";
+    fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    BatchLabelInput input = new BatchLabelInput();
+    input.create = ImmutableList.of(fooInput);
+
+    ChangeInfo changeInfo = gApi.projects().name(testProject.get()).labelsReview(input);
+
+    assertThat(changeInfo.subject).isEqualTo("Review labels change");
+    Config config = new Config();
+    config.fromText(
+        gApi.changes()
+            .id(changeInfo.changeId)
+            .revision(1)
+            .file("project.config")
+            .content()
+            .asString());
+    assertThat(config.getString("label", "Foo", "function"))
+        .isEqualTo(LabelFunction.NO_OP.getFunctionName());
+  }
+
+  @Test
+  public void cannotCreateLabelsChangeCreatingLabelWithDeprecatedFunction() throws Exception {
+    for (LabelFunction deprecatedLabelFunction :
+        ImmutableList.of(
+            LabelFunction.ANY_WITH_BLOCK,
+            LabelFunction.MAX_NO_BLOCK,
+            LabelFunction.MAX_WITH_BLOCK)) {
+      Project.NameKey testProject = projectOperations.newProject().create();
+      LabelDefinitionInput fooInput = new LabelDefinitionInput();
+      fooInput.name = "Foo";
+      fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+      fooInput.function = deprecatedLabelFunction.getFunctionName();
+      BatchLabelInput input = new BatchLabelInput();
+      input.create = ImmutableList.of(fooInput);
+
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.projects().name(testProject.get()).labelsReview(input));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Function %s of label %s is deprecated. The function can only be set to %s. Use"
+                      + " submit requirements instead of label functions.",
+                  fooInput.function,
+                  fooInput.name,
+                  ImmutableList.of(
+                      LabelFunction.NO_BLOCK.getFunctionName(),
+                      LabelFunction.NO_OP.getFunctionName(),
+                      LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+    }
+  }
+
+  @Test
+  public void cannotCreateLabelsChangeUpdatingLabelToDeprecatedFunction() throws Exception {
+    for (LabelFunction deprecatedLabelFunction :
+        ImmutableList.of(
+            LabelFunction.ANY_WITH_BLOCK,
+            LabelFunction.MAX_NO_BLOCK,
+            LabelFunction.MAX_WITH_BLOCK)) {
+      LabelDefinitionInput labelInput = new LabelDefinitionInput();
+      labelInput.function = deprecatedLabelFunction.getFunctionName();
+      BatchLabelInput input = new BatchLabelInput();
+      input.update = ImmutableMap.of(LabelId.CODE_REVIEW, labelInput);
+
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.projects().name(allProjects.get()).labelsReview(input));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Function %s of label %s is deprecated. The function can only be set to %s. Use"
+                      + " submit requirements instead of label functions.",
+                  labelInput.function,
+                  LabelId.CODE_REVIEW,
+                  ImmutableList.of(
+                      LabelFunction.NO_BLOCK.getFunctionName(),
+                      LabelFunction.NO_OP.getFunctionName(),
+                      LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
index 9a926ea..babfd6a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectConfigIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -23,9 +24,11 @@
 import com.google.gerrit.acceptance.ExtensionRegistry;
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.entities.SubmitRequirement;
@@ -35,6 +38,7 @@
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ConfigValue;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.git.ObjectIds;
@@ -1234,6 +1238,139 @@
             + " 'label.Foo.copyCondition'): line 1:0 no viable alternative at input ':'");
   }
 
+  @Test
+  public void theDefaultForBooleanProjectConfigIsUsedIfNoDefaultIsConfigured() throws Exception {
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.ENABLE_SIGNED_PUSH))
+        .isEqualTo(BooleanProjectConfig.ENABLE_SIGNED_PUSH.getDefaultValue());
+  }
+
+  @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "change.workInProgressByDefault=true")
+  public void configureTrueAsDefaultForBooleanProjectConfig() throws Exception {
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT))
+        .isTrue();
+
+    // true can be overridden in the project
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateProject(
+              b ->
+                  b.setBooleanConfig(
+                      BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT, InheritableBoolean.FALSE));
+      u.save();
+    }
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT))
+        .isFalse();
+  }
+
+  @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.enableSignedPush=forced")
+  public void configureForcedAsDefaultForBooleanProjectConfig() throws Exception {
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.ENABLE_SIGNED_PUSH))
+        .isTrue();
+
+    // forced cannot be overridden in the project
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateProject(
+              b ->
+                  b.setBooleanConfig(
+                      BooleanProjectConfig.ENABLE_SIGNED_PUSH, InheritableBoolean.FALSE));
+      u.save();
+    }
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.ENABLE_SIGNED_PUSH))
+        .isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
+  public void configureFalseAsDefaultForBooleanProjectConfig() throws Exception {
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isFalse();
+
+    // false can be overridden in the project
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateProject(
+              b ->
+                  b.setBooleanConfig(
+                      BooleanProjectConfig.REJECT_IMPLICIT_MERGES, InheritableBoolean.TRUE));
+      u.save();
+    }
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.enableSignedPush=invalid")
+  public void invalidDefaultForBooleanProjectConfigIsIgnored() throws Exception {
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.ENABLE_SIGNED_PUSH))
+        .isEqualTo(BooleanProjectConfig.ENABLE_SIGNED_PUSH.getDefaultValue());
+    /* Set the boolean-config to !defaultValue. */
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateProject(
+              b ->
+                  b.setBooleanConfig(
+                      BooleanProjectConfig.ENABLE_SIGNED_PUSH,
+                      BooleanProjectConfig.ENABLE_SIGNED_PUSH.getDefaultValue()
+                          ? InheritableBoolean.FALSE
+                          : InheritableBoolean.TRUE));
+      u.save();
+    }
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.ENABLE_SIGNED_PUSH))
+        .isEqualTo(!BooleanProjectConfig.ENABLE_SIGNED_PUSH.getDefaultValue());
+  }
+
+  @Test
+  @GerritConfig(
+      name = "repository.*.defaultConfig",
+      values = {"receive.rejectImplicitMerges=false", "receive.rejectImplicitMerges=true"})
+  public void firstConfigureDefaultForBooleanProjectConfigApplies() throws Exception {
+    assertThat(
+            projectCache
+                .get(project)
+                .orElseThrow(illegalState(project))
+                .is(BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isFalse();
+  }
+
   private void testChangingCopyCondition(
       String initialCopyCondition, @Nullable String newCopyCondition) throws Exception {
     testChangingCopyCondition(
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 6f604d2..09cd037 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.CommentLinkInput;
@@ -64,6 +65,8 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
@@ -529,6 +532,37 @@
   }
 
   @Test
+  public void nonActiveProjectCanBeMadeActiveThroughCodeReview() throws Exception {
+    for (ProjectState nonActiveState :
+        ImmutableList.of(ProjectState.READ_ONLY, ProjectState.HIDDEN)) {
+      // ACTIVE => NON_ACTIVE
+      ConfigInput ci1 = new ConfigInput();
+      ci1.state = nonActiveState;
+      gApi.projects().name(project.get()).config(ci1);
+      assertThat(gApi.projects().name(project.get()).config().state).isEqualTo(nonActiveState);
+      // NON_ACTIVE => ACTIVE
+      ConfigInput ci2 = new ConfigInput();
+      ci2.state = ProjectState.ACTIVE;
+      ChangeInfo changeInfo = gApi.projects().name(project.get()).configReview(ci2);
+      ReviewInput reviewInput = new ReviewInput();
+      reviewInput.label("Code-Review", 2);
+      gApi.changes().id(changeInfo.project, changeInfo._number).current().review(reviewInput);
+
+      // check that the submit action is enabled
+      Map<String, ActionInfo> actions =
+          gApi.changes().id(changeInfo.project, changeInfo._number).current().actions();
+      assertThat(actions).containsKey("submit");
+      ActionInfo submitAction = actions.get("submit");
+      assertThat(submitAction.enabled).isTrue();
+
+      gApi.changes().id(changeInfo.project, changeInfo._number).current().submit();
+
+      // ACTIVE is represented as null in the API
+      assertThat(gApi.projects().name(project.get()).config().state).isNull();
+    }
+  }
+
+  @Test
   public void reindexProject() throws Exception {
     ProjectIndexedCounter projectIndexedCounter = new ProjectIndexedCounter();
     try (Registration registration =
@@ -1155,6 +1189,7 @@
       name = "experiments.disabled",
       // The test intentionally create an implicit merge change.
       value = "GerritBackendFeature__reject_implicit_merges_on_merge")
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void commitsIncludedInRefsMergedChangeNonTipCommit() throws Exception {
     String branchWithChange1 = R_HEADS + "branch-with-change1";
     String tagWithChange1 = R_TAGS + "tag-with-change1";
@@ -1190,6 +1225,8 @@
     createBranch(BranchNameKey.create(project, "non-visible-branch"));
     blockReadPermission(nonVisibleBranch);
 
+    // Use a non-admin user, since admins can always see all branches.
+    requestScopeOperations.setApiUser(user.id());
     assertThat(
             getCommitsIncludedInRefs(
                 change.getCommit().getName(), Arrays.asList(R_HEADS_MASTER, nonVisibleBranch)))
@@ -1205,6 +1242,8 @@
     // on master so that tag-with-change becomes non-visible
     blockReadPermission(R_HEADS_MASTER);
 
+    // Use a non-admin user, since admins can always see all tags.
+    requestScopeOperations.setApiUser(user.id());
     assertThat(
             getCommitsIncludedInRefs(
                 change.getCommit().getName(), Arrays.asList(R_HEADS_MASTER, nonVisibleTag)))
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
index 62b37c3..f34396e 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
@@ -21,12 +21,17 @@
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.NoHttpd;
 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.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.BatchSubmitRequirementInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -39,6 +44,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.inject.Inject;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import org.junit.Test;
 
@@ -46,6 +52,7 @@
 public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Test
   public void cannotGetANonExistingSR() throws Exception {
@@ -270,6 +277,7 @@
 
     assertThat(info.name).isEqualTo("code-review");
     assertThat(info.description).isEqualTo(input.description);
+    assertThat(info.projectName).isEqualTo(project.get());
     assertThat(info.applicabilityExpression).isEqualTo(input.applicabilityExpression);
     assertThat(info.applicabilityExpression).isEqualTo(input.applicabilityExpression);
     assertThat(info.submittabilityExpression).isEqualTo(input.submittabilityExpression);
@@ -589,7 +597,71 @@
     infos = gApi.projects().name(project.get()).submitRequirements().withInherited(true).get();
 
     assertThat(names(infos))
-        .containsExactly("No-Unresolved-Comments", "Code-Review", "base-sr", "sr-1", "sr-2");
+        .containsExactly("Code-Review", "No-Unresolved-Comments", "base-sr", "sr-1", "sr-2")
+        .inOrder();
+    assertThat(infos.get(0).name).isEqualTo("Code-Review");
+    assertThat(infos.get(0).projectName).isEqualTo(allProjects.get());
+    assertThat(infos.get(1).name).isEqualTo("No-Unresolved-Comments");
+    assertThat(infos.get(1).projectName).isEqualTo(allProjects.get());
+    assertThat(infos.get(2).name).isEqualTo("base-sr");
+    assertThat(infos.get(2).projectName).isEqualTo(allProjects.get());
+    assertThat(infos.get(3).name).isEqualTo("sr-1");
+    assertThat(infos.get(3).projectName).isEqualTo(project.get());
+    assertThat(infos.get(4).name).isEqualTo("sr-2");
+    assertThat(infos.get(4).projectName).isEqualTo(project.get());
+  }
+
+  @Test
+  public void listSRsWithInheritanceIncludesGlobalSubmitRequirements() throws Exception {
+    SubmitRequirement globalSubmitRequirement =
+        SubmitRequirement.builder()
+            .setName("Global-Submit-Requirement")
+            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+            .setAllowOverrideInChildProjects(false)
+            .build();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+      List<SubmitRequirementInfo> infos =
+          gApi.projects().name(project.get()).submitRequirements().withInherited(true).get();
+
+      assertThat(names(infos))
+          .containsExactly("Global-Submit-Requirement", "Code-Review", "No-Unresolved-Comments")
+          .inOrder();
+      assertThat(infos.get(0).name).isEqualTo("Global-Submit-Requirement");
+      assertThat(infos.get(0).projectName).isNull();
+      assertThat(infos.get(1).name).isEqualTo("Code-Review");
+      assertThat(infos.get(1).projectName).isEqualTo(allProjects.get());
+      assertThat(infos.get(2).name).isEqualTo("No-Unresolved-Comments");
+      assertThat(infos.get(2).projectName).isEqualTo(allProjects.get());
+    }
+  }
+
+  @Test
+  public void listGlobalSubmitRequirements() throws Exception {
+    SubmitRequirement globalSubmitRequirement =
+        SubmitRequirement.builder()
+            .setName("Global-Submit-Requirement")
+            .setDescription(Optional.of("a global submit requirement"))
+            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+            .setAllowOverrideInChildProjects(false)
+            .build();
+
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+      ImmutableList<SubmitRequirementInfo> infos =
+          gApi.config().server().listGlobalSubmitRequirements().get();
+      assertThat(names(infos)).containsExactly(globalSubmitRequirement.name());
+
+      SubmitRequirementInfo submitRequirement = Iterables.getOnlyElement(infos);
+      assertThat(submitRequirement.projectName).isNull();
+      assertThat(submitRequirement.description)
+          .isEqualTo(globalSubmitRequirement.description().get());
+      assertThat(submitRequirement.applicabilityExpression).isNull();
+      assertThat(submitRequirement.submittabilityExpression)
+          .isEqualTo(globalSubmitRequirement.submittabilityExpression().expressionString());
+      assertThat(submitRequirement.overrideExpression).isNull();
+      assertThat(submitRequirement.allowOverrideInChildProjects).isFalse();
+    }
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
index 7f951d9..e46f0df 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
@@ -116,7 +116,7 @@
             .file(FILE_NAME)
             .content(FILE_CONTENT)
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java
index 1918395..454675b 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/CommentWithFixIT.java
@@ -501,9 +501,9 @@
             .file(FILE_NAME)
             .content(FILE_CONTENT)
             .owner(testUser)
-            .create();
+            .createV1();
 
-    // Add Robot Comment to the change
+    // Add comment to the change
     fixReplacementInfo.path = FILE_NAME;
     fixReplacementInfo.replacement = "Modified content";
     fixReplacementInfo.range = createRange(3, 1, 3, 3);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
index d61c2d6..00d61b3 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
@@ -68,7 +68,7 @@
   @Test
   public void onlyCommentsBeforeTargetPatchsetArePorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     PatchSet.Id patchset3Id = changeOps.change(changeId).newPatchset().create();
@@ -85,7 +85,7 @@
   @Test
   public void commentsOnAnyPatchsetBeforeTargetPatchsetArePorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     changeOps.change(changeId).newPatchset().create();
     PatchSet.Id patchset3Id = changeOps.change(changeId).newPatchset().create();
@@ -104,7 +104,7 @@
   @Test
   public void severalCommentsFromEarlierPatchsetArePorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -121,7 +121,7 @@
   @Test
   public void completeCommentThreadIsPorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -139,7 +139,7 @@
   @Test
   public void onlyUnresolvedPublishedCommentsArePorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -188,7 +188,7 @@
   public void resolvedAndUnresolvedDraftCommentsArePorted() throws Exception {
     Account.Id accountId = accountOps.newAccount().create();
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -206,7 +206,7 @@
   @Test
   public void unresolvedStateOfLastCommentInThreadMatters() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -226,7 +226,7 @@
   @Test
   public void unresolvedStateOfLastCommentByDateMattersForBranchedThreads() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments. Comments should be more than 1 second apart as NoteDb only supports second
@@ -257,7 +257,7 @@
   public void unresolvedStateOfDraftCommentsIsIgnoredForPublishedComments() throws Exception {
     Account.Id accountId = accountOps.newAccount().create();
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -281,7 +281,7 @@
   public void draftCommentsAreNotPortedViaApiForPublishedComments() throws Exception {
     Account.Id accountId = accountOps.newAccount().create();
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add draft comment.
@@ -298,7 +298,7 @@
   public void publishedCommentsAreNotPortedViaApiForDraftComments() throws Exception {
     Account.Id accountId = accountOps.newAccount().create();
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -314,7 +314,7 @@
   public void draftCommentCanBePorted() throws Exception {
     Account.Id accountId = accountOps.newAccount().create();
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add draft comment.
@@ -331,7 +331,7 @@
     Account.Id userId = accountOps.newAccount().create();
     Account.Id otherUserId = accountOps.newAccount().create();
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add draft comment.
@@ -346,7 +346,8 @@
   @Test
   public void publishedCommentsOfAllTypesArePorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").create();
+    Change.Id changeId =
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -377,7 +378,7 @@
   @Test
   public void commentOnParentCommitIsPorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -391,7 +392,7 @@
   @Test
   public void commentOnInvalidParentIsPorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -405,7 +406,7 @@
   @Test
   public void commentsOnInvalidPositionArePorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\n").create();
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -422,7 +423,7 @@
   @Test
   public void commentsOnInvalidPositionKeepTheirInvalidPosition() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -436,7 +437,7 @@
   @Test
   public void portedCommentHasOriginalUuid() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -450,7 +451,7 @@
   @Test
   public void portedCommentHasOriginalPatchset() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -465,7 +466,7 @@
   public void portedDraftCommentHasPatchsetFilled() throws Exception {
     // Set up change and patchsets.
     Account.Id authorId = accountOps.newAccount().create();
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -484,7 +485,7 @@
   @Test
   public void portedCommentHasOriginalPatchsetCommitId() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -498,7 +499,7 @@
   @Test
   public void portedCommentHasOriginalMessage() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -512,7 +513,7 @@
   @Test
   public void portedReplyStillRefersToParentComment() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comments.
@@ -529,7 +530,7 @@
   public void portedPublishedCommentHasOriginalAuthor() throws Exception {
     // Set up change and patchsets.
     Account.Id authorId = accountOps.newAccount().create();
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -537,12 +538,12 @@
 
     CommentInfo portedComment = getPortedComment(patchset2Id, commentUuid);
 
-    assertThat(portedComment).author().id().isEqualTo(authorId.get());
+    assertThat(portedComment).author().hasIdThat().isEqualTo(authorId.get());
   }
 
   @Test
   public void anonymousUsersGetAuthExceptionForPortedDrafts() throws Exception {
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchsetId = changeOps.change(changeId).currentPatchset().get().patchsetId();
 
     requestScopeOps.setApiUserAnonymous();
@@ -563,7 +564,7 @@
   public void portedDraftCommentHasNoAuthor() throws Exception {
     // Set up change and patchsets.
     Account.Id authorId = accountOps.newAccount().create();
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -580,7 +581,7 @@
   @Test
   public void portedCommentHasOriginalTag() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     TestPatchset patchset1 = changeOps.change(changeId).currentPatchset().get();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -594,7 +595,7 @@
   @Test
   public void portedCommentHasUpdatedTimestamp() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -608,7 +609,7 @@
   @Test
   public void portedCommentDoesNotHaveChangeMessageId() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -625,7 +626,7 @@
   @Test
   public void pathOfPortedCommentIsOnlyIndicatedInMap() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1").create();
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -642,7 +643,7 @@
   public void portedRangeCommentCanHandleAddedLines() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -673,7 +674,7 @@
   public void portedRangeCommentCanHandleDeletedLines() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -704,7 +705,7 @@
   public void portedRangeCommentCanHandlePureRename() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
@@ -732,7 +733,7 @@
   public void portedRangeCommentCanHandleRenameWithLineShift() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -767,7 +768,7 @@
   public void portedRangeCommentAdditionallyAppearsOnCopyAtIndependentPosition() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     // Gerrit currently only identifies a copy if a rename also happens at the same time. Modify the
     // renamed file slightly different than the copied file so that the end location of the comment
@@ -814,7 +815,7 @@
   public void lineOfPortedRangeCommentFollowsContract() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -843,7 +844,7 @@
   public void portedRangeCommentBecomesFileCommentOnConflict() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -874,7 +875,7 @@
   public void portedRangeCommentEndingOnLineJustBeforeModificationCanBePorted() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -905,7 +906,7 @@
   public void portedRangeCommentEndingAtStartOfModifiedLineCanBePorted() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -936,7 +937,7 @@
   public void portedRangeCommentEndingWithinModifiedLineBecomesFileComment() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -965,7 +966,7 @@
   public void portedRangeCommentWithinModifiedLineBecomesFileComment() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -995,7 +996,7 @@
       throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1024,7 +1025,7 @@
   public void portedRangeCommentStartingOnLineJustAfterModificationCanBePorted() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1061,7 +1062,7 @@
       throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1090,7 +1091,7 @@
   public void portedRangeCommentBecomesPatchsetLevelCommentOnFileDeletion() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").delete().create();
@@ -1114,7 +1115,8 @@
   @Test
   public void overlappingRangeCommentsArePortedToNewPosition() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").create();
+    Change.Id changeId =
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1151,7 +1153,7 @@
   public void portedLineCommentCanHandleAddedLines() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1172,7 +1174,7 @@
   public void portedLineCommentCanHandleDeletedLines() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1193,7 +1195,7 @@
   public void portedLineCommentCanHandlePureRename() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
@@ -1211,7 +1213,7 @@
   public void portedLineCommentCanHandleRenameWithLineShift() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     changeOps
         .change(changeId)
@@ -1235,7 +1237,7 @@
   public void portedLineCommentAdditionallyAppearsOnCopyAtIndependentPosition() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     // Gerrit currently only identifies a copy if a rename also happens at the same time. Modify the
     // renamed file slightly different than the copied file so that the end location of the comment
@@ -1269,7 +1271,7 @@
   public void portedLineCommentBecomesFileCommentOnConflict() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1292,7 +1294,7 @@
   public void portedLineCommentOnLineJustBeforeModificationCanBePorted() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1313,7 +1315,7 @@
   public void portedLineCommentOnStartLineOfModificationBecomesFileComment() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1334,7 +1336,7 @@
   public void portedLineCommentOnLastLineOfModificationBecomesFileComment() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1355,7 +1357,7 @@
   public void portedLineCommentOnLineJustAfterModificationCanBePorted() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1376,7 +1378,7 @@
   public void portedLineCommentBecomesPatchsetLevelCommentOnFileDeletion() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").delete().create();
@@ -1393,7 +1395,8 @@
   @Test
   public void overlappingLineCommentsArePortedToNewPosition() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").create();
+    Change.Id changeId =
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1416,7 +1419,7 @@
   public void portedFileCommentIsObliviousToAdjustedFileContent() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1437,7 +1440,7 @@
   public void portedFileCommentCanHandleRename() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
@@ -1455,7 +1458,7 @@
   public void portedFileCommentAdditionallyAppearsOnCopy() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1480,7 +1483,7 @@
   public void portedFileCommentBecomesPatchsetLevelCommentOnFileDeletion() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").delete().create();
@@ -1498,7 +1501,7 @@
   public void portedPatchsetLevelCommentIsObliviousToAdjustedFileContent() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1519,7 +1522,7 @@
   public void portedPatchsetLevelCommentIsObliviousToRename() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").create();
+        changeOps.newChange().file("myFile").content("Line 1\nLine 2\nLine 3\nLine 4\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").renameTo("newFileName").create();
@@ -1535,7 +1538,7 @@
   public void commentOnCommitMessageIsPortedToNewPosition() throws Exception {
     // Set up change and patchsets.
     Change.Id changeId =
-        changeOps.newChange().commitMessage("Summary line\n\nText 1\nText 2").create();
+        changeOps.newChange().commitMessage("Summary line\n\nText 1\nText 2").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps
@@ -1560,15 +1563,17 @@
   @Test
   public void commentOnParentIsPortedToNewPosition() throws Exception {
     // Set up change and patchsets.
-    Change.Id parentChangeId = changeOps.newChange().file("myFile").content("Line 1\n").create();
+    Change.Id parentChangeId =
+        changeOps.newChange().project(project).file("myFile").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
+            .project(project)
             .childOf()
             .change(parentChangeId)
             .file("myFile")
             .content("Line one\n")
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id parentPatchset2Id =
@@ -1592,18 +1597,21 @@
   @Test
   public void commentOnFirstParentIsPortedToNewPosition() throws Exception {
     // Set up change and patchsets.
-    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
-    Change.Id parent2ChangeId = changeOps.newChange().file("file2").content("Line 1\n").create();
+    Change.Id parent1ChangeId =
+        changeOps.newChange().project(project).file("file1").content("Line 1\n").createV1();
+    Change.Id parent2ChangeId =
+        changeOps.newChange().project(project).file("file2").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
             .file("file1")
             .content("Line one\n")
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id parent1Patchset2Id =
@@ -1634,18 +1642,21 @@
   @Test
   public void commentOnSecondParentIsPortedToNewPosition() throws Exception {
     // Set up change and patchsets.
-    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
-    Change.Id parent2ChangeId = changeOps.newChange().file("file2").content("Line 1\n").create();
+    Change.Id parent1ChangeId =
+        changeOps.newChange().project(project).file("file1").content("Line 1\n").createV1();
+    Change.Id parent2ChangeId =
+        changeOps.newChange().project(project).file("file2").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
             .file("file2")
             .content("Line one\n")
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id parent2Patchset2Id =
@@ -1677,16 +1688,19 @@
   public void commentOnAutoMergeCommitIsPortedToNewPosition() throws Exception {
     // Set up change and patchsets. Use the same file so that there's a meaningful auto-merge
     // commit/diff.
-    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
-    Change.Id parent2ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
+    Change.Id parent1ChangeId =
+        changeOps.newChange().project(project).file("file1").content("Line 1\n").createV1();
+    Change.Id parent2ChangeId =
+        changeOps.newChange().project(project).file("file1").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id parent1Patchset2Id =
@@ -1729,16 +1743,19 @@
   public void commentOnFirstParentIsPortedToSingleParentWhenPatchsetChangedToNonMergeCommit()
       throws Exception {
     // Set up change and patchsets.
-    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
-    Change.Id parent2ChangeId = changeOps.newChange().file("file2").content("Line 1\n").create();
+    Change.Id parent1ChangeId =
+        changeOps.newChange().project(project).file("file1").content("Line 1\n").createV1();
+    Change.Id parent2ChangeId =
+        changeOps.newChange().project(project).file("file2").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id parent1PatchsetId2 =
@@ -1770,16 +1787,19 @@
   public void commentOnSecondParentBecomesPatchsetLevelCommentWhenPatchsetChangedToNonMergeCommit()
       throws Exception {
     // Set up change and patchsets.
-    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
-    Change.Id parent2ChangeId = changeOps.newChange().file("file2").content("Line 1\n").create();
+    Change.Id parent1ChangeId =
+        changeOps.newChange().project(project).file("file1").content("Line 1\n").createV1();
+    Change.Id parent2ChangeId =
+        changeOps.newChange().project(project).file("file2").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id childPatchset2Id =
@@ -1804,8 +1824,8 @@
       commentOnAutoMergeCommitBecomesPatchsetLevelCommentWhenPatchsetChangedToNonMergeCommit()
           throws Exception {
     // Set up change and patchsets.
-    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
-    Change.Id parent2ChangeId = changeOps.newChange().file("file1").content("Line 1\n").create();
+    Change.Id parent1ChangeId = changeOps.newChange().file("file1").content("Line 1\n").createV1();
+    Change.Id parent2ChangeId = changeOps.newChange().file("file1").content("Line 1\n").createV1();
     Change.Id childChangeId =
         changeOps
             .newChange()
@@ -1813,7 +1833,7 @@
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
     PatchSet.Id childPatchset1Id =
         changeOps.change(childChangeId).currentPatchset().get().patchsetId();
     PatchSet.Id childPatchset2Id =
@@ -1833,7 +1853,7 @@
   @Test
   public void whitespaceOnlyModificationsAreAlsoConsideredWhenPorting() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\n").create();
+    Change.Id changeId = changeOps.newChange().file("myFile").content("Line 1\n").createV1();
     PatchSet.Id patchset1Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchset2Id =
         changeOps.change(changeId).newPatchset().file("myFile").content("\nLine 1\n").create();
@@ -1848,7 +1868,7 @@
   @Test
   public void deletedCommentContentIsNotPorted() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchsetId1 = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchsetId2 = changeOps.change(changeId).newPatchset().create();
     // Add comment.
@@ -1868,7 +1888,7 @@
   @Test
   public void setOfPortedCommentsCanChangeOnRepeatedCalls() throws Exception {
     // Set up change and patchsets.
-    Change.Id changeId = changeOps.newChange().create();
+    Change.Id changeId = changeOps.newChange().createV1();
     PatchSet.Id patchsetId1 = changeOps.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id patchsetId2 = changeOps.change(changeId).newPatchset().create();
     // Add comment.
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java
index c5dba34..8635b15 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java
@@ -170,6 +170,24 @@
   }
 
   @Test
+  public void previewFixWithBadRange() throws Exception {
+    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+    fixReplacementInfo1.path = FILE_NAME;
+    fixReplacementInfo1.replacement = "some replacement code";
+    fixReplacementInfo1.range = createRange(30000, 9, 8, 4);
+
+    List<FixReplacementInfo> fixReplacementInfoList = Arrays.asList(fixReplacementInfo1);
+    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
+    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;
+
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(changeId).current().getFixPreview(applyProvidedFixInput));
+    assertThat(exception).hasMessageThat().startsWith("replacement is not valid: ");
+  }
+
+  @Test
   public void previewFixForDifferentPatchset() throws Exception {
     int previousRevision = gApi.changes().id(changeId).get().currentRevisionNumber;
     amendChange(changeId);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 7eb33b6..56da33d 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -3049,7 +3049,7 @@
             .newChange()
             .file(imageFileName)
             .content(new String(imageBytes, UTF_8))
-            .create();
+            .createV1();
 
     DiffInfo diffInfo = gApi.changes().id(changeId.get()).current().file(imageFileName).diff();
 
@@ -3068,19 +3068,21 @@
     Change.Id changeId1 =
         changeOperations
             .newChange()
+            .project(project)
             .file(imageFileName)
             .content(new String(imageBytes, UTF_8))
-            .create();
+            .createV1();
 
     byte[] newImageBytes = createRgbImage(0, 255, 0);
     Change.Id changeId2 =
         changeOperations
             .newChange()
+            .project(project)
             .childOf()
             .change(changeId1)
             .file(imageFileName)
             .content(new String(newImageBytes, UTF_8))
-            .create();
+            .createV1();
 
     DiffInfo diffInfo = gApi.changes().id(changeId2.get()).current().file(imageFileName).diff();
 
@@ -3106,18 +3108,20 @@
     Change.Id changeId1 =
         changeOperations
             .newChange()
+            .project(project)
             .file(imageFileName)
             .content(new String(imageBytes, UTF_8))
-            .create();
+            .createV1();
 
     Change.Id changeId2 =
         changeOperations
             .newChange()
+            .project(project)
             .childOf()
             .change(changeId1)
             .file(imageFileName)
             .delete()
-            .create();
+            .createV1();
 
     DiffInfo diffInfo = gApi.changes().id(changeId2.get()).current().file(imageFileName).diff();
 
@@ -3142,18 +3146,24 @@
     byte[] imageBytes1 = createRgbImage(255, 0, 0);
     String imageContent1 = new String(imageBytes1, UTF_8);
     Change.Id changeId1 =
-        changeOperations.newChange().file(imageFileName1).content(imageContent1).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .file(imageFileName1)
+            .content(imageContent1)
+            .createV1();
 
     String imageFileName2 = "another_image.png";
     byte[] imageBytes2 = createRgbImage(0, 255, 0);
     Change.Id changeId2 =
         changeOperations
             .newChange()
+            .project(project)
             .childOf()
             .change(changeId1)
             .file(imageFileName2)
             .content(new String(imageBytes2, UTF_8))
-            .create();
+            .createV1();
 
     // Since file imageFileName1 was not touched in the second change, trying to get the diff for it
     // should probably fail with '404 Not Found'.
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 92a6273..6f43935 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -31,7 +31,6 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -334,6 +333,8 @@
 
   @Test
   public void cherryPick() throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+
     PushOneCommit.Result r = pushTo("refs/for/master%topic=someTopic");
     CherryPickInput in = new CherryPickInput();
     in.destination = "foo";
@@ -354,8 +355,11 @@
     assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(head.name());
     assertThat(currentRevision.conflicts).isNotNull();
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.getName());
     assertThat(currentRevision.conflicts.ours).isEqualTo(head.getName());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(r.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
 
     ChangeInfo cherryPickChangeInfoWithDetails = cherry.get();
     assertThat(cherryPickChangeInfoWithDetails.workInProgress).isNull();
@@ -389,7 +393,7 @@
             .file("file")
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -597,17 +601,56 @@
     ChangeApi change = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
     CherryPickInput in = new CherryPickInput();
     in.destination = "master";
-    in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
-    ChangeInfo cherryInfo = change.revision(r.getCommit().name()).cherryPick(in).get();
+    ChangeInfo cherryInfo = change.current().cherryPick(in).get();
     assertThat(cherryInfo.messages).hasSize(2);
-    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+    Iterator<ChangeMessageInfo> cherryMessageIt = cherryInfo.messages.iterator();
     assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
 
     // Existing change was updated.
     assertThat(cherryInfo._number).isEqualTo(change.get()._number);
+    assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
     assertThat(cherryInfo.cherryPickOfPatchSet).isEqualTo(1);
-    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
-    assertThat(cherryIt.next().message).isEqualTo("Patch Set 2: Cherry Picked from branch master.");
+    assertThat(cherryMessageIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryMessageIt.next().message)
+        .isEqualTo("Patch Set 2: Cherry Picked from branch master.");
+  }
+
+  @Test
+  public void restoreOldPatchSetByCherryPickToSameBranch() throws Exception {
+    Change.Id changeId =
+        changeOperations.newChange().project(project).file("a.txt").content("aContent").createV1();
+    ChangeApi change = gApi.changes().id(project.get(), changeId.get());
+
+    // Amend the change, deleting file a.txt and adding file b.txt.
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .file("a.txt")
+        .delete()
+        .file("b.txt")
+        .content("bContent")
+        .create();
+
+    // Restore patch set 1 by cherry-picking it.
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    ChangeInfo cherryInfo = change.revision(1).cherryPick(in).get();
+    assertThat(cherryInfo.messages).hasSize(3);
+    Iterator<ChangeMessageInfo> cherryMessageIt = cherryInfo.messages.iterator();
+    assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
+
+    // Existing change was updated.
+    assertThat(cherryInfo._number).isEqualTo(change.get()._number);
+    assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
+    assertThat(cherryInfo.cherryPickOfPatchSet).isEqualTo(1);
+    assertThat(cherryMessageIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryMessageIt.next().message).isEqualTo("Uploaded patch set 2.");
+    assertThat(cherryMessageIt.next().message)
+        .isEqualTo("Patch Set 3: Cherry Picked from branch master.");
+
+    // File a.txt has been restored and b.txt has been removed.
+    Map<String, FileInfo> files = change.current().files();
+    assertThat(files.keySet()).containsExactly("a.txt", COMMIT_MSG);
   }
 
   @Test
@@ -713,7 +756,17 @@
 
   @Test
   public void cherryPickConflictWithAllowConflicts() throws Exception {
-    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+    testCherryPickConflictWithAllowConflicts(/* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void cherryPickConflictWithAllowConflictsUsingDiff3() throws Exception {
+    testCherryPickConflictWithAllowConflicts(/* useDiff3= */ true);
+  }
+
+  private void testCherryPickConflictWithAllowConflicts(boolean useDiff3) throws Exception {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
 
     // Create a branch and push a commit to it (by-passing review)
     String destBranch = "foo";
@@ -728,7 +781,7 @@
     push.to("refs/heads/" + destBranch);
 
     // Create a change on master with a commit that conflicts with the commit on the other branch.
-    testRepo.reset(initial);
+    testRepo.reset(initialHead);
     String changeContent = "another content";
     push =
         pushFactory.create(
@@ -768,8 +821,11 @@
     assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(head.name());
     assertThat(currentRevision.conflicts).isNotNull();
     assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+    assertThat(currentRevision.conflicts.base).isEqualTo(initialHead.getName());
     assertThat(currentRevision.conflicts.ours).isEqualTo(head.getName());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(r.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
 
     // Verify that subject and topic on the cherry-pick change have been correctly populated.
     assertThat(cherryPickChange.subject).contains(in.message);
@@ -786,20 +842,23 @@
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String destSha1 = abbreviateName(projectOperations.project(project).getHead(destBranch), 6);
-    String changeSha1 = abbreviateName(r.getCommit(), 6);
     assertThat(fileContent)
         .isEqualTo(
             "<<<<<<< HEAD   ("
-                + destSha1
+                + projectOperations.project(project).getHead(destBranch).getName()
                 + " test commit)\n"
                 + destContent
                 + "\n"
+                + (useDiff3
+                    ? String.format(
+                        "||||||| BASE   (%s %s)\n",
+                        initialHead.getName(), initialHead.getShortMessage())
+                    : "")
                 + "=======\n"
                 + changeContent
                 + "\n"
                 + ">>>>>>> CHANGE ("
-                + changeSha1
+                + r.getCommit().getName()
                 + " test commit)\n");
 
     // Get details of cherry-pick change.
@@ -848,8 +907,11 @@
         .isEqualTo(existingChange.getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
     assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+    assertThat(currentRevision.conflicts.base).isEqualTo(tip);
     assertThat(currentRevision.conflicts.ours).isEqualTo(existingChange.getCommit().name());
     assertThat(currentRevision.conflicts.theirs).isEqualTo(srcChange.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
   }
 
   @Test
@@ -1345,6 +1407,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void cherryPickToMergedChangeRevision() throws Exception {
     createBranch(BranchNameKey.create(project, "foo"));
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
deleted file mode 100644
index 10d1c77..0000000
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ /dev/null
@@ -1,1753 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.api.revision;
-
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.common.collect.MoreCollectors.onlyElement;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
-import static com.google.gerrit.acceptance.server.change.CommentsUtil.createFixReplacementInfo;
-import static com.google.gerrit.acceptance.server.change.CommentsUtil.createFixSuggestionInfo;
-import static com.google.gerrit.acceptance.server.change.CommentsUtil.createRange;
-import static com.google.gerrit.entities.Patch.COMMIT_MSG;
-import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
-import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
-import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
-import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.UseClockStep;
-import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
-import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
-import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.ChangeType;
-import com.google.gerrit.extensions.common.DiffInfo;
-import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
-import com.google.gerrit.extensions.common.EditInfo;
-import com.google.gerrit.extensions.common.FixReplacementInfo;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
-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.testing.BinaryResultSubject;
-import com.google.gerrit.testing.TestCommentHelper;
-import com.google.gerrit.testing.TestTimeUtil;
-import com.google.inject.Inject;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import org.junit.Before;
-import org.junit.Test;
-
-public class RobotCommentsIT extends AbstractDaemonTest {
-  @Inject private TestCommentHelper testCommentHelper;
-  @Inject private ChangeOperations changeOperations;
-  @Inject private AccountOperations accountOperations;
-  @Inject private RequestScopeOperations requestScopeOperations;
-
-  private static final String PLAIN_TEXT_CONTENT_TYPE = "text/plain";
-  private static final String GERRIT_COMMIT_MESSAGE_TYPE = "text/x-gerrit-commit-message";
-
-  private static final String FILE_NAME = "file_to_fix.txt";
-  private static final String FILE_NAME2 = "another_file_to_fix.txt";
-  private static final String FILE_NAME3 = "file_without_newline_at_end.txt";
-  private static final String FILE_CONTENT =
-      "First line\nSecond line\nThird line\nFourth line\nFifth line\nSixth line"
-          + "\nSeventh line\nEighth line\nNinth line\nTenth line\n";
-  private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
-  private static final String FILE_CONTENT3 = "1st line\n2nd line";
-
-  private String changeId;
-  private String commitId;
-  private FixReplacementInfo fixReplacementInfo;
-  private FixSuggestionInfo fixSuggestionInfo;
-  private RobotCommentInput withFixRobotCommentInput;
-
-  @Before
-  public void setUp() throws Exception {
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            "Provide files which can be used for fixes",
-            ImmutableMap.of(
-                FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2, FILE_NAME3, FILE_CONTENT3));
-    PushOneCommit.Result changeResult = push.to("refs/for/master");
-    changeId = changeResult.getChangeId();
-    commitId = changeResult.getCommit().getName();
-
-    fixReplacementInfo = createFixReplacementInfo();
-    fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfo);
-    withFixRobotCommentInput =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo);
-  }
-
-  @Test
-  public void retrievingRobotCommentsBeforeAddingAnyDoesNotRaiseAnException() throws Exception {
-    Map<String, List<RobotCommentInfo>> robotComments =
-        gApi.changes().id(changeId).current().robotComments();
-
-    assertThat(robotComments).isNotNull();
-    assertThat(robotComments).isEmpty();
-  }
-
-  @Test
-  public void addedRobotCommentsCanBeRetrieved() throws Exception {
-    RobotCommentInput in = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    testCommentHelper.addRobotComment(changeId, in);
-
-    Map<String, List<RobotCommentInfo>> out = gApi.changes().id(changeId).current().robotComments();
-
-    assertThat(out).hasSize(1);
-    RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path));
-    assertRobotComment(comment, in, false);
-  }
-
-  @Test
-  public void addedRobotCommentsCanBeRetrievedByChange() throws Exception {
-    RobotCommentInput in = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    testCommentHelper.addRobotComment(changeId, in);
-
-    pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master");
-
-    RobotCommentInput in2 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    testCommentHelper.addRobotComment(changeId, in2);
-
-    Map<String, List<RobotCommentInfo>> out = gApi.changes().id(changeId).robotComments();
-
-    assertThat(out).hasSize(1);
-    assertThat(out.get(in.path)).hasSize(2);
-
-    RobotCommentInfo comment1 = out.get(in.path).get(0);
-    assertRobotComment(comment1, in, false);
-    RobotCommentInfo comment2 = out.get(in.path).get(1);
-    assertRobotComment(comment2, in2, false);
-  }
-
-  @UseClockStep
-  @Test
-  public void addedRobotCommentsAreLinkedToChangeMessages() throws Exception {
-    // Advancing the time after creating the change so that the first robot comment is not in the
-    // same timestamp as with the change creation.
-    TestTimeUtil.incrementClock(10, TimeUnit.SECONDS);
-
-    RobotCommentInput c1 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    RobotCommentInput c2 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    RobotCommentInput c3 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-
-    // Give the robot comments identifiable names for testing
-    c1.message = "robot comment 1";
-    c2.message = "robot comment 2";
-    c3.message = "robot comment 3";
-
-    testCommentHelper.addRobotComment(changeId, c1, "robot message 1");
-    TestTimeUtil.incrementClock(5, TimeUnit.SECONDS);
-
-    testCommentHelper.addRobotComment(changeId, c2, "robot message 2");
-    TestTimeUtil.incrementClock(5, TimeUnit.SECONDS);
-
-    testCommentHelper.addRobotComment(changeId, c3, "robot message 3");
-    TestTimeUtil.incrementClock(5, TimeUnit.SECONDS);
-
-    Map<String, List<RobotCommentInfo>> robotComments = gApi.changes().id(changeId).robotComments();
-    List<RobotCommentInfo> robotCommentsList =
-        robotComments.values().stream().flatMap(List::stream).collect(toList());
-
-    List<ChangeMessageInfo> allMessages =
-        gApi.changes().id(changeId).get(MESSAGES).messages.stream().collect(toList());
-
-    assertThat(allMessages.stream().map(cm -> cm.message).collect(toList()))
-        .containsExactly(
-            "Uploaded patch set 1.",
-            "Patch Set 1:\n\n(1 comment)\n\nrobot message 1",
-            "Patch Set 1:\n\n(1 comment)\n\nrobot message 2",
-            "Patch Set 1:\n\n(1 comment)\n\nrobot message 3");
-
-    assertThat(robotCommentsList.stream().map(c -> c.message).collect(toList()))
-        .containsExactly("robot comment 1", "robot comment 2", "robot comment 3");
-
-    String message1ChangeId =
-        allMessages.stream()
-            .filter(c -> c.message.contains("robot message 1"))
-            .collect(onlyElement())
-            .id;
-    String message2ChangeId =
-        allMessages.stream()
-            .filter(c -> c.message.contains("robot message 2"))
-            .collect(onlyElement())
-            .id;
-    String message3ChangeId =
-        allMessages.stream()
-            .filter(c -> c.message.contains("robot message 3"))
-            .collect(onlyElement())
-            .id;
-
-    String comment1MessageId =
-        robotCommentsList.stream()
-            .filter(c -> c.message.equals("robot comment 1"))
-            .collect(onlyElement())
-            .changeMessageId;
-    String comment2MessageId =
-        robotCommentsList.stream()
-            .filter(c -> c.message.equals("robot comment 2"))
-            .collect(onlyElement())
-            .changeMessageId;
-    String comment3MessageId =
-        robotCommentsList.stream()
-            .filter(c -> c.message.equals("robot comment 3"))
-            .collect(onlyElement())
-            .changeMessageId;
-
-    /**
-     * All change messages have the auto-generated tag. Robot comments can be linked to
-     * auto-generated messages where each comment is linked to the next nearest change message in
-     * timestamp
-     */
-    assertThat(message1ChangeId).isEqualTo(comment1MessageId);
-    assertThat(message2ChangeId).isEqualTo(comment2MessageId);
-    assertThat(message3ChangeId).isEqualTo(comment3MessageId);
-  }
-
-  @Test
-  public void robotCommentsCanBeRetrievedAsList() throws Exception {
-    RobotCommentInput robotCommentInput = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos =
-        gApi.changes().id(changeId).current().robotCommentsAsList();
-
-    assertThat(robotCommentInfos).hasSize(1);
-    RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
-    assertRobotComment(robotCommentInfo, robotCommentInput);
-  }
-
-  @Test
-  public void specificRobotCommentCanBeRetrieved() throws Exception {
-    RobotCommentInput robotCommentInput = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
-
-    RobotCommentInfo specificRobotCommentInfo =
-        gApi.changes().id(changeId).current().robotComment(robotCommentInfo.id).get();
-    assertRobotComment(specificRobotCommentInfo, robotCommentInput);
-  }
-
-  @Test
-  public void robotCommentWithoutOptionalFieldsCanBeAdded() throws Exception {
-    RobotCommentInput in = TestCommentHelper.createRobotCommentInputWithMandatoryFields(FILE_NAME);
-    testCommentHelper.addRobotComment(changeId, in);
-
-    Map<String, List<RobotCommentInfo>> out = gApi.changes().id(changeId).current().robotComments();
-    assertThat(out).hasSize(1);
-    RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path));
-    assertRobotComment(comment, in, false);
-  }
-
-  @Test
-  public void patchsetLevelRobotCommentCanBeAddedAndRetrieved() throws Exception {
-    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
-    testCommentHelper.addRobotComment(changeId, input);
-
-    List<RobotCommentInfo> results = getRobotComments();
-    assertThatList(results).onlyElement().path().isEqualTo(PATCHSET_LEVEL);
-  }
-
-  @Test
-  public void patchsetLevelRobotCommentCantHaveLine() throws Exception {
-    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
-    input.line = 1;
-    BadRequestException ex =
-        assertThrows(
-            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
-    assertThat(ex.getMessage()).contains("line");
-  }
-
-  @Test
-  public void patchsetLevelRobotCommentCantHaveRange() throws Exception {
-    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
-    input.range = createRange(2, 9, 5, 10);
-    BadRequestException ex =
-        assertThrows(
-            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
-    assertThat(ex.getMessage()).contains("range");
-  }
-
-  @Test
-  public void patchsetLevelRobotCommentCantHaveSide() throws Exception {
-    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
-    input.side = Side.REVISION;
-    BadRequestException ex =
-        assertThrows(
-            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
-    assertThat(ex.getMessage()).contains("side");
-  }
-
-  @Test
-  public void fixSuggestionCannotPointToPatchsetLevel() throws Exception {
-    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    FixReplacementInfo brokenFixReplacement = createFixReplacementInfo();
-    brokenFixReplacement.path = PATCHSET_LEVEL;
-    input.fixSuggestions = ImmutableList.of(createFixSuggestionInfo(brokenFixReplacement));
-    BadRequestException ex =
-        assertThrows(
-            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
-    assertThat(ex.getMessage()).contains("file path must not be " + PATCHSET_LEVEL);
-  }
-
-  @Test
-  public void robotCommentInvalidInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
-    input.inReplyTo = "invalid";
-    BadRequestException ex =
-        assertThrows(
-            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
-    assertThat(ex.getMessage()).contains("inReplyTo");
-  }
-
-  @Test
-  public void canCreateRobotCommentWithRobotCommentAsParent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentRobotCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-
-    ReviewInput.RobotCommentInput robotCommentInput =
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(COMMIT_MSG);
-    robotCommentInput.message = "comment reply";
-    robotCommentInput.inReplyTo = parentRobotCommentUuid;
-    testCommentHelper.addRobotComment(changeId, robotCommentInput);
-
-    RobotCommentInfo resultComment =
-        Iterables.getOnlyElement(
-            gApi.changes().id(changeId.get()).current().robotCommentsAsList().stream()
-                .filter(c -> c.message.equals("comment reply"))
-                .collect(toImmutableSet()));
-    assertThat(resultComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
-  }
-
-  @Test
-  public void canCreateRobotCommentWithHumanCommentAsParent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String changeIdString = changeOperations.change(changeId).get().changeId();
-    String parentCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newComment().create();
-
-    ReviewInput.RobotCommentInput robotCommentInput =
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(COMMIT_MSG);
-    robotCommentInput.message = "comment reply";
-    robotCommentInput.inReplyTo = parentCommentUuid;
-    testCommentHelper.addRobotComment(changeIdString, robotCommentInput);
-
-    RobotCommentInfo resultComment =
-        Iterables.getOnlyElement(
-            gApi.changes().id(changeIdString).current().robotCommentsAsList().stream()
-                .filter(c -> c.message.equals("comment reply"))
-                .collect(toImmutableSet()));
-    assertThat(resultComment.inReplyTo).isEqualTo(parentCommentUuid);
-  }
-
-  @Test
-  public void hugeRobotCommentIsRejected() {
-    int defaultSizeLimit = 1 << 20;
-    fixReplacementInfo.replacement = getStringFor(defaultSizeLimit + 1);
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown).hasMessageThat().contains("limit");
-  }
-
-  @Test
-  public void reasonablyLargeRobotCommentIsAccepted() throws Exception {
-    int defaultSizeLimit = 1 << 20;
-    // Allow for a few hundred bytes in other fields.
-    fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - 666);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    assertThat(robotCommentInfos).hasSize(1);
-  }
-
-  @Test
-  @GerritConfig(name = "change.robotCommentSizeLimit", value = "10k")
-  public void maximumAllowedSizeOfRobotCommentCanBeAdjusted() {
-    int sizeLimit = 10 << 20;
-    fixReplacementInfo.replacement = getStringFor(sizeLimit);
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown).hasMessageThat().contains("limit");
-  }
-
-  @Test
-  @GerritConfig(name = "change.robotCommentSizeLimit", value = "0")
-  public void zeroForMaximumAllowedSizeOfRobotCommentRemovesRestriction() throws Exception {
-    int defaultSizeLimit = 1 << 20;
-    fixReplacementInfo.replacement = getStringFor(2 * defaultSizeLimit);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    assertThat(robotCommentInfos).hasSize(1);
-  }
-
-  @Test
-  @GerritConfig(name = "change.robotCommentSizeLimit", value = "-1")
-  public void negativeValueForMaximumAllowedSizeOfRobotCommentRemovesRestriction()
-      throws Exception {
-    int defaultSizeLimit = 1 << 20;
-    fixReplacementInfo.replacement = getStringFor(2 * defaultSizeLimit);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    assertThat(robotCommentInfos).hasSize(1);
-  }
-
-  @Test
-  public void addedFixSuggestionCanBeRetrieved() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().isNotNull();
-  }
-
-  @Test
-  public void fixIdIsGeneratedForFixSuggestion() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().fixId().isNotEmpty();
-    assertThatList(robotCommentInfos)
-        .onlyElement()
-        .onlyFixSuggestion()
-        .fixId()
-        .isNotEqualTo(fixSuggestionInfo.fixId);
-  }
-
-  @Test
-  public void descriptionOfFixSuggestionIsAcceptedAsIs() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos)
-        .onlyElement()
-        .onlyFixSuggestion()
-        .description()
-        .isEqualTo(fixSuggestionInfo.description);
-  }
-
-  @Test
-  public void descriptionOfFixSuggestionIsMandatory() {
-    fixSuggestionInfo.description = null;
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            String.format(
-                "A description is required for the suggested fix of the comment on %s",
-                withFixRobotCommentInput.path));
-  }
-
-  @Test
-  public void addedFixReplacementCanBeRetrieved() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos)
-        .onlyElement()
-        .onlyFixSuggestion()
-        .onlyReplacement()
-        .isNotNull();
-  }
-
-  @Test
-  public void fixReplacementsAreMandatory() {
-    fixSuggestionInfo.replacements = Collections.emptyList();
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            String.format(
-                "At least one replacement is required"
-                    + " for the suggested fix of the comment on %s",
-                withFixRobotCommentInput.path));
-  }
-
-  @Test
-  public void pathOfFixReplacementIsAcceptedAsIs() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos)
-        .onlyElement()
-        .onlyFixSuggestion()
-        .onlyReplacement()
-        .path()
-        .isEqualTo(fixReplacementInfo.path);
-  }
-
-  @Test
-  public void pathOfFixReplacementIsMandatory() {
-    fixReplacementInfo.path = null;
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            String.format(
-                "A file path must be given for the replacement of the comment on %s",
-                withFixRobotCommentInput.path));
-  }
-
-  @Test
-  public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos)
-        .onlyElement()
-        .onlyFixSuggestion()
-        .onlyReplacement()
-        .range()
-        .isEqualTo(fixReplacementInfo.range);
-  }
-
-  @Test
-  public void rangeOfFixReplacementIsMandatory() {
-    fixReplacementInfo.range = null;
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            String.format(
-                "A range must be given for the replacement of the comment on %s",
-                withFixRobotCommentInput.path));
-  }
-
-  @Test
-  public void rangeOfFixReplacementNeedsToBeValid() {
-    fixReplacementInfo.range = createRange(13, 9, 5, 10);
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown).hasMessageThat().contains("Range (13:9 - 5:10)");
-  }
-
-  @Test
-  public void commentWithRangeAndLine_lineIsIgnored() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 1);
-    fixReplacementInfo1.replacement = "First modification\n";
-
-    withFixRobotCommentInput.line = 1;
-    withFixRobotCommentInput.range = createRange(2, 0, 3, 1);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotComments = getRobotComments();
-    assertThat(robotComments.get(0).line).isEqualTo(3);
-  }
-
-  @Test
-  public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap() {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 1);
-    fixReplacementInfo1.replacement = "First modification\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
-    fixReplacementInfo2.replacement = "Second modification\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown).hasMessageThat().contains("overlap");
-  }
-
-  @Test
-  public void rangesOfFixReplacementsOfSameFixSuggestionForDifferentFileMayOverlap()
-      throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 1);
-    fixReplacementInfo1.replacement = "First modification\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME2;
-    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
-    fixReplacementInfo2.replacement = "Second modification\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(1);
-  }
-
-  @Test
-  public void rangesOfFixReplacementsOfDifferentFixSuggestionsForSameFileMayOverlap()
-      throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 1);
-    fixReplacementInfo1.replacement = "First modification\n";
-    FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
-    fixReplacementInfo2.replacement = "Second modification\n";
-    FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
-
-    withFixRobotCommentInput.fixSuggestions =
-        ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(2);
-  }
-
-  @Test
-  public void fixReplacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
-    fixReplacementInfo1.replacement = "First modification\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
-    fixReplacementInfo2.replacement = "Second modification\n";
-
-    FixReplacementInfo fixReplacementInfo3 = new FixReplacementInfo();
-    fixReplacementInfo3.path = FILE_NAME;
-    fixReplacementInfo3.range = createRange(4, 0, 5, 0);
-    fixReplacementInfo3.replacement = "Third modification\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo2, fixReplacementInfo1, fixReplacementInfo3);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().replacements().hasSize(3);
-  }
-
-  @Test
-  public void replacementStringOfFixReplacementIsAcceptedAsIs() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    assertThatList(robotCommentInfos)
-        .onlyElement()
-        .onlyFixSuggestion()
-        .onlyReplacement()
-        .replacement()
-        .isEqualTo(fixReplacementInfo.replacement);
-  }
-
-  @Test
-  public void replacementStringOfFixReplacementIsMandatory() {
-    fixReplacementInfo.replacement = null;
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            String.format(
-                "A content for replacement must be "
-                    + "indicated for the replacement of the comment on %s",
-                withFixRobotCommentInput.path));
-  }
-
-  @Test
-  public void storedFixWithinALineCanBeApplied() throws Exception {
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
-                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
-  }
-
-  @Test
-  public void applyStoredFixAfterUpdatingPreferredEmail() throws Exception {
-    String emailOne = "email1@example.com";
-    Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
-
-    // Create change
-    Change.Id change =
-        changeOperations
-            .newChange()
-            .project(project)
-            .file(FILE_NAME)
-            .content(FILE_CONTENT)
-            .owner(testUser)
-            .create();
-
-    // Add Robot Comment to the change
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-    testCommentHelper.addRobotComment(project + "~" + change.get(), withFixRobotCommentInput);
-
-    // Change preferred email for the user
-    String emailTwo = "email2@example.com";
-    accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
-    requestScopeOperations.setApiUser(testUser);
-
-    // Fetch Fix ID
-    List<RobotCommentInfo> robotCommentInfoList =
-        gApi.changes().id(change.get()).current().robotCommentsAsList();
-
-    List<String> fixIds = getFixIds(robotCommentInfoList);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    // Apply fix
-    gApi.changes().id(change.get()).current().applyFix(fixId);
-
-    EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
-    assertThat(editInfo.commit.committer.email).isEqualTo(emailOne);
-  }
-
-  @Test
-  public void storedFixSpanningMultipleLinesCanBeApplied() throws Exception {
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content\n5";
-    fixReplacementInfo.range = createRange(3, 2, 5, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nSecond line\nThModified content\n5th line\nSixth line\nSeventh line\n"
-                + "Eighth line\nNinth line\nTenth line\n");
-  }
-
-  @Test
-  public void storedFixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
-    fixReplacementInfo1.replacement = "First modification\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
-    fixReplacementInfo2.replacement = "Some other modified content\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nFirst modification\nSome other modified content\nFourth line\nFifth line\n"
-                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
-  }
-
-  @Test
-  public void twoStoredFixesOnSameFileCanBeApplied() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
-    fixReplacementInfo1.replacement = "First modification\n";
-    FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(8, 0, 9, 0);
-    fixReplacementInfo2.replacement = "Some other modified content\n";
-    FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
-
-    RobotCommentInput robotCommentInput1 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo1);
-    RobotCommentInput robotCommentInput2 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo2);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput1);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput2);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
-                + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
-  }
-
-  @Test
-  public void twoConflictingStoredFixesOnSameFileCannotBeApplied() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 1);
-    fixReplacementInfo1.replacement = "First modification\n";
-    FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
-    fixReplacementInfo2.replacement = "Some other modified content\n";
-    FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
-
-    RobotCommentInput robotCommentInput1 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo1);
-    RobotCommentInput robotCommentInput2 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo2);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput1);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput2);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
-    ResourceConflictException thrown =
-        assertThrows(
-            ResourceConflictException.class,
-            () -> gApi.changes().id(changeId).current().applyFix(fixIds.get(1)));
-    assertThat(thrown).hasMessageThat().contains("merge");
-  }
-
-  @Test
-  public void twoStoredFixesOfSameRobotCommentCanBeApplied() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
-    fixReplacementInfo1.replacement = "First modification\n";
-    FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME;
-    fixReplacementInfo2.range = createRange(8, 0, 9, 0);
-    fixReplacementInfo2.replacement = "Some other modified content\n";
-    FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
-
-    withFixRobotCommentInput.fixSuggestions =
-        ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
-                + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
-  }
-
-  @Test
-  public void storedFixReferringToDifferentFileThanRobotCommentCanBeApplied() throws Exception {
-    fixReplacementInfo.path = FILE_NAME2;
-    fixReplacementInfo.range = createRange(2, 0, 3, 0);
-    fixReplacementInfo.replacement = "Modified content\n";
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo("1st line\nModified content\n3rd line\n");
-  }
-
-  @Test
-  public void storedFixInvolvingTwoFilesCanBeApplied() throws Exception {
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = FILE_NAME;
-    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
-    fixReplacementInfo1.replacement = "First modification\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME2;
-    fixReplacementInfo2.range = createRange(1, 0, 2, 0);
-    fixReplacementInfo2.replacement = "Different file modification\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
-                + "Seventh line\nEighth line\nNinth line\nTenth line\n");
-    Optional<BinaryResult> file2 = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
-    BinaryResultSubject.assertThat(file2)
-        .value()
-        .asString()
-        .isEqualTo("Different file modification\n2nd line\n3rd line\n");
-  }
-
-  @Test
-  public void storedFixReferringToNonExistentFileCannotBeApplied() throws Exception {
-    fixReplacementInfo.path = "a_non_existent_file.txt";
-    fixReplacementInfo.range = createRange(1, 0, 2, 0);
-    fixReplacementInfo.replacement = "Modified content\n";
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    assertThrows(
-        ResourceNotFoundException.class,
-        () -> gApi.changes().id(changeId).current().applyFix(fixId));
-  }
-
-  @Test
-  public void storedFixOnPreviousPatchSetWithoutChangeEditCannotBeApplied() throws Exception {
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    // Remember patch set and add another one.
-    String previousRevision = gApi.changes().id(changeId).get().currentRevision;
-    amendChange(changeId);
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    ResourceConflictException thrown =
-        assertThrows(
-            ResourceConflictException.class,
-            () -> gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId));
-    assertThat(thrown).hasMessageThat().contains("current");
-  }
-
-  @Test
-  public void storedFixOnPreviousPatchSetWithExistingChangeEditCanBeApplied() throws Exception {
-    // Create an empty change edit.
-    gApi.changes().id(changeId).edit().create();
-
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    // Remember patch set and add another one.
-    String previousRevision = gApi.changes().id(changeId).get().currentRevision;
-    amendChange(changeId);
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    EditInfo editInfo = gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId);
-
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo(
-            "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
-                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
-    assertThat(editInfo).baseRevision().isEqualTo(previousRevision);
-  }
-
-  @Test
-  public void storedFixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied()
-      throws Exception {
-    // Create an empty change edit.
-    gApi.changes().id(changeId).edit().create();
-
-    // Add another patch set.
-    amendChange(changeId);
-
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    ResourceConflictException thrown =
-        assertThrows(
-            ResourceConflictException.class,
-            () -> gApi.changes().id(changeId).current().applyFix(fixId));
-    assertThat(thrown).hasMessageThat().contains("based");
-  }
-
-  @Test
-  public void storedFixDoesNotModifyCommitMessageOfChangeEdit() throws Exception {
-    String changeEditCommitMessage =
-        "This is the commit message of the change edit.\n\nChange-Id: " + changeId + "\n";
-    gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage);
-
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    String fixId = Iterables.getOnlyElement(getFixIds(getRobotComments()));
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
-    assertThat(commitMessage).isEqualTo(changeEditCommitMessage);
-  }
-
-  @Test
-  public void storedFixOnCommitMessageCanBeApplied() throws Exception {
-    // Set a dedicated commit message.
-    String footer = "\nChange-Id: " + changeId + "\n";
-    String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
-    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
-    gApi.changes().id(changeId).edit().publish();
-
-    withFixRobotCommentInput.path = Patch.COMMIT_MSG;
-    fixReplacementInfo.path = Patch.COMMIT_MSG;
-    fixReplacementInfo.replacement = "Modified line\n";
-    fixReplacementInfo.range = createRange(7, 0, 8, 0);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    String fixId = Iterables.getOnlyElement(getFixIds(getRobotComments()));
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
-    assertThat(commitMessage).isEqualTo("Modified line\nLine 2 of commit message\n" + footer);
-  }
-
-  @Test
-  public void storedFixOnHeaderPartOfCommitMessageCannotBeApplied() throws Exception {
-    // Set a dedicated commit message.
-    String footer = "Change-Id: " + changeId;
-    String originalCommitMessage =
-        "Line 1 of commit message\nLine 2 of commit message\n" + "\n" + footer + "\n";
-    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
-    gApi.changes().id(changeId).edit().publish();
-
-    withFixRobotCommentInput.path = Patch.COMMIT_MSG;
-    fixReplacementInfo.path = Patch.COMMIT_MSG;
-    fixReplacementInfo.replacement = "Modified line\n";
-    fixReplacementInfo.range = createRange(1, 0, 2, 0);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    String fixId = Iterables.getOnlyElement(getFixIds(getRobotComments()));
-
-    ResourceConflictException exception =
-        assertThrows(
-            ResourceConflictException.class,
-            () -> gApi.changes().id(changeId).current().applyFix(fixId));
-    assertThat(exception).hasMessageThat().contains("header");
-  }
-
-  @Test
-  public void storedFixContainingSeveralModificationsOfCommitMessageCanBeApplied()
-      throws Exception {
-    // Set a dedicated commit message.
-    String footer = "\nChange-Id: " + changeId + "\n";
-    String originalCommitMessage =
-        "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
-    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
-    gApi.changes().id(changeId).edit().publish();
-
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = Patch.COMMIT_MSG;
-    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
-    fixReplacementInfo1.replacement = "Modified line 1\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = Patch.COMMIT_MSG;
-    fixReplacementInfo2.range = createRange(9, 0, 10, 0);
-    fixReplacementInfo2.replacement = "Modified line 3\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-    withFixRobotCommentInput.path = Patch.COMMIT_MSG;
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    String fixId = Iterables.getOnlyElement(getFixIds(getRobotComments()));
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
-    assertThat(commitMessage)
-        .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
-  }
-
-  @Test
-  public void storedFixModifyingTheCommitMessageAndAFileCanBeApplied() throws Exception {
-    // Set a dedicated commit message.
-    String footer = "\nChange-Id: " + changeId + "\n";
-    String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
-    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
-    gApi.changes().id(changeId).edit().publish();
-
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = Patch.COMMIT_MSG;
-    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
-    fixReplacementInfo1.replacement = "Modified line 1\n";
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = FILE_NAME2;
-    fixReplacementInfo2.range = createRange(1, 0, 2, 0);
-    fixReplacementInfo2.replacement = "File modification\n";
-
-    FixSuggestionInfo fixSuggestionInfo =
-        createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
-    withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    String fixId = Iterables.getOnlyElement(getFixIds(getRobotComments()));
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
-    assertThat(commitMessage).isEqualTo("Modified line 1\nLine 2 of commit message\n" + footer);
-    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
-    BinaryResultSubject.assertThat(file)
-        .value()
-        .asString()
-        .isEqualTo("File modification\n2nd line\n3rd line\n");
-  }
-
-  @Test
-  public void twoStoredFixesOnCommitMessageCanBeAppliedOneAfterTheOther() throws Exception {
-    // Set a dedicated commit message.
-    String footer = "\nChange-Id: " + changeId + "\n";
-    String originalCommitMessage =
-        "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
-    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
-    gApi.changes().id(changeId).edit().publish();
-
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = Patch.COMMIT_MSG;
-    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
-    fixReplacementInfo1.replacement = "Modified line 1\n";
-    FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = Patch.COMMIT_MSG;
-    fixReplacementInfo2.range = createRange(9, 0, 10, 0);
-    fixReplacementInfo2.replacement = "Modified line 3\n";
-    FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
-
-    RobotCommentInput robotCommentInput1 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo1);
-    RobotCommentInput robotCommentInput2 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo2);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput1);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput2);
-    List<String> fixIds = getFixIds(getRobotComments());
-
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
-
-    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
-    assertThat(commitMessage)
-        .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
-  }
-
-  @Test
-  public void twoConflictingStoredFixesOnCommitMessageCanNotBeAppliedOneAfterTheOther()
-      throws Exception {
-    // Set a dedicated commit message.
-    String footer = "Change-Id: " + changeId;
-    String originalCommitMessage =
-        "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n\n"
-            + footer
-            + "\n";
-    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
-    gApi.changes().id(changeId).edit().publish();
-
-    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
-    fixReplacementInfo1.path = Patch.COMMIT_MSG;
-    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
-    fixReplacementInfo1.replacement = "Modified line 1\n";
-    FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
-
-    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
-    fixReplacementInfo2.path = Patch.COMMIT_MSG;
-    fixReplacementInfo2.range = createRange(7, 0, 10, 0);
-    fixReplacementInfo2.replacement = "Differently modified line 1\n";
-    FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
-
-    RobotCommentInput robotCommentInput1 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo1);
-    RobotCommentInput robotCommentInput2 =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo2);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput1);
-    testCommentHelper.addRobotComment(changeId, robotCommentInput2);
-    List<String> fixIds = getFixIds(getRobotComments());
-
-    gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
-    assertThrows(
-        ResourceConflictException.class,
-        () -> gApi.changes().id(changeId).current().applyFix(fixIds.get(1)));
-  }
-
-  @Test
-  public void applyingStoredFixTwiceIsIdempotent() throws Exception {
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    gApi.changes().id(changeId).current().applyFix(fixId);
-    String expectedEditCommit =
-        gApi.changes().id(changeId).edit().get().map(edit -> edit.commit.commit).orElse("");
-
-    // Apply the fix again.
-    gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<EditInfo> editInfo = gApi.changes().id(changeId).edit().get();
-    assertThat(editInfo).value().commit().commit().isEqualTo(expectedEditCommit);
-  }
-
-  @Test
-  public void nonExistentStoredFixCannotBeApplied() throws Exception {
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-    String nonExistentFixId = fixId + "_non-existent";
-
-    assertThrows(
-        ResourceNotFoundException.class,
-        () -> gApi.changes().id(changeId).current().applyFix(nonExistentFixId));
-  }
-
-  @Test
-  public void applyingStoredFixReturnsEditInfoForCreatedChangeEdit() throws Exception {
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
-    String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
-    assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
-    String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
-    assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
-  }
-
-  @Test
-  public void applyingStoredFixOnTopOfChangeEditReturnsEditInfoForUpdatedChangeEdit()
-      throws Exception {
-    gApi.changes().id(changeId).edit().create();
-
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
-
-    Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
-    String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
-    assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
-    String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
-    assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
-  }
-
-  @Test
-  public void createdChangeEditIsBasedOnCurrentPatchSet() throws Exception {
-    String currentRevision = gApi.changes().id(changeId).get().currentRevision;
-
-    fixReplacementInfo.path = FILE_NAME;
-    fixReplacementInfo.replacement = "Modified content";
-    fixReplacementInfo.range = createRange(3, 1, 3, 3);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
-
-    assertThat(editInfo).baseRevision().isEqualTo(currentRevision);
-  }
-
-  @Test
-  public void queryChangesWithCommentCounts() throws Exception {
-    PushOneCommit.Result r1 = createChange();
-    PushOneCommit.Result r2 =
-        pushFactory
-            .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
-            .to("refs/for/master");
-
-    testCommentHelper.addRobotComment(
-        r2.getChangeId(), TestCommentHelper.createRobotCommentInputWithMandatoryFields(FILE_NAME));
-
-    try (AutoCloseable ignored = disableNoteDb()) {
-      ChangeInfo result = Iterables.getOnlyElement(query(r2.getChangeId()));
-      // currently, we create all robot comments as 'resolved' by default.
-      // if we allow users to resolve a robot comment, then this test should
-      // be modified.
-      assertThat(result.unresolvedCommentCount).isEqualTo(0);
-      assertThat(result.totalCommentCount).isEqualTo(1);
-    }
-  }
-
-  @Test
-  public void previewStoredFixWithNonexistentFixId() throws Exception {
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    assertThrows(
-        ResourceNotFoundException.class,
-        () -> gApi.changes().id(changeId).current().getFixPreview("Non existing fixId"));
-  }
-
-  @Test
-  public void previewStoredFixForCommitMsg() throws Exception {
-    String footer = "Change-Id: " + changeId;
-    updateCommitMessage(
-        changeId,
-        "Commit title\n\nCommit message line 1\nLine 2\nLine 3\nLast line\n\n" + footer + "\n");
-    FixReplacementInfo commitMsgReplacement = new FixReplacementInfo();
-    commitMsgReplacement.path = Patch.COMMIT_MSG;
-    // The test assumes that the first 5 lines is a header.
-    // Line 10 has content "Line 2"
-    commitMsgReplacement.range = createRange(10, 0, 11, 0);
-    commitMsgReplacement.replacement = "New content\n";
-
-    FixSuggestionInfo commitMsgSuggestionInfo = createFixSuggestionInfo(commitMsgReplacement);
-    RobotCommentInput commitMsgRobotCommentInput =
-        TestCommentHelper.createRobotCommentInput(Patch.COMMIT_MSG, commitMsgSuggestionInfo);
-    testCommentHelper.addRobotComment(changeId, commitMsgRobotCommentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
-    assertThat(fixPreview).hasSize(1);
-    assertThat(fixPreview).containsKey(Patch.COMMIT_MSG);
-
-    DiffInfo diff = fixPreview.get(Patch.COMMIT_MSG);
-    assertThat(diff).metaA().name().isEqualTo(Patch.COMMIT_MSG);
-    assertThat(diff).metaA().contentType().isEqualTo(GERRIT_COMMIT_MESSAGE_TYPE);
-    assertThat(diff).metaB().name().isEqualTo(Patch.COMMIT_MSG);
-    assertThat(diff).metaB().contentType().isEqualTo(GERRIT_COMMIT_MESSAGE_TYPE);
-
-    assertThat(diff).content().element(0).commonLines().hasSize(9);
-    // Header has a dynamic content, do not check it
-    assertThat(diff).content().element(0).commonLines().element(6).isEqualTo("Commit title");
-    assertThat(diff).content().element(0).commonLines().element(7).isEqualTo("");
-    assertThat(diff)
-        .content()
-        .element(0)
-        .commonLines()
-        .element(8)
-        .isEqualTo("Commit message line 1");
-    assertThat(diff).content().element(1).linesOfA().containsExactly("Line 2");
-    assertThat(diff).content().element(1).linesOfB().containsExactly("New content");
-    assertThat(diff)
-        .content()
-        .element(2)
-        .commonLines()
-        .containsExactly("Line 3", "Last line", "", footer, "");
-  }
-
-  private void updateCommitMessage(String changeId, String newCommitMessage) throws Exception {
-    gApi.changes().id(changeId).edit().create();
-    gApi.changes().id(changeId).edit().modifyCommitMessage(newCommitMessage);
-    PublishChangeEditInput publishInput = new PublishChangeEditInput();
-    gApi.changes().id(changeId).edit().publish(publishInput);
-  }
-
-  @Test
-  public void previewStoredFixForNonExistingFile() throws Exception {
-    FixReplacementInfo replacement = new FixReplacementInfo();
-    replacement.path = "a_non_existent_file.txt";
-    replacement.range = createRange(1, 0, 2, 0);
-    replacement.replacement = "Modified content\n";
-
-    FixSuggestionInfo fixSuggestion = createFixSuggestionInfo(replacement);
-    RobotCommentInput commentInput =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME2, fixSuggestion);
-    testCommentHelper.addRobotComment(changeId, commentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    assertThrows(
-        ResourceNotFoundException.class,
-        () -> gApi.changes().id(changeId).current().getFixPreview(fixId));
-  }
-
-  @Test
-  public void previewStoredFix() throws Exception {
-    FixReplacementInfo fixReplacementInfoFile1 = new FixReplacementInfo();
-    fixReplacementInfoFile1.path = FILE_NAME;
-    fixReplacementInfoFile1.replacement = "some replacement code";
-    fixReplacementInfoFile1.range = createRange(3, 9, 8, 4);
-
-    FixReplacementInfo fixReplacementInfoFile2 = new FixReplacementInfo();
-    fixReplacementInfoFile2.path = FILE_NAME2;
-    fixReplacementInfoFile2.replacement = "New line\n";
-    fixReplacementInfoFile2.range = createRange(2, 0, 2, 0);
-
-    fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfoFile1, fixReplacementInfoFile2);
-
-    withFixRobotCommentInput =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo);
-
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
-    assertThat(fixPreview).hasSize(2);
-    assertThat(fixPreview).containsKey(FILE_NAME);
-    assertThat(fixPreview).containsKey(FILE_NAME2);
-
-    DiffInfo diff = fixPreview.get(FILE_NAME);
-    assertThat(diff).intralineStatus().isEqualTo(IntraLineStatus.OK);
-    assertThat(diff).webLinks().isNull();
-    assertThat(diff).binary().isNull();
-    assertThat(diff).diffHeader().isNull();
-    assertThat(diff).changeType().isEqualTo(ChangeType.MODIFIED);
-    assertThat(diff).metaA().totalLineCount().isEqualTo(11);
-    assertThat(diff).metaA().name().isEqualTo(FILE_NAME);
-    assertThat(diff).metaA().commitId().isEqualTo(commitId);
-    assertThat(diff).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
-    assertThat(diff).metaA().webLinks().isNull();
-    assertThat(diff).metaB().totalLineCount().isEqualTo(6);
-    assertThat(diff).metaB().name().isEqualTo(FILE_NAME);
-    assertThat(diff).metaB().commitId().isNull();
-    assertThat(diff).metaB().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
-    assertThat(diff).metaB().webLinks().isNull();
-
-    assertThat(diff).content().hasSize(3);
-    assertThat(diff)
-        .content()
-        .element(0)
-        .commonLines()
-        .containsExactly("First line", "Second line");
-    assertThat(diff).content().element(0).linesOfA().isNull();
-    assertThat(diff).content().element(0).linesOfB().isNull();
-
-    assertThat(diff).content().element(1).commonLines().isNull();
-    assertThat(diff)
-        .content()
-        .element(1)
-        .linesOfA()
-        .containsExactly(
-            "Third line", "Fourth line", "Fifth line", "Sixth line", "Seventh line", "Eighth line");
-    assertThat(diff)
-        .content()
-        .element(1)
-        .linesOfB()
-        .containsExactly("Third linsome replacement codeth line");
-
-    assertThat(diff)
-        .content()
-        .element(2)
-        .commonLines()
-        .containsExactly("Ninth line", "Tenth line", "");
-    assertThat(diff).content().element(2).linesOfA().isNull();
-    assertThat(diff).content().element(2).linesOfB().isNull();
-
-    DiffInfo diff2 = fixPreview.get(FILE_NAME2);
-    assertThat(diff2).intralineStatus().isEqualTo(IntraLineStatus.OK);
-    assertThat(diff2).webLinks().isNull();
-    assertThat(diff2).binary().isNull();
-    assertThat(diff2).diffHeader().isNull();
-    assertThat(diff2).changeType().isEqualTo(ChangeType.MODIFIED);
-    assertThat(diff2).metaA().totalLineCount().isEqualTo(4);
-    assertThat(diff2).metaA().name().isEqualTo(FILE_NAME2);
-    assertThat(diff2).metaA().commitId().isEqualTo(commitId);
-    assertThat(diff2).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
-    assertThat(diff2).metaA().webLinks().isNull();
-    assertThat(diff2).metaB().totalLineCount().isEqualTo(5);
-    assertThat(diff2).metaB().name().isEqualTo(FILE_NAME2);
-    assertThat(diff2).metaB().commitId().isNull();
-    assertThat(diff2).metaA().contentType().isEqualTo(PLAIN_TEXT_CONTENT_TYPE);
-    assertThat(diff2).metaB().webLinks().isNull();
-
-    assertThat(diff2).content().hasSize(3);
-    assertThat(diff2).content().element(0).commonLines().containsExactly("1st line");
-    assertThat(diff2).content().element(0).linesOfA().isNull();
-    assertThat(diff2).content().element(0).linesOfB().isNull();
-
-    assertThat(diff2).content().element(1).commonLines().isNull();
-    assertThat(diff2).content().element(1).linesOfA().isNull();
-    assertThat(diff2).content().element(1).linesOfB().containsExactly("New line");
-
-    assertThat(diff2)
-        .content()
-        .element(2)
-        .commonLines()
-        .containsExactly("2nd line", "3rd line", "");
-    assertThat(diff2).content().element(2).linesOfA().isNull();
-    assertThat(diff2).content().element(2).linesOfB().isNull();
-  }
-
-  @Test
-  public void previewStoredFix_additionalCommentWithNullFixes_noException() throws Exception {
-    FixReplacementInfo fixReplacementInfoFile1 = new FixReplacementInfo();
-    fixReplacementInfoFile1.path = FILE_NAME;
-    fixReplacementInfoFile1.replacement = "some replacement code";
-    fixReplacementInfoFile1.range = createRange(3, 9, 8, 4);
-
-    fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfoFile1);
-
-    withFixRobotCommentInput =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME, fixSuggestionInfo);
-    testCommentHelper.addRobotComment(changeId, withFixRobotCommentInput);
-
-    ReviewInput.CommentInput commentWithoutFix = new CommentInput();
-    commentWithoutFix.message = "Hello";
-    commentWithoutFix.patchSet = 1;
-    commentWithoutFix.path = FILE_NAME;
-    testCommentHelper.addComment(changeId, commentWithoutFix);
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
-
-    assertThat(fixPreview).hasSize(1);
-    assertThat(fixPreview).containsKey(FILE_NAME);
-  }
-
-  @Test
-  public void previewStoredFixAddNewLineAtEnd() throws Exception {
-    FixReplacementInfo replacement = new FixReplacementInfo();
-    replacement.path = FILE_NAME3;
-    replacement.range = createRange(2, 8, 2, 8);
-    replacement.replacement = "\n";
-
-    FixSuggestionInfo fixSuggestion = createFixSuggestionInfo(replacement);
-    RobotCommentInput commentInput =
-        TestCommentHelper.createRobotCommentInput(FILE_NAME3, fixSuggestion);
-    testCommentHelper.addRobotComment(changeId, commentInput);
-
-    List<RobotCommentInfo> robotCommentInfos = getRobotComments();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    Map<String, DiffInfo> fixPreview = gApi.changes().id(changeId).current().getFixPreview(fixId);
-
-    assertThat(fixPreview).hasSize(1);
-    assertThat(fixPreview).containsKey(FILE_NAME3);
-
-    DiffInfo diff = fixPreview.get(FILE_NAME3);
-    assertThat(diff).metaA().totalLineCount().isEqualTo(2);
-    // Original file doesn't have EOL marker at the end of file.
-    // Due to the additional EOL mark diff has one additional line
-    // This behavior is in line with ordinary get diff API.
-    assertThat(diff).metaB().totalLineCount().isEqualTo(3);
-
-    assertThat(diff).content().hasSize(2);
-    assertThat(diff).content().element(0).commonLines().containsExactly("1st line");
-    assertThat(diff).content().element(1).linesOfA().containsExactly("2nd line");
-    assertThat(diff).content().element(1).linesOfB().containsExactly("2nd line", "");
-  }
-
-  private List<RobotCommentInfo> getRobotComments() throws RestApiException {
-    return gApi.changes().id(changeId).current().robotCommentsAsList();
-  }
-
-  private void assertRobotComment(RobotCommentInfo c, RobotCommentInput expected) {
-    assertRobotComment(c, expected, true);
-  }
-
-  private void assertRobotComment(
-      RobotCommentInfo c, RobotCommentInput expected, boolean expectPath) {
-    assertThat(c.robotId).isEqualTo(expected.robotId);
-    assertThat(c.robotRunId).isEqualTo(expected.robotRunId);
-    assertThat(c.url).isEqualTo(expected.url);
-    assertThat(c.properties).isEqualTo(expected.properties);
-    assertThat(c.line).isEqualTo(expected.line);
-    assertThat(c.message).isEqualTo(expected.message);
-
-    assertThat(c.author.email).isEqualTo(admin.email());
-
-    if (expectPath) {
-      assertThat(c.path).isEqualTo(expected.path);
-    } else {
-      assertThat(c.path).isNull();
-    }
-  }
-
-  private static String getStringFor(int numberOfBytes) {
-    char[] chars = new char[numberOfBytes];
-    // 'a' will require one byte even when mapped to a JSON string
-    Arrays.fill(chars, 'a');
-    return new String(chars);
-  }
-
-  private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
-    assertThatList(robotComments).isNotNull();
-    return robotComments.stream()
-        .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
-        .filter(Objects::nonNull)
-        .flatMap(List::stream)
-        .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
-        .collect(toList());
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/config/GlobalLabelIT.java b/javatests/com/google/gerrit/acceptance/config/GlobalLabelIT.java
new file mode 100644
index 0000000..d378f25
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/config/GlobalLabelIT.java
@@ -0,0 +1,223 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.config;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class GlobalLabelIT extends AbstractDaemonTest {
+  @Inject private ChangeOperations changeOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  @Test
+  public void globalLabelAppliesToAllProjects() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .build();
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      testThatGlobalLabelAppliesToProject(allProjects, globalLabelType);
+      testThatGlobalLabelAppliesToProject(project, globalLabelType);
+
+      Project.NameKey otherProject = projectOperations.newProject().create();
+      testThatGlobalLabelAppliesToProject(otherProject, globalLabelType);
+    }
+  }
+
+  private void testThatGlobalLabelAppliesToProject(
+      Project.NameKey project, LabelType globalLabelType) throws RestApiException {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+    assertThat(changeInfo.labels).containsKey(globalLabelType.getName());
+
+    LabelInfo globalLabelInfo = changeInfo.labels.get(globalLabelType.getName());
+    assertThat(globalLabelInfo.description).isEqualTo(globalLabelType.getDescription().get());
+    assertThat(globalLabelInfo.values)
+        .containsExactlyEntriesIn(
+            globalLabelType.getValues().stream()
+                .collect(
+                    toImmutableMap(
+                        labelValue -> LabelValue.formatValue(labelValue.getValue()),
+                        LabelValue::getText)));
+  }
+
+  @Test
+  public void globalLabelCannotBeOverriddenIfOverrideIsNotAllowed() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .setCanOverride(false)
+            .build();
+
+    // Try overriding the label with different values.
+    configLabel(
+        project,
+        globalLabelType.getName(),
+        LabelFunction.NO_OP,
+        LabelValue.create((short) 2, "Approved"),
+        LabelValue.create((short) 1, "LGTM"),
+        LabelValue.create((short) 0, "No vote"),
+        LabelValue.create((short) -1, "Rejected"),
+        LabelValue.create((short) -2, "Veto"));
+
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+
+      ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+      assertThat(changeInfo.labels).containsKey(globalLabelType.getName());
+
+      // Assert that the global label has not been overridden.
+      LabelInfo globalLabelInfo = changeInfo.labels.get(globalLabelType.getName());
+      assertThat(globalLabelInfo.values)
+          .containsExactlyEntriesIn(
+              globalLabelType.getValues().stream()
+                  .collect(
+                      toImmutableMap(
+                          labelValue -> LabelValue.formatValue(labelValue.getValue()),
+                          LabelValue::getText)));
+    }
+  }
+
+  @Test
+  public void globalLabelCannotBeRemovedIfOverrideIsNotAllowed() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .setCanOverride(false)
+            .build();
+
+    // Try removing the label (a label without values overrides an inherited label).
+    configLabel(project, globalLabelType.getName(), LabelFunction.NO_OP);
+
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+
+      ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+      assertThat(changeInfo.labels).containsKey(globalLabelType.getName());
+
+      // Assert that the global label has not been removed.
+      LabelInfo globalLabelInfo = changeInfo.labels.get(globalLabelType.getName());
+      assertThat(globalLabelInfo.values)
+          .containsExactlyEntriesIn(
+              globalLabelType.getValues().stream()
+                  .collect(
+                      toImmutableMap(
+                          labelValue -> LabelValue.formatValue(labelValue.getValue()),
+                          LabelValue::getText)));
+    }
+  }
+
+  @Test
+  public void globalLabelCanBeOverriddenIfOverrideIsAllowed() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .setCanOverride(true)
+            .build();
+
+    // Override the label with different values.
+    configLabel(
+        project,
+        globalLabelType.getName(),
+        LabelFunction.NO_OP,
+        LabelValue.create((short) 2, "Approved"),
+        LabelValue.create((short) 1, "LGTM"),
+        LabelValue.create((short) 0, "No vote"),
+        LabelValue.create((short) -1, "Rejected"),
+        LabelValue.create((short) -2, "Veto"));
+
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+
+      ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+      assertThat(changeInfo.labels).containsKey(globalLabelType.getName());
+
+      // Assert that the global label has been overridden.
+      LabelInfo globalLabelInfo = changeInfo.labels.get(globalLabelType.getName());
+      assertThat(globalLabelInfo.values)
+          .containsExactly(
+              "+2", "Approved", "+1", "LGTM", " 0", "No vote", "-1", "Rejected", "-2", "Veto");
+    }
+  }
+
+  @Test
+  public void globalLabelCanBeRemovedIfOverrideIsAllowed() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .setCanOverride(true)
+            .build();
+
+    // Remove the label (a label without values overrides an inherited label).
+    configLabel(project, globalLabelType.getName(), LabelFunction.NO_OP);
+
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+
+      // Assert that the global label has been removed.
+      ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+      assertThat(changeInfo.labels).doesNotContainKey(globalLabelType.getName());
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 918916b..24ff37d 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.FileMode;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
@@ -77,7 +78,6 @@
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.restapi.change.ChangeEdits;
@@ -89,6 +89,7 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.Base64;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -109,7 +110,6 @@
   private static final String FILE_NAME2 = "foo2";
   private static final String FILE_NAME3 = "foo3";
   private static final String FILE_NAME4 = "foo4";
-  private static final int FILE_MODE = 100644;
   private static final String CONTENT_OLD_STR = "bar";
   private static final byte[] CONTENT_OLD = CONTENT_OLD_STR.getBytes(UTF_8);
   private static final String CONTENT_NEW_STR = "baz";
@@ -302,61 +302,34 @@
         .hasMessageThat()
         .isEqualTo(
             String.format(
-                "Rebasing change edit onto another patchset results in merge conflicts.\n\n"
-                    + "merge conflict(s):\n%s\n\n"
-                    + "Download the edit patchset and rebase manually to preserve changes.",
+                """
+                Rebasing change edit onto another patchset results in merge conflicts.
+
+                merge conflict(s):
+                * %s
+
+                Download the edit patchset and rebase manually to preserve changes.
+                """,
                 FILE_NAME));
   }
 
   @Test
   public void rebaseEditWithConflictsAllowed() throws Exception {
-    // Create change where FILE_NAME has OLD_CONTENT
-    String changeId = newChange(admin.newIdent());
-
-    PatchSet previousPatchSet = getCurrentPatchSet(changeId);
-    createEmptyEditFor(changeId);
-    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
-
-    // add new patch set that touches the same file as the edit
-    addNewPatchSetWithModifiedFile(changeId, FILE_NAME, new String(CONTENT_NEW2, UTF_8));
-    PatchSet currentPatchSet = getCurrentPatchSet(changeId);
-
-    Optional<EditInfo> originalEdit = getEdit(changeId);
-    assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.commitId().name());
-
-    Timestamp beforeRebase = originalEdit.get().commit.committer.date;
-
-    RebaseChangeEditInput input = new RebaseChangeEditInput();
-    input.allowConflicts = true;
-    EditInfo rebasedEdit = gApi.changes().id(changeId).edit().rebase(input);
-
-    ensureSameBytes(
-        getFileContentOfEdit(changeId, FILE_NAME),
-        String.format(
-                "<<<<<<< PATCH SET (%s %s)\n"
-                    + "%s\n"
-                    + "=======\n"
-                    + "%s\n"
-                    + ">>>>>>> EDIT      (%s %s)\n",
-                ObjectIds.abbreviateName(currentPatchSet.commitId(), 6),
-                gApi.changes().id(changeId).get().subject,
-                CONTENT_NEW2_STR,
-                CONTENT_NEW_STR,
-                ObjectIds.abbreviateName(ObjectId.fromString(originalEdit.get().commit.commit), 6),
-                originalEdit.get().commit.subject)
-            .getBytes(UTF_8));
-    assertThat(rebasedEdit).baseRevision().isEqualTo(currentPatchSet.commitId().name());
-    assertThat(rebasedEdit).commit().committer().date().isNotEqualTo(beforeRebase);
-    assertThat(rebasedEdit).containsGitConflicts().isTrue();
+    testRebaseEditWithConflictsAllowed(/* useDiff3= */ false);
   }
 
   @Test
   @GerritConfig(name = "change.diff3ConflictView", value = "true")
   public void rebaseEditWithConflictsAllowedUsingDiff3() throws Exception {
+    testRebaseEditWithConflictsAllowed(/* useDiff3= */ true);
+  }
+
+  private void testRebaseEditWithConflictsAllowed(boolean useDiff3) throws Exception {
     // Create change where FILE_NAME has OLD_CONTENT
     String changeId = newChange(admin.newIdent());
 
     PatchSet previousPatchSet = getCurrentPatchSet(changeId);
+    String subjectPreviousPatchSet = gApi.changes().id(changeId).get().subject;
     createEmptyEditFor(changeId);
     gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
 
@@ -378,17 +351,21 @@
         String.format(
                 "<<<<<<< PATCH SET (%s %s)\n"
                     + "%s\n"
-                    + "||||||| BASE\n"
-                    + "%s\n"
+                    + (useDiff3
+                        ? String.format(
+                            "||||||| BASE      (%s %s)\n%s\n",
+                            previousPatchSet.commitId().name(),
+                            subjectPreviousPatchSet,
+                            CONTENT_OLD_STR)
+                        : "")
                     + "=======\n"
                     + "%s\n"
                     + ">>>>>>> EDIT      (%s %s)\n",
-                ObjectIds.abbreviateName(currentPatchSet.commitId(), 6),
+                currentPatchSet.commitId().getName(),
                 gApi.changes().id(changeId).get().subject,
                 CONTENT_NEW2_STR,
-                CONTENT_OLD_STR,
                 CONTENT_NEW_STR,
-                ObjectIds.abbreviateName(ObjectId.fromString(originalEdit.get().commit.commit), 6),
+                originalEdit.get().commit.commit,
                 originalEdit.get().commit.subject)
             .getBytes(UTF_8));
     assertThat(rebasedEdit).baseRevision().isEqualTo(currentPatchSet.commitId().name());
@@ -423,7 +400,7 @@
     Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
 
     // Create change to edit
-    Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+    Change.Id change = changeOperations.newChange().project(project).owner(testUser).createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -511,7 +488,7 @@
             .file(FILE_NAME)
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -870,7 +847,7 @@
     Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
 
     // Create change to edit
-    Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+    Change.Id change = changeOperations.newChange().project(project).owner(testUser).createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -1046,7 +1023,7 @@
             .file(FILE_NAME)
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -1081,7 +1058,7 @@
             .file(FILE_NAME)
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -1143,7 +1120,7 @@
             .file(FILE_NAME)
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
@@ -1217,21 +1194,148 @@
   }
 
   @Test
-  public void changeEditModifyFileModeRest() throws Exception {
-    createEmptyEditFor(changeId);
+  public void changeEditNewFileIsCreatedAsRegularFileIfFileModeIsNotSet() throws Exception {
+    String fileName = "new.txt";
+
     FileContentInput in = new FileContentInput();
-    in.binary_content = CONTENT_BINARY_ENCODED_NEW;
-    in.fileMode = FILE_MODE;
-    adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW;
+    gApi.changes().id(changeId).edit().modifyFile(fileName, in);
+
+    ensureSameBytes(getFileContentOfEdit(changeId, fileName), CONTENT_BINARY_DECODED_NEW);
+
+    // Publish the change edit so that we can assert the file mode.
+    gApi.changes().id(changeId).edit().publish(new PublishChangeEditInput());
+
+    int mode = gApi.changes().id(changeId).get().getCurrentRevision().files.get(fileName).newMode;
+    assertThat(mode).isEqualTo(FileMode.REGULAR_FILE.getMode());
+  }
+
+  @Test
+  public void changeEditModifyingFileDoesNotUpdateFileModeIfFileModeIsNotSet() throws Exception {
+    FileContentInput in = new FileContentInput();
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW;
+    in.fileMode = FileMode.EXECUTABLE_FILE.getModeAsOctal();
+    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, in);
+
     ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW);
+
+    // Update the file content without setting the file mode:
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW2;
+    in.fileMode = null;
+    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, in);
+
+    ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW2);
+
+    // Publish the change edit so that we can assert the file mode.
+    gApi.changes().id(changeId).edit().publish(new PublishChangeEditInput());
+
+    int mode = gApi.changes().id(changeId).get().getCurrentRevision().files.get(FILE_NAME).newMode;
+    assertThat(mode).isEqualTo(FileMode.EXECUTABLE_FILE.getMode());
+  }
+
+  @Test
+  public void changeEditModifyFileMode() throws Exception {
+    int mode = gApi.changes().id(changeId).get().getCurrentRevision().files.get(FILE_NAME).newMode;
+    assertThat(mode).isEqualTo(FileMode.REGULAR_FILE.getMode());
+
+    FileContentInput in = new FileContentInput();
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW;
+    in.fileMode = FileMode.EXECUTABLE_FILE.getModeAsOctal();
+    gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, in);
+
+    ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW);
+
+    // Publish the change edit so that we can assert the new file mode.
+    gApi.changes().id(changeId).edit().publish(new PublishChangeEditInput());
+
+    mode = gApi.changes().id(changeId).get().getCurrentRevision().files.get(FILE_NAME).newMode;
+    assertThat(mode).isEqualTo(FileMode.EXECUTABLE_FILE.getMode());
+  }
+
+  @Test
+  public void changeEditCreateGitLink() throws Exception {
+    String gitlinkFileName = "foo/bar";
+    byte[] gitlinkContent = "7bcbf0ae382f9a20f96563b6af2c0e6fff20a22f".getBytes(UTF_8);
+
+    FileContentInput in = new FileContentInput();
+    in.binaryContent =
+        "data:text/plain;base64," + Base64.getEncoder().encodeToString(gitlinkContent);
+    in.fileMode = FileMode.GITLINK.getModeAsOctal();
+    gApi.changes().id(changeId).edit().modifyFile(gitlinkFileName, in);
+
+    ensureSameBytes(getFileContentOfEdit(changeId, gitlinkFileName), gitlinkContent);
+
+    // Publish the change edit so that we can assert the file mode.
+    gApi.changes().id(changeId).edit().publish(new PublishChangeEditInput());
+
+    int mode =
+        gApi.changes().id(changeId).get().getCurrentRevision().files.get(gitlinkFileName).newMode;
+    assertThat(mode).isEqualTo(FileMode.GITLINK.getMode());
+  }
+
+  @Test
+  public void changeEditCreateSymLink() throws Exception {
+    String symlinkFileName = "foo/bar";
+    byte[] symlinkContent = "path/to/target".getBytes(UTF_8);
+
+    FileContentInput in = new FileContentInput();
+    in.binaryContent =
+        "data:text/plain;base64," + Base64.getEncoder().encodeToString(symlinkContent);
+    in.fileMode = FileMode.SYMLINK.getModeAsOctal();
+    gApi.changes().id(changeId).edit().modifyFile(symlinkFileName, in);
+
+    ensureSameBytes(getFileContentOfEdit(changeId, symlinkFileName), symlinkContent);
+
+    // Publish the change edit so that we can assert the file mode.
+    gApi.changes().id(changeId).edit().publish(new PublishChangeEditInput());
+
+    int mode =
+        gApi.changes().id(changeId).get().getCurrentRevision().files.get(symlinkFileName).newMode;
+    assertThat(mode).isEqualTo(FileMode.SYMLINK.getMode());
+  }
+
+  @Test
+  public void changeEditCreateGitLink_invalidContentIsRejected() throws Exception {
+    String gitlinkFileName = "foo/bar";
+    byte[] invalidGitlinkContent = "not a SHA1".getBytes(UTF_8);
+
+    FileContentInput in = new FileContentInput();
+    in.binaryContent =
+        "data:text/plain;base64," + Base64.getEncoder().encodeToString(invalidGitlinkContent);
+    in.fileMode = FileMode.GITLINK.getModeAsOctal();
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(changeId).edit().modifyFile(gitlinkFileName, in));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format("The content for gitlink '%s' must be a valid SHA1.", gitlinkFileName));
+  }
+
+  @Test
+  public void changeEditModifyFileMode_invalidFileModeIsRejected() throws Exception {
+    FileContentInput in = new FileContentInput();
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW;
+    in.fileMode = 16;
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> gApi.changes().id(changeId).edit().modifyFile(FILE_NAME, in));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "file mode %s is invalid: supported values are 100644 (regular file), 100755"
+                    + " (executable file), 120000 (symlink) and 160000 (gitlink)",
+                in.fileMode));
   }
 
   @Test
   public void changeEditModifyFileSetEmptyContentModeRest() throws Exception {
     createEmptyEditFor(changeId);
     FileContentInput in = new FileContentInput();
-    in.binary_content = CONTENT_BINARY_ENCODED_EMPTY;
-    in.fileMode = FILE_MODE;
+    in.binaryContent = CONTENT_BINARY_ENCODED_EMPTY;
     adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
     ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), "".getBytes(UTF_8));
   }
@@ -1239,10 +1343,10 @@
   @Test
   public void createAndUploadBinaryInChangeEditOneRequestRest() throws Exception {
     FileContentInput in = new FileContentInput();
-    in.binary_content = CONTENT_BINARY_ENCODED_NEW;
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW;
     adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
     ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW);
-    in.binary_content = CONTENT_BINARY_ENCODED_NEW2;
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW2;
     adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertNoContent();
     ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_BINARY_DECODED_NEW2);
   }
@@ -1250,7 +1354,7 @@
   @Test
   public void invalidBase64UploadBinaryInChangeEditOneRequestRest() throws Exception {
     FileContentInput in = new FileContentInput();
-    in.binary_content = CONTENT_BINARY_ENCODED_NEW3;
+    in.binaryContent = CONTENT_BINARY_ENCODED_NEW3;
     adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
   }
 
@@ -1259,7 +1363,7 @@
     createEmptyEditFor(changeId);
 
     FileContentInput in = new FileContentInput();
-    in.binary_content = null;
+    in.binaryContent = null;
     adminRestSession.put(urlEditFile(changeId, FILE_NAME), in).assertBadRequest();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 16d1996..bfa5130 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -53,6 +53,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Streams;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -69,7 +70,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccountGroup;
-import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
@@ -112,10 +112,12 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.receive.NoteDbPushOption;
+import com.google.gerrit.server.git.receive.PushOptionsValidator;
 import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.git.validators.CommitValidationInfo;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -1649,6 +1651,23 @@
   }
 
   @Test
+  public void pushNewChangeWithHashtagsWhileUpdatingExisting() throws Exception {
+    String hashtag1 = "tag1";
+    String hashtag2 = "tag2";
+
+    @SuppressWarnings("unused")
+    PushOneCommit.Result r1 = pushTo("refs/for/master%hashtag=" + hashtag1);
+    testRepo
+        .amendRef("HEAD")
+        .add(PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT + "2")
+        .create();
+    PushOneCommit.Result r2 = pushTo("refs/for/master%hashtag=" + hashtag2);
+    ImmutableSet<String> expected = ImmutableSet.of(hashtag2);
+    Set<String> hashtags = gApi.changes().id(r2.getChangeId()).getHashtags();
+    assertThat(hashtags).containsExactlyElementsIn(expected);
+  }
+
+  @Test
   public void pushCommitUsingSignedOffBy() throws Exception {
     PushOneCommit push =
         pushFactory.create(
@@ -1792,17 +1811,23 @@
   public void pushToNonVisibleBranchIsRejected() throws Exception {
     String master = "refs/heads/master";
 
+    // Do the test as a non-admin user, since admins can always see all branches, even if read is
+    // blocked.
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    userRepo.branch("HEAD").commit().message("New Commit 1").insertChangeId().create();
+
     projectOperations
         .project(project)
         .forUpdate()
+        .add(allow(Permission.PUSH).ref(master).group(REGISTERED_USERS))
+        .add(allow(Permission.FORGE_COMMITTER).ref(master).group(REGISTERED_USERS))
         .add(block(Permission.READ).ref(master).group(REGISTERED_USERS))
         .update();
 
-    testRepo.branch("HEAD").commit().message("New Commit 1").insertChangeId().create();
     // Since the branch is not visible to the caller, the command tries to create the ref resulting
     // in the command being rejected because the ref already exists.
     assertPushRejected(
-        pushHead(testRepo, master),
+        pushHead(userRepo, master),
         master,
         "Cannot create ref 'refs/heads/master' because it already exists.");
 
@@ -1812,8 +1837,8 @@
         .add(allow(Permission.READ).ref(master).group(REGISTERED_USERS))
         .update();
 
-    testRepo.branch("HEAD").commit().message("New Commit 2").insertChangeId().create();
-    assertPushOk(pushHead(testRepo, master), master);
+    userRepo.branch("HEAD").commit().message("New Commit 2").insertChangeId().create();
+    assertPushOk(pushHead(userRepo, master), master);
   }
 
   @Test
@@ -2128,10 +2153,13 @@
         pushFactory.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "a.txt", "content");
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    RevCommit commitChange1 = r.getCommit();
+    RevCommit commit = r.getCommit();
 
-    createCommit(testRepo, commitChange1.getFullMessage());
+    RevCommit followUpCommitWithSameChangeId = createCommit(testRepo, commit.getFullMessage());
 
+    // The parent commit of followUpCommitWithSameChangeId is the current patch set of the change.
+    // Pushing followUpCommitWithSameChangeId fails because it has the same change ID as its parent
+    // commit and having dependencies between patch sets of the same change is not allowed.
     pushForReviewRejected(
         testRepo,
         "same Change-Id in multiple changes.\n"
@@ -2152,6 +2180,22 @@
         "same Change-Id in multiple changes.\n"
             + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
             + " commit");
+
+    // Amend the change and check that pushing the followUpCommitWithSameChangeId still fails
+    testRepo.reset(commit);
+    r = amendChange(r.getChangeId());
+    r.assertOkStatus();
+
+    // The parent commit of followUpCommitWithSameChangeId is an outdated patch set of the change
+    // now. Pushing followUpCommitWithSameChangeId still fails because it has the same change ID as
+    // its parent commit and having dependencies between patch sets of the same change is not
+    // allowed.
+    testRepo.reset(followUpCommitWithSameChangeId);
+    pushForReviewRejected(
+        testRepo,
+        "same Change-Id in multiple changes.\n"
+            + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+            + " commit");
   }
 
   @Test
@@ -2298,27 +2342,6 @@
   }
 
   @Test
-  public void pushWithEmailInFooter() throws Exception {
-    pushWithReviewerInFooter(user.getNameEmail().toString(), user);
-  }
-
-  @Test
-  public void pushWithNameInFooter() throws Exception {
-    pushWithReviewerInFooter(user.fullName(), user);
-  }
-
-  @Test
-  public void pushWithEmailInFooterNotFound() throws Exception {
-    pushWithReviewerInFooter(
-        Address.create("No Body", "notarealuser@example.com").toString(), null);
-  }
-
-  @Test
-  public void pushWithNameInFooterNotFound() throws Exception {
-    pushWithReviewerInFooter("Notauser", null);
-  }
-
-  @Test
   public void pushNewPatchsetOverridingStickyLabel() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
       LabelType codeReview = TestLabels.codeReview().toBuilder().setCopyCondition("is:MAX").build();
@@ -3235,6 +3258,38 @@
   }
 
   @Test
+  public void pushWithMalformedCustomKeyedValuesRejects() throws Exception {
+    List<String> pushOptions = new ArrayList<>();
+    pushOptions.add("custom-keyed-value=foo-bar");
+
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    push.setPushOptions(pushOptions);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertErrorStatus(
+        "the value for option 'custom-keyed-value' must be given as '<key>:<format>'");
+  }
+
+  @Test
+  public void pushWithCustomKeyedValuesPasses() throws Exception {
+    List<String> pushOptions = new ArrayList<>();
+    pushOptions.add("custom-keyed-value=foo:bar");
+
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    push.setPushOptions(pushOptions);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertOkStatus();
+    assertThat(r.getChange().customKeyedValues()).isEqualTo(ImmutableMap.of("foo", "bar"));
+  }
+
+  @Test
+  public void cannotPushWithCustomKeyedValuesInRefName() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master%custom-keyed-value=foo-bar");
+    r.assertErrorStatus(
+        "option 'custom-keyed-value' cannot be specified as an option in the ref name, use a push"
+            + " option instead ('-o custom-keyed-value=<key>:<value>')");
+  }
+
+  @Test
   public void pushForMasterWithUnknownOption() throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(ImmutableList.of("unknown=foo"));
@@ -3379,6 +3434,104 @@
     }
   }
 
+  @Test
+  public void rejectPushOptionFromValidator() throws Exception {
+    PushOptionsValidator pushOptionsValidator =
+        (Project.NameKey projectName, String refName, ListMultimap<String, String> pushOptions) ->
+            pushOptions.containsKey("topic")
+                ? ImmutableList.of(
+                    new ValidationMessage("setting a topic is not allowed", /* isError= */ true))
+                : ImmutableList.of();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(pushOptionsValidator)) {
+      // Push with a push option that is being rejected.
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      List<String> pushOptions = new ArrayList<>();
+      pushOptions.add("topic=myTopic");
+      push.setPushOptions(pushOptions);
+      PushOneCommit.Result r = push.to("refs/for/master");
+      r.assertErrorStatus("setting a topic is not allowed");
+
+      // Push with an option in the refname that is being rejected.
+      r = pushTo("refs/for/master%topic=myTopic");
+      r.assertErrorStatus("setting a topic is not allowed");
+
+      // Pushing with another push options is fine.
+      pushOptions = new ArrayList<>();
+      pushOptions.add("notify=NONE");
+      push.setPushOptions(pushOptions);
+      r = push.to("refs/for/master");
+      r.assertOkStatus();
+
+      // Pushing with another option in the refname is fine.
+      r = pushTo("refs/for/master%notify=NONE");
+      r.assertOkStatus();
+    }
+  }
+
+  @Test
+  public void printMessageForPushOptionFromValidator() throws Exception {
+    PushOptionsValidator pushOptionsValidator =
+        (Project.NameKey projectName, String refName, ListMultimap<String, String> pushOptions) ->
+            pushOptions.containsKey("topic")
+                ? ImmutableList.of(
+                    new ValidationMessage(
+                        "setting a topic is deprecated", ValidationMessage.Type.WARNING))
+                : ImmutableList.of();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(pushOptionsValidator)) {
+      // Push with a push option that is being rejected.
+      PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+      List<String> pushOptions = new ArrayList<>();
+      pushOptions.add("topic=myTopic");
+      push.setPushOptions(pushOptions);
+      PushOneCommit.Result r = push.to("refs/for/master");
+      r.assertOkStatus();
+      r.assertMessage("warning: setting a topic is deprecated");
+
+      // Push with an option in the refname that is being rejected.
+      r = pushTo("refs/for/master%topic=myTopic");
+      r.assertOkStatus();
+      r.assertMessage("warning: setting a topic is deprecated");
+
+      // Pushing with another push options doesn't print the message.
+      push = pushFactory.create(admin.newIdent(), testRepo);
+      pushOptions = new ArrayList<>();
+      pushOptions.add("notify=NONE");
+      push.setPushOptions(pushOptions);
+      r = push.to("refs/for/master");
+      r.assertOkStatus();
+      r.assertNotMessage("warning: setting a topic is deprecated");
+
+      // Pushing with another option in the refname doesn't print the message.
+      r = pushTo("refs/for/master%notify=NONE");
+      r.assertOkStatus();
+      r.assertNotMessage("warning: setting a topic is deprecated");
+    }
+  }
+
+  @Test
+  public void cannotSetPushJustificationWhenSubmitOptionIsNotUsed() throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    List<String> pushOptions = new ArrayList<>();
+    pushOptions.add("push-justification=test justification");
+    push.setPushOptions(pushOptions);
+    PushOneCommit.Result r = push.to("refs/for/master");
+    r.assertErrorStatus(
+        "when pushing for a review a push justification can only be set when the"
+            + " 'submit' option is used");
+
+    // pushing with the submit option works
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    r = push.to("refs/for/master%submit");
+    r.assertOkStatus();
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;
@@ -3416,37 +3569,6 @@
         .isEqualTo(reviewer.email());
   }
 
-  private void pushWithReviewerInFooter(String nameEmail, TestAccount expectedReviewer)
-      throws Exception {
-    int n = 5;
-    String r = "refs/for/master";
-    ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
-    List<RevCommit> commits = createChanges(n, r, ImmutableList.of("Acked-By: " + nameEmail));
-    for (int i = 0; i < n; i++) {
-      RevCommit c = commits.get(i);
-      ChangeData cd = byCommit(c);
-      String name = "reviewers for " + (i + 1);
-      if (expectedReviewer != null) {
-        assertWithMessage(name).that(cd.reviewers().all()).containsExactly(expectedReviewer.id());
-        // Remove reviewer from PS1 so we can test adding this same reviewer on PS2 below.
-        gApi.changes().id(cd.getId().get()).reviewer(expectedReviewer.id().toString()).remove();
-      }
-      assertWithMessage(name).that(byCommit(c).reviewers().all()).isEmpty();
-    }
-
-    List<RevCommit> commits2 = amendChanges(initialHead, commits, r);
-    for (int i = 0; i < n; i++) {
-      RevCommit c = commits2.get(i);
-      ChangeData cd = byCommit(c);
-      String name = "reviewers for " + (i + 1);
-      if (expectedReviewer != null) {
-        assertWithMessage(name).that(cd.reviewers().all()).containsExactly(expectedReviewer.id());
-      } else {
-        assertWithMessage(name).that(byCommit(c).reviewers().all()).isEmpty();
-      }
-    }
-  }
-
   private List<RevCommit> createChanges(int n, String refsFor) throws Exception {
     return createChanges(n, refsFor, ImmutableList.of());
   }
diff --git a/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java b/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
index 3b158a9..25d25c7 100644
--- a/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
@@ -23,19 +24,28 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.MergeInput;
+import com.google.inject.Inject;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.junit.Before;
 import org.junit.Test;
 
 /** Ensures that auto merge commits are created when a new patch set or change is uploaded. */
 public class AutoMergeIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   private RevCommit parent1;
   private RevCommit parent2;
 
@@ -100,6 +110,187 @@
   }
 
   @Test
+  public void autoMergeCreatedWhenPushingMergeBetweenTwoInitialCommits() throws Exception {
+    Project.NameKey projectWithoutInitialCommit =
+        projectOperations.newProject().createEmptyCommit(false).create();
+
+    TestRepository<InMemoryRepository> testRepo =
+        cloneProject(projectWithoutInitialCommit, getCloneAsAccount(configRule.description()));
+
+    RevCommit initialCommit1 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message("Initial Change 1")
+                .insertChangeId()
+                .add("file1", "contents1")
+                .create());
+    RevCommit initialCommit2 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message("Initial Change 2")
+                .insertChangeId()
+                .add("file1", "contents2")
+                .create());
+    RevCommit mergeCommit =
+        testRepo
+            .branch("master")
+            .commit()
+            .message("Merge Change")
+            .parent(initialCommit1)
+            .parent(initialCommit2)
+            .insertChangeId()
+            .create();
+    testRepo.reset(mergeCommit);
+    PushResult r = pushHead(testRepo, "refs/for/master");
+    assertThat(r.getRemoteUpdate("refs/for/master").getStatus()).isEqualTo(Status.OK);
+
+    assertAutoMergeCreated(projectWithoutInitialCommit, mergeCommit);
+  }
+
+  @Test
+  public void autoMergeCreatedWhenPushingCrissCrossMerge() throws Exception {
+    RevCommit initialCommit = repo().parseCommit(repo().exactRef(HEAD).getLeaf().getObjectId());
+    RevCommit baseCommit1 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .parent(initialCommit)
+                .message("Change 1")
+                .insertChangeId()
+                .add("file1", "contents1")
+                .create());
+    RevCommit baseCommit2 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .parent(initialCommit)
+                .message("Change 2")
+                .insertChangeId()
+                .add("file2", "contents2")
+                .create());
+    RevCommit mergeCommit1 =
+        testRepo
+            .commit()
+            .message("Merge Change In Source")
+            .parent(baseCommit1)
+            .parent(baseCommit2)
+            .insertChangeId()
+            .create();
+    RevCommit mergeCommit2 =
+        testRepo
+            .commit()
+            .message("Merge Change In Target")
+            .parent(baseCommit2)
+            .parent(baseCommit1)
+            .insertChangeId()
+            .create();
+    RevCommit conflictingCommit1 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message("Change 1")
+                .parent(mergeCommit1)
+                .insertChangeId()
+                .add("conflicting-file", "contents1")
+                .create());
+    RevCommit conflictingCommit2 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message("Change 2")
+                .parent(mergeCommit2)
+                .insertChangeId()
+                .add("conflicting-file", "contents2")
+                .create());
+    RevCommit crissCrossMerge =
+        testRepo
+            .commit()
+            .message("Criss-Cross Merge")
+            .parent(conflictingCommit1)
+            .parent(conflictingCommit2)
+            .insertChangeId()
+            .create();
+    testRepo.reset(crissCrossMerge);
+    PushResult r = pushHead(testRepo, "refs/for/master");
+    assertThat(r.getRemoteUpdate("refs/for/master").getStatus()).isEqualTo(Status.OK);
+    assertAutoMergeCreated(crissCrossMerge);
+  }
+
+  @Test
+  public void autoMergeCreatedWhenPushingCrissCrossMergeWithConflictingBases() throws Exception {
+    RevCommit initialCommit = repo().parseCommit(repo().exactRef(HEAD).getLeaf().getObjectId());
+    String baseFile = "baseFile,txt";
+    RevCommit baseCommit1 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .parent(initialCommit)
+                .message("Change 1")
+                .insertChangeId()
+                .add(baseFile, "contents1")
+                .create());
+    RevCommit baseCommit2 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .parent(initialCommit)
+                .message("Change 2")
+                .insertChangeId()
+                .add(baseFile, "contents2")
+                .create());
+    RevCommit mergeCommit1 =
+        testRepo
+            .commit()
+            .message("Merge Change In Source")
+            .add(baseFile, "contents1")
+            .parent(baseCommit1)
+            .parent(baseCommit2)
+            .insertChangeId()
+            .create();
+    RevCommit mergeCommit2 =
+        testRepo
+            .commit()
+            .message("Merge Change In Target")
+            .add(baseFile, "contents2")
+            .parent(baseCommit2)
+            .parent(baseCommit1)
+            .insertChangeId()
+            .create();
+    RevCommit conflictingCommit1 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message("Change 1")
+                .parent(mergeCommit1)
+                .insertChangeId()
+                .add("conflicting-file", "contents1")
+                .create());
+    RevCommit conflictingCommit2 =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message("Change 2")
+                .parent(mergeCommit2)
+                .insertChangeId()
+                .add("conflicting-file", "contents2")
+                .create());
+    RevCommit crissCrossMerge =
+        testRepo
+            .commit()
+            .message("Criss-Cross Merge")
+            .parent(conflictingCommit1)
+            .parent(conflictingCommit2)
+            .insertChangeId()
+            .create();
+    testRepo.reset(crissCrossMerge);
+    PushResult r = pushHead(testRepo, "refs/for/master");
+    assertThat(r.getRemoteUpdate("refs/for/master").getStatus()).isEqualTo(Status.OK);
+    assertAutoMergeCreated(crissCrossMerge);
+  }
+
+  @Test
   public void autoMergeCreatedWhenChangeCreatedOnApi() throws Exception {
     ChangeInput ci = new ChangeInput(project.get(), "master", "Merge commit");
     ci.merge = new MergeInput();
@@ -187,6 +378,11 @@
   }
 
   private void assertAutoMergeCreated(ObjectId mergeCommit) throws Exception {
+    assertAutoMergeCreated(project, mergeCommit);
+  }
+
+  private void assertAutoMergeCreated(Project.NameKey project, ObjectId mergeCommit)
+      throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
       assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNotNull();
     }
diff --git a/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java b/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java
index c059ae4..e419226 100644
--- a/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/DirectPushRefUpdateContextIT.java
@@ -14,19 +14,31 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static java.util.function.Function.identity;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+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.entities.Permission;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
 import com.google.gerrit.testing.RefUpdateContextCollector;
+import com.google.inject.Inject;
 import java.util.Map.Entry;
 import org.junit.Rule;
 import org.junit.Test;
 
 public class DirectPushRefUpdateContextIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
+
   @Rule
   public RefUpdateContextCollector refUpdateContextCollector = new RefUpdateContextCollector();
 
@@ -69,4 +81,74 @@
     assertThat(ctx.getUpdateType()).isEqualTo(RefUpdateType.DIRECT_PUSH);
     assertThat(ctx.getJustification()).hasValue("test justification");
   }
+
+  @Test
+  public void submitOnPushWithoutJustification_emptyJustification() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
+    push.setPushOptions(ImmutableList.of("submit"));
+    PushOneCommit.Result result = push.to("refs/for/master");
+
+    result.assertOkStatus();
+    ImmutableList<Entry<String, ImmutableList<RefUpdateContext>>> updates =
+        refUpdateContextCollector.getContextsByUpdateType(RefUpdateType.DIRECT_PUSH);
+    ImmutableMap<String, ImmutableList<RefUpdateContext>> refUpdateContextsByRef =
+        updates.stream().collect(toImmutableMap(Entry::getKey, Entry::getValue));
+    assertThat(refUpdateContextsByRef.keySet())
+        .containsExactly("refs/heads/master", RefNames.changeMetaRef(result.getChange().getId()));
+
+    ImmutableList<RefUpdateContext> ctxts = refUpdateContextsByRef.get("refs/heads/master");
+    ImmutableListMultimap<RefUpdateContext.RefUpdateType, RefUpdateContext>
+        refUpdateContextsByType =
+            ctxts.stream()
+                .collect(toImmutableListMultimap(RefUpdateContext::getUpdateType, identity()));
+    assertThat(refUpdateContextsByType.keySet())
+        .containsExactly(RefUpdateType.DIRECT_PUSH, RefUpdateType.MERGE_CHANGE);
+    ImmutableList<RefUpdateContext> directPushRefUpdatesContexts =
+        refUpdateContextsByType.get(RefUpdateType.DIRECT_PUSH);
+    assertThat(directPushRefUpdatesContexts).hasSize(1);
+    RefUpdateContext directPushUpdateCtx = directPushRefUpdatesContexts.get(0);
+    assertThat(directPushUpdateCtx.getJustification()).isEmpty();
+  }
+
+  @Test
+  public void submitOnPushWithJustification_justificationStoredInContext() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    PushOneCommit push =
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
+    push.setPushOptions(ImmutableList.of("push-justification=test justification", "submit"));
+    PushOneCommit.Result result = push.to("refs/for/master");
+
+    result.assertOkStatus();
+    ImmutableList<Entry<String, ImmutableList<RefUpdateContext>>> updates =
+        refUpdateContextCollector.getContextsByUpdateType(RefUpdateType.DIRECT_PUSH);
+    ImmutableMap<String, ImmutableList<RefUpdateContext>> refUpdateContextsByRef =
+        updates.stream().collect(toImmutableMap(Entry::getKey, Entry::getValue));
+    assertThat(refUpdateContextsByRef.keySet())
+        .containsExactly("refs/heads/master", RefNames.changeMetaRef(result.getChange().getId()));
+
+    ImmutableList<RefUpdateContext> ctxts = refUpdateContextsByRef.get("refs/heads/master");
+    ImmutableListMultimap<RefUpdateContext.RefUpdateType, RefUpdateContext>
+        refUpdateContextsByType =
+            ctxts.stream()
+                .collect(toImmutableListMultimap(RefUpdateContext::getUpdateType, identity()));
+    assertThat(refUpdateContextsByType.keySet())
+        .containsExactly(RefUpdateType.DIRECT_PUSH, RefUpdateType.MERGE_CHANGE);
+    ImmutableList<RefUpdateContext> directPushRefUpdatesContexts =
+        refUpdateContextsByType.get(RefUpdateType.DIRECT_PUSH);
+    assertThat(directPushRefUpdatesContexts).hasSize(1);
+    RefUpdateContext directPushUpdateCtx = directPushRefUpdatesContexts.get(0);
+    assertThat(directPushUpdateCtx.getJustification()).hasValue("test justification");
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java
index 90ba7c1..9ea2a83 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckOnReceiveIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.git.ObjectIds;
 import java.util.Locale;
 import org.eclipse.jgit.lib.ObjectId;
@@ -53,6 +54,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void implicitMergeCheckOff() throws Exception {
     ObjectId base = repo().exactRef("HEAD").getObjectId();
     push("refs/heads/stable", "0", "f", "0");
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
index ba881b6..1c74165 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeOnSubmitIT.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -69,12 +70,14 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void singleChangeImplicitMerge() throws Exception {
     PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
     assertSubmitRejectedWithImplicitMerge(implicitMerge.getChangeId());
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void chainOfChangesImplicitMerge() throws Exception {
     PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
     PushOneCommit.Result c1 = createApprovedChange("master", implicitMerge);
@@ -107,6 +110,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void chainOfChangesNotOnTopOfTargetBranchTipWithImplicitMerge() throws Exception {
     // Add one more commit to master branch.
     pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
@@ -119,6 +123,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void chainOfChangesEndsWithExplicitMerge_onlyExplcitMergeCanBeSubmitted()
       throws Exception {
     PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
@@ -131,6 +136,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void twoChainOfChangesSameTopic_oneChainImplicitMerge_rejectedOnSubmit() throws Exception {
     cfg.setBoolean("change", null, "submitWholeTopic", true);
     PushOneCommit.Result c1 = createApprovedChange("master", masterTip);
@@ -206,6 +212,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void twoChainOfChangesEndsWithExplicitMergeSameTopicNotTipOfBranches_canBeSubmitted()
       throws Exception {
     cfg.setBoolean("change", null, "submitWholeTopic", true);
@@ -235,6 +242,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void twoChainOfChangesDifferentBranchesSameTopic_oneChainImplicitMerge_rejectedOnSubmit()
       throws Exception {
     cfg.setBoolean("change", null, "submitWholeTopic", true);
@@ -260,6 +268,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void explicitMergeOnTopOfChain_onlyTopSubmittable() throws Exception {
     PushOneCommit.Result implicitMerge = createApprovedChange("master", otherTip);
     PushOneCommit.Result im1 = createApprovedChange("master", implicitMerge);
@@ -273,6 +282,7 @@
   }
 
   @Test
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void explicitMergeOnTopOfChainParentIsNotBranchTip_onlyTopSubmittable() throws Exception {
     // Add one more commit to master and other branches.
     pushTo("refs/heads/master", ImmutableMap.of(), masterTip);
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index bb9228d..90a5d94 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -97,7 +97,7 @@
 
   @Test
   public void pushNewCommitsRequiresPushPermission() throws Exception {
-    testRepo.branch("HEAD").commit().create();
+    RevCommit commit = testRepo.branch("HEAD").commit().create();
     projectOperations
         .project(project)
         .forUpdate()
@@ -106,10 +106,14 @@
 
     PushResult r = push("HEAD:refs/heads/newbranch");
 
-    String msg = "update for creating new commit object not permitted";
+    String msg =
+        String.format(
+            "prohibited by Gerrit: Unable to resolve object '%s'. Check that the object exists on"
+                + " the server or get update permission to create new commit objects.",
+            commit);
     RemoteRefUpdate rru = r.getRemoteUpdate("refs/heads/newbranch");
     assertThat(rru.getStatus()).isNotEqualTo(Status.OK);
-    assertThat(rru.getMessage()).contains(msg);
+    assertThat(rru.getMessage()).isEqualTo(msg);
 
     projectOperations
         .project(project)
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 78136c8..969c98f 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 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.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;
@@ -216,6 +217,9 @@
     psRef4 = cd4.currentPatchSet().id().toRefName();
     metaRef4 = RefNames.changeMetaRef(cd4.getId());
 
+    // Reset HEAD to origin/master
+    testRepo.reset("origin/master");
+
     try (Repository repo = repoManager.openRepository(project)) {
       //   rcMaster (c1 master master-tag) <-- rcBranch (c2 branch)
       //       \                                  \
@@ -328,6 +332,34 @@
   }
 
   @Test
+  public void uploadPackAllRefsVisibleToAdminWithoutExplicitReadPermission() throws Exception {
+    // Remove read access
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    // Admins can always read all branches, tags and changes.
+    assertUploadPackRefsAsAdmin(
+        "HEAD",
+        psRef1,
+        metaRef1,
+        psRef2,
+        metaRef2,
+        psRef3,
+        metaRef3,
+        psRef4,
+        metaRef4,
+        "refs/heads/branch",
+        "refs/heads/master",
+        RefNames.REFS_CONFIG,
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag",
+        "refs/tags/tree-tag");
+  }
+
+  @Test
   public void grantReadOnRefsTagsIsNoOp() throws Exception {
     projectOperations
         .project(project)
@@ -1517,6 +1549,15 @@
     assertRefs(project, user, true, expectedRefs);
   }
 
+  /**
+   * Assert that refs seen by an admin user match the expected refs.
+   *
+   * @param expectedRefs expected refs.
+   */
+  private void assertUploadPackRefsAsAdmin(String... expectedRefs) throws Exception {
+    assertRefs(project, admin, true, expectedRefs);
+  }
+
   private void assertRefs(
       Project.NameKey project, TestAccount user, boolean disableDb, String... expectedRefs)
       throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index 66c4078..8f011f8 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -74,6 +74,39 @@
   }
 
   @Test
+  public void reindexWithReadOnlyCaches() throws Exception {
+    setUpChange();
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      assertThat(gApi.config().server().caches("diff_summary").get().entries.disk).isEqualTo(1L);
+      gApi.config().server().caches("diff_summary").flush();
+      assertThat(gApi.config().server().caches("diff_summary").get().entries.disk).isNull();
+    }
+
+    runGerrit(
+        "reindex",
+        "-d",
+        sitePaths.site_path.toString(),
+        "--show-stack-trace",
+        "--verbose",
+        "--read-only-disk-caches");
+    assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion());
+    assertIndexQueries();
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      assertThat(gApi.config().server().caches("diff_summary").get().entries.disk).isNull();
+    }
+
+    runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace", "--verbose");
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      assertThat(gApi.config().server().caches("diff_summary").get().entries.disk).isEqualTo(1L);
+    }
+  }
+
+  @Test
   public void reindexWithSkipExistingDocumentsEnabled() throws Exception {
     updateConfig(config -> config.setBoolean("index", null, "reuseExistingDocuments", true));
     setUpChange();
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index 9fab01b..4e24a94 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -10,7 +10,7 @@
         "pgm",
         "no_windows",
     ],
-    vm_args = ["-Xmx512m"],
+    vm_args = ["-Xmx1g"],
     deps = [
         ":util",
         "//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
index 98228be..b79977e 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
@@ -21,11 +21,13 @@
 import com.google.errorprone.annotations.MustBeClosed;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.server.account.AuthTokenCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import java.io.IOException;
@@ -82,8 +84,27 @@
     assertThat(projectsLastModified).isEqualTo(projectsLastModifiedAfterInit);
   }
 
-  private void initSite() throws Exception {
-    runGerrit("init", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+  @Test
+  public void initInDevModeCreatesAdminUserWithToken() throws Exception {
+    initSite("--dev");
+
+    try (ServerContext ctx = startServer()) {
+      AuthTokenCache tokenCache = ctx.getInjector().getInstance(AuthTokenCache.class);
+      assertThat(tokenCache.get(Account.id(10000))).isNotNull();
+    }
+  }
+
+  private void initSite(String... additionalOptions) throws Exception {
+    if (additionalOptions.length > 0) {
+      runGerrit(
+          "init",
+          "-d",
+          sitePaths.site_path.toString(),
+          "--show-stack-trace",
+          String.join(" ", additionalOptions));
+    } else {
+      runGerrit("init", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+    }
   }
 
   private void setProjectsIndexLastModifiedInThePast(Path indexDir, Instant time)
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
index 6df7b83..8e1b86e 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.extensions.common.SubmitRequirementInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.restapi.project.CreateLabel;
 import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement;
 import com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement.Status;
 import com.google.gerrit.server.schema.UpdateUI;
@@ -44,8 +45,8 @@
 /** Test for {@link com.google.gerrit.server.schema.MigrateLabelFunctionsToSubmitRequirement}. */
 @Sandboxed
 public class MigrateLabelFunctionsToSubmitRequirementIT extends AbstractDaemonTest {
-
   @Inject private ProjectOperations projectOperations;
+  @Inject private CreateLabel createLabel;
 
   @Test
   public void migrateBlockingLabel_maxWithBlock() throws Exception {
@@ -469,7 +470,7 @@
     input.function = function;
     input.ignoreSelfApproval = ignoreSelfApproval;
     input.values = values;
-    gApi.projects().name(project.get()).label(labelName).create(input);
+    createLabel.createLabelWithoutInputValidation(project, labelName, input);
   }
 
   private void createLabelWithBranch(
@@ -484,7 +485,7 @@
     input.ignoreSelfApproval = ignoreSelfApproval;
     input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
     input.branches = refPatterns;
-    gApi.projects().name(project.get()).label(labelName).create(input);
+    createLabel.createLabelWithoutInputValidation(project, labelName, input);
   }
 
   @CanIgnoreReturnValue
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigratePasswordsToTokensIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigratePasswordsToTokensIT.java
new file mode 100644
index 0000000..9b4c146
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigratePasswordsToTokensIT.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.server.account.PasswordMigrator;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import java.util.List;
+import org.junit.Test;
+
+@NoHttpd
+public class MigratePasswordsToTokensIT extends StandaloneSiteTest {
+
+  @Test
+  public void httpPasswordIsBeingMigratedToToken() throws Exception {
+    initSite();
+
+    String username = "foo";
+    String httpPassword = "secret";
+    int accountId;
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      accountId = gApi.accounts().create(username).detail()._accountId;
+      Project.NameKey allUsers = ctx.getInjector().getInstance(AllUsersName.class);
+      ExternalIdNotes extIdNotes = getExternalIdNotes(ctx, allUsers);
+      ExternalIdFactory extIdFactory = ctx.getInjector().getInstance(ExternalIdFactory.class);
+      MetaDataUpdate md = getMetaDataUpdate(ctx, allUsers);
+      extIdNotes.upsert(
+          extIdFactory.create(
+              SCHEME_USERNAME, username, Account.id(accountId), null, httpPassword));
+      extIdNotes.commit(md);
+    }
+
+    runGerrit("MigratePasswordsToTokens", "-d", sitePaths.site_path.toString());
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      List<AuthTokenInfo> actual = gApi.accounts().id(accountId).getTokens();
+      assertThat(actual.size()).isEqualTo(1);
+      assertThat(actual.get(0).id).isEqualTo(PasswordMigrator.DEFAULT_ID);
+      assertThat(actual.get(0).token).isNull();
+
+      Project.NameKey allUsers = ctx.getInjector().getInstance(AllUsersName.class);
+      ExternalIdNotes extIdNotes = getExternalIdNotes(ctx, allUsers);
+      ExternalIdKeyFactory extIdKeyFactory =
+          ctx.getInjector().getInstance(ExternalIdKeyFactory.class);
+      assertThat(extIdNotes.get(extIdKeyFactory.create(SCHEME_USERNAME, username)).get().password())
+          .isNull();
+    }
+  }
+
+  private void initSite() throws Exception {
+    runGerrit("init", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+  }
+
+  private static ExternalIdNotes getExternalIdNotes(ServerContext ctx, Project.NameKey allUsers)
+      throws Exception {
+    GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
+    ExternalIdNotes.FactoryNoReindex extIdNotesFactory =
+        ctx.getInjector().getInstance(ExternalIdNotes.FactoryNoReindex.class);
+    return extIdNotesFactory.load(repoManager.openRepository(allUsers));
+  }
+
+  private static MetaDataUpdate getMetaDataUpdate(ServerContext ctx, Project.NameKey allUsers)
+      throws Exception {
+    MetaDataUpdate.Server metaDataUpdateFactory =
+        ctx.getInjector().getInstance(MetaDataUpdate.Server.class);
+    return metaDataUpdateFactory.create(allUsers);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ReduceMaxTokenLifetimeIT.java b/javatests/com/google/gerrit/acceptance/pgm/ReduceMaxTokenLifetimeIT.java
new file mode 100644
index 0000000..af0ece7
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReduceMaxTokenLifetimeIT.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.auth.AuthTokenInfo;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import org.junit.Test;
+
+@NoHttpd
+public class ReduceMaxTokenLifetimeIT extends StandaloneSiteTest {
+  private static final String USERNAME = "foo";
+  private static final String TOKEN_ID = "id";
+
+  @Test
+  public void tokenWithNoLifetimeGetsLifetime() throws Exception {
+    initSite();
+    Account.Id accountId = createAccountWithToken(null);
+
+    runGerrit("ReduceMaxTokenLifetime", "-d", sitePaths.site_path.toString(), "--lifetime", "1d");
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      List<AuthTokenInfo> actual = gApi.accounts().id(accountId.get()).getTokens();
+      assertThat(actual.get(0).id).isEqualTo(TOKEN_ID);
+      assertThat(actual.get(0).expiration).isNotNull();
+    }
+  }
+
+  @Test
+  public void tokenWithTooLongLifetimeGetsAdaptedLifetime() throws Exception {
+    initSite();
+    Account.Id accountId = createAccountWithToken("2d");
+
+    runGerrit("ReduceMaxTokenLifetime", "-d", sitePaths.site_path.toString(), "--lifetime", "1d");
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      List<AuthTokenInfo> actual = gApi.accounts().id(accountId.get()).getTokens();
+      assertThat(actual.get(0).id).isEqualTo(TOKEN_ID);
+      assertThat(actual.get(0).expiration)
+          .isLessThan(Timestamp.from(Instant.now().plus(1, ChronoUnit.DAYS)));
+    }
+  }
+
+  @Test
+  public void tokenWithValidLifetimeDontGetAdapted() throws Exception {
+    initSite();
+    Account.Id accountId = createAccountWithToken("2h");
+
+    runGerrit("ReduceMaxTokenLifetime", "-d", sitePaths.site_path.toString(), "--lifetime", "1d");
+
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      List<AuthTokenInfo> actual = gApi.accounts().id(accountId.get()).getTokens();
+      assertThat(actual.get(0).id).isEqualTo(TOKEN_ID);
+      assertThat(actual.get(0).expiration)
+          .isLessThan(Timestamp.from(Instant.now().plus(2, ChronoUnit.HOURS)));
+    }
+  }
+
+  private void initSite() throws Exception {
+    runGerrit("init", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+  }
+
+  private Account.Id createAccountWithToken(@Nullable String tokenLifetime) throws Exception {
+    Account.Id accountId;
+    try (ServerContext ctx = startServer()) {
+      GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
+      accountId = Account.id(gApi.accounts().create(USERNAME).detail()._accountId);
+
+      AuthTokenInput input = new AuthTokenInput();
+      input.id = TOKEN_ID;
+      if (tokenLifetime != null) {
+        input.lifetime = tokenLifetime;
+      }
+      gApi.accounts().id(accountId.get()).createToken(input);
+    }
+    return accountId;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index 2cc4857..c3a497c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -183,7 +183,8 @@
             "/projects/" + name("new"),
             new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "invalid"));
     response.assertBadRequest();
-    assertThat(response.getEntityContent()).isEqualTo("Invalid deadline. Invalid value: invalid");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Invalid deadline. Invalid time unit value: invalid");
   }
 
   @Test
@@ -664,7 +665,7 @@
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Invalid deadline. Invalid value: invalid");
+    r.assertErrorStatus("Invalid deadline. Invalid time unit value: invalid");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
index b398d57..058c920 100644
--- a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.httpd.restapi.RestApiServlet.X_GERRIT_UPDATED_REF;
 import static com.google.gerrit.httpd.restapi.RestApiServlet.X_GERRIT_UPDATED_REF_ENABLED;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
 import static org.apache.http.HttpStatus.SC_OK;
 
 import com.google.common.collect.ImmutableList;
@@ -25,9 +26,11 @@
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.restapi.Url;
@@ -51,6 +54,7 @@
       new BasicHeader(X_GERRIT_UPDATED_REF_ENABLED, "true");
   private static Pattern ANY_SPACE = Pattern.compile("\\s");
 
+  @Inject private ChangeOperations changeOperations;
   @Inject private ProjectOperations projectOperations;
 
   @Test
@@ -492,6 +496,20 @@
     anonymousRestSession.get("/" + changeNumber).assertTemporaryRedirect(redirectUri);
   }
 
+  @Test
+  public void unrecognizedOptionIsRejectedAsBadRequest() throws Exception {
+    String unrecognizedOption = "@";
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    RestResponse response =
+        adminRestSession.get(
+            String.format(
+                "/changes/%s/submitted_together?o=%s", changeIdentifier.id(), unrecognizedOption));
+    assertThat(response.getStatusCode()).isEqualTo(SC_BAD_REQUEST);
+
+    assertThat(response.getEntityContent())
+        .isEqualTo(String.format("option not recognized: %s", unrecognizedOption));
+  }
+
   private ObjectId getMetaRefSha1(Result change) {
     return change.getChange().notes().getRevision();
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java b/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
index 5fef74c..36ce553 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CreateAccountIT.java
@@ -19,10 +19,19 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import java.util.List;
 import org.junit.Test;
 
 public class CreateAccountIT extends AbstractDaemonTest {
+  @Inject AuthTokenAccessor tokenAccessor;
+
   @Test
   public void createAccountRestApi() throws Exception {
     AccountInput input = new AccountInput();
@@ -34,6 +43,22 @@
   }
 
   @Test
+  public void createAccountWithTokenAsAdminSucceeds() throws Exception {
+    AccountInput input = new AccountInput();
+    input.username = "foo";
+    AuthTokenInput token = new AuthTokenInput();
+    token.id = "test";
+    token.token = "secret";
+    input.tokens = List.of(token);
+    RestResponse r = adminRestSession.put("/accounts/" + input.username, input);
+    r.assertCreated();
+
+    JsonObject json = JsonParser.parseReader(r.getReader()).getAsJsonObject();
+    assertThat(tokenAccessor.getToken(Account.id(json.get("_account_id").getAsInt()), token.id))
+        .isNotNull();
+  }
+
+  @Test
   @GerritConfig(name = "auth.userNameToLowerCase", value = "false")
   public void createAccountRestApiUserNameToLowerCaseFalse() throws Exception {
     AccountInput input = new AccountInput();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index e62d365..d9ba200 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -474,11 +474,6 @@
         createExternalIdWithDuplicateEmail("foo:bar"));
   }
 
-  @Test
-  public void pushToExternalIdsBranchRejectsBadPassword() throws Exception {
-    testPushToExternalIdsBranchRejectsInvalidExternalId(createExternalIdWithBadPassword("foo"));
-  }
-
   private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
       throws Exception {
     projectOperations
@@ -566,11 +561,8 @@
 
     // create valid external IDs
     insertExtId(
-        externalIdFactory.createWithPassword(
-            externalIdKeyFactory.parse(nextId(scheme, i)),
-            admin.id(),
-            "admin.other@example.com",
-            "secret-password"));
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse(nextId(scheme, i)), admin.id(), "admin.other@example.com"));
     insertExtId(externalIdFactory.createEmail(admin.id(), "admin.other@example.com"));
     insertExtId(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
   }
@@ -611,14 +603,6 @@
                 + extIdWithDuplicateEmail.email()
                 + "'"));
 
-    ExternalId extIdWithBadPassword = createExternalIdWithBadPassword("admin-username");
-    insertExtId(extIdWithBadPassword);
-    expectedProblems.add(
-        consistencyError(
-            "External ID '"
-                + extIdWithBadPassword.key().get()
-                + "' has an invalid password: unrecognized algorithm"));
-
     return expectedProblems;
   }
 
@@ -692,14 +676,6 @@
         externalIdKeyFactory.parse(externalId), user.id(), admin.email());
   }
 
-  private ExternalId createExternalIdWithBadPassword(String username) {
-    return externalIdFactory.create(
-        externalIdKeyFactory.create(SCHEME_USERNAME, username),
-        admin.id(),
-        null,
-        "non-hashed-password-is-not-allowed");
-  }
-
   private static String nextId(String scheme, MutableInteger i) {
     return scheme + ":foo" + ++i.value;
   }
@@ -764,6 +740,7 @@
   }
 
   @Test
+  @Deprecated
   public void unsetHttpPassword() throws Exception {
     ExternalId extId =
         externalIdFactory.createWithPassword(
@@ -805,8 +782,7 @@
 
     // update the first external ID
     ExternalId updatedExtId1 =
-        externalIdFactory.create(
-            extId1.key(), accountId, "foo.bar@example.com", /* hashedPassword= */ null);
+        externalIdFactory.createWithEmail(extId1.key(), accountId, "foo.bar@example.com");
     accountsUpdateProvider
         .get()
         .update("Update External ID", accountId, u -> u.updateExternalId(updatedExtId1));
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/HttpPasswordIT.java b/javatests/com/google/gerrit/acceptance/rest/account/HttpPasswordIT.java
new file mode 100644
index 0000000..db1b85e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/account/HttpPasswordIT.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.common.HttpPasswordInput;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.PasswordMigrator;
+import com.google.inject.Inject;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpPasswordIT extends AbstractDaemonTest {
+  @Inject AuthTokenAccessor tokenAccessor;
+
+  TestAccount testUser;
+  RestSession restSession;
+
+  @Before
+  public void setUp() throws Exception {
+    testUser = accountCreator.create(RandomStringUtils.randomAlphabetic(10));
+    restSession = server.createRestSession(testUser);
+  }
+
+  @Test
+  public void generateOwnPasswordSucceeds() throws Exception {
+    HttpPasswordInput passwordInput = new HttpPasswordInput();
+    passwordInput.generate = true;
+    RestResponse resp = restSession.put("/accounts/self/password.http", passwordInput);
+    resp.assertCreated();
+
+    String returnedToken =
+        Iterables.get(Splitter.onPattern(System.lineSeparator()).split(resp.getEntityContent()), 1);
+    assertThat(returnedToken.substring(1, returnedToken.length() - 1)).isNotEmpty();
+
+    assertThat(tokenAccessor.getToken(testUser.id(), PasswordMigrator.DEFAULT_ID)).isPresent();
+  }
+
+  @Test
+  public void createPasswordForOtherUserFailsForNonAdmins() throws Exception {
+    HttpPasswordInput passwordInput = new HttpPasswordInput();
+    passwordInput.generate = true;
+    restSession
+        .put(String.format("/accounts/%d/password.http", admin.id().get()), passwordInput)
+        .assertForbidden();
+  }
+
+  @Test
+  public void createPasswordForOtherUserSucceedsForAdmins() throws Exception {
+    HttpPasswordInput passwordInput = new HttpPasswordInput();
+    passwordInput.generate = true;
+    adminRestSession
+        .put(String.format("/accounts/%d/password.http", testUser.id().get()), passwordInput)
+        .assertCreated();
+  }
+
+  @Test
+  public void setSpecificTokenFailsForNonAdmins() throws Exception {
+    HttpPasswordInput passwordInput = new HttpPasswordInput();
+    passwordInput.httpPassword = "secret";
+    restSession
+        .put(String.format("/accounts/%d/password.http", testUser.id().get()), passwordInput)
+        .assertForbidden();
+  }
+
+  @Test
+  public void setSpecificTokenSucceedsForAdmins() throws Exception {
+    HttpPasswordInput passwordInput = new HttpPasswordInput();
+    passwordInput.httpPassword = "secret";
+    RestResponse resp =
+        adminRestSession.put(
+            String.format("/accounts/%d/password.http", testUser.id().get()), passwordInput);
+
+    resp.assertCreated();
+
+    String returnedToken =
+        Iterables.get(Splitter.onPattern(System.lineSeparator()).split(resp.getEntityContent()), 1);
+    assertThat(returnedToken.substring(1, returnedToken.length() - 1))
+        .isEqualTo(passwordInput.httpPassword);
+
+    assertThat(tokenAccessor.getToken(testUser.id(), PasswordMigrator.DEFAULT_ID)).isPresent();
+  }
+
+  @Test
+  public void deletePasswordSucceeds() throws Exception {
+    restSession
+        .delete(String.format("/accounts/%d/password.http", testUser.id().get()))
+        .assertNoContent();
+    assertThat(tokenAccessor.getToken(testUser.id(), PasswordMigrator.DEFAULT_ID)).isEmpty();
+  }
+
+  @Test
+  public void deleteTokenForOtherUserSucceedsForAdmins() throws Exception {
+    adminRestSession
+        .delete(String.format("/accounts/%d/password.http", testUser.id().get()))
+        .assertNoContent();
+    assertThat(tokenAccessor.getToken(testUser.id(), PasswordMigrator.DEFAULT_ID)).isEmpty();
+  }
+
+  @Test
+  public void deleteTokenForOtherUserFailsForNonAdmins() throws Exception {
+    restSession
+        .delete(String.format("/accounts/%d/tokens/adminToken1", admin.id().get()))
+        .assertForbidden();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 5aac80d..625d53b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -50,12 +50,10 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -428,33 +426,6 @@
   }
 
   @Test
-  public void voteOnBehalfOfWithRobotComment() throws Exception {
-    allowCodeReviewOnBehalfOf();
-    PushOneCommit.Result r = createChange();
-
-    ReviewInput in = new ReviewInput();
-    in.onBehalfOf = user.id().toString();
-    in.label("Code-Review", 1);
-    RobotCommentInput ci = new RobotCommentInput();
-    ci.robotId = "my-robot";
-    ci.robotRunId = "abcd1234";
-    ci.path = Patch.COMMIT_MSG;
-    ci.side = Side.REVISION;
-    ci.line = 1;
-    ci.message = "message";
-    in.robotComments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
-    gApi.changes().id(r.getChangeId()).current().review(in);
-
-    ChangeData cd = r.getChange();
-    RobotComment c = Iterables.getOnlyElement(commentsUtil.robotCommentsByChange(cd.notes()));
-    assertThat(c.message).isEqualTo(ci.message);
-    assertThat(c.robotId).isEqualTo(ci.robotId);
-    assertThat(c.robotRunId).isEqualTo(ci.robotRunId);
-    assertThat(c.author.getId()).isEqualTo(user.id());
-    assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
-  }
-
-  @Test
   public void voteOnBehalfOfCannotModifyDrafts() throws Exception {
     allowCodeReviewOnBehalfOf();
     PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/TokenIT.java b/javatests/com/google/gerrit/acceptance/rest/account/TokenIT.java
new file mode 100644
index 0000000..e9f5ff3
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/account/TokenIT.java
@@ -0,0 +1,319 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TokenIT extends AbstractDaemonTest {
+  @Inject AuthTokenAccessor tokenAccessor;
+  @Inject ExtensionRegistry extensionRegistry;
+
+  private AuthTokenInput authTokenInput;
+  private RefUpdateCounter refUpdateCounter;
+
+  private static class RefUpdateCounter implements GitReferenceUpdatedListener {
+    private final AtomicInteger counter = new AtomicInteger();
+    private final String projectName;
+    private final String refPrefix;
+
+    RefUpdateCounter(String projectName, String refPrefix) {
+      this.projectName = projectName;
+      this.refPrefix = refPrefix;
+    }
+
+    @Override
+    public void onGitReferenceUpdated(Event event) {
+      if (event.getProjectName().equals(projectName) && event.getRefName().startsWith(refPrefix)) {
+        counter.incrementAndGet();
+      }
+    }
+
+    public int getCount() {
+      return counter.get();
+    }
+
+    public void reset() {
+      counter.set(0);
+    }
+  }
+
+  @Before
+  public void setup() throws Exception {
+    addUserTokens();
+    addAdminTokens();
+
+    String id = "testToken";
+    authTokenInput = new AuthTokenInput();
+    authTokenInput.id = id;
+
+    refUpdateCounter = new RefUpdateCounter(allUsers.name(), RefNames.refsUsers(user.id()));
+  }
+
+  @Test
+  public void assertGenerateOwnTokenSucceeds() throws Exception {
+
+    assertRefUpdateEventFired(
+        () -> {
+          RestResponse resp =
+              userRestSession.put(
+                  String.format("/accounts/self/tokens/%s", authTokenInput.id), authTokenInput);
+          resp.assertCreated();
+
+          JsonObject createdToken = JsonParser.parseReader(resp.getReader()).getAsJsonObject();
+          assertThat(createdToken.get("id").getAsString()).isEqualTo(authTokenInput.id);
+          assertThat(createdToken.get("token").getAsString()).isNotNull();
+
+          assertThat(tokenAccessor.getToken(user.id(), authTokenInput.id)).isPresent();
+        });
+  }
+
+  @Test
+  public void assertCreateTokenForOtherUserFailsForNonAdmins() throws Exception {
+    assertNoRefUpdateEvents(
+        () ->
+            userRestSession
+                .put(
+                    String.format("/accounts/%d/tokens/%s", admin.id().get(), authTokenInput.id),
+                    authTokenInput)
+                .assertForbidden());
+  }
+
+  @Test
+  public void assertCreateTokenForOtherUserSucceedsForAdmins() throws Exception {
+    assertRefUpdateEventFired(
+        () ->
+            adminRestSession
+                .put(
+                    String.format("/accounts/%d/tokens/%s", user.id().get(), authTokenInput.id),
+                    authTokenInput)
+                .assertCreated());
+  }
+
+  @Test
+  public void assertSetSpecificTokenFailsForNonAdmins() throws Exception {
+    authTokenInput.token = "secret";
+    assertNoRefUpdateEvents(
+        () ->
+            userRestSession
+                .put(
+                    String.format("/accounts/%d/tokens/%s", user.id().get(), authTokenInput.id),
+                    authTokenInput)
+                .assertForbidden());
+  }
+
+  @Test
+  public void assertSetSpecificTokenSucceedsForAdmins() throws Exception {
+    assertRefUpdateEventFired(
+        () -> {
+          authTokenInput.token = "secret";
+          RestResponse resp =
+              adminRestSession.put(
+                  String.format("/accounts/%d/tokens/%s", user.id().get(), authTokenInput.id),
+                  authTokenInput);
+
+          resp.assertCreated();
+
+          JsonObject createdToken = JsonParser.parseReader(resp.getReader()).getAsJsonObject();
+          assertThat(createdToken.get("id").getAsString()).isEqualTo(authTokenInput.id);
+          assertThat(createdToken.get("token").getAsString()).isEqualTo(authTokenInput.token);
+
+          assertThat(tokenAccessor.getToken(user.id(), authTokenInput.id)).isPresent();
+        });
+  }
+
+  @Test
+  public void assertListTokensSucceeds() throws Exception {
+    RestResponse resp = userRestSession.get(String.format("/accounts/%d/tokens", user.id().get()));
+    resp.assertOK();
+
+    JsonArray json = JsonParser.parseReader(resp.getReader()).getAsJsonArray();
+    assertThat(json.size()).isEqualTo(1);
+    assertThat(json.get(0).getAsJsonObject().get("id").getAsString()).isEqualTo("userToken1");
+  }
+
+  @Test
+  public void assertListTokensForOtherUserSucceedsForAdmins() throws Exception {
+    adminRestSession.get(String.format("/accounts/%d/tokens", user.id().get())).assertOK();
+  }
+
+  @Test
+  public void assertListTokensForOtherUserFailsForNonAdmins() throws Exception {
+    userRestSession.get(String.format("/accounts/%d/tokens", admin.id().get())).assertForbidden();
+  }
+
+  @Test
+  public void assertDeleteTokenSucceeds() throws Exception {
+    assertRefUpdateEventFired(
+        () ->
+            userRestSession
+                .delete(String.format("/accounts/%d/tokens/userToken1", user.id().get()))
+                .assertNoContent());
+    assertThat(tokenAccessor.getToken(user.id(), "userToken1")).isEmpty();
+  }
+
+  @Test
+  public void assertDeleteTokenForOtherUserSucceedsForAdmins() throws Exception {
+    assertRefUpdateEventFired(
+        () ->
+            adminRestSession
+                .delete(String.format("/accounts/%d/tokens/userToken1", user.id().get()))
+                .assertNoContent());
+  }
+
+  @Test
+  public void assertDeleteTokenForOtherUserFailsForNonAdmins() throws Exception {
+    assertNoRefUpdateEvents(
+        () ->
+            userRestSession
+                .delete(String.format("/accounts/%d/tokens/adminToken1", admin.id().get()))
+                .assertForbidden());
+  }
+
+  @Test
+  public void assertCreateTokensWithLifetimeSucceeds() throws Exception {
+    assertRefUpdateEventsCount(
+        5,
+        () -> {
+          for (String lifetime : List.of("5min", "1h", "1d", "1mon", "3y")) {
+            authTokenInput.lifetime = lifetime;
+            authTokenInput.id = String.format("testToken_%s", lifetime);
+            RestResponse resp =
+                userRestSession.put(
+                    String.format("/accounts/self/tokens/%s", authTokenInput.id), authTokenInput);
+            resp.assertCreated();
+
+            JsonObject createdToken = JsonParser.parseReader(resp.getReader()).getAsJsonObject();
+            assertThat(createdToken.get("id").getAsString()).isEqualTo(authTokenInput.id);
+            assertThat(createdToken.get("token").getAsString()).isNotNull();
+            assertThat(
+                    TimeUnit.NANOSECONDS.toMinutes(
+                        Math.abs(
+                            Timestamp.valueOf(createdToken.get("expiration").getAsString())
+                                .toInstant()
+                                .compareTo(
+                                    Instant.now()
+                                        .plusSeconds(
+                                            ConfigUtil.getTimeUnit(
+                                                lifetime, 0, TimeUnit.SECONDS))))))
+                .isLessThan(1L);
+
+            assertThat(tokenAccessor.getToken(user.id(), authTokenInput.id)).isPresent();
+          }
+        });
+  }
+
+  @Test
+  public void assertInvalidLifetimeFormatReturnsBadRequest() throws Exception {
+    assertNoRefUpdateEvents(
+        () -> {
+          authTokenInput.lifetime = "1invalid";
+          RestResponse resp =
+              userRestSession.put(
+                  String.format("/accounts/self/tokens/%s", authTokenInput.id), authTokenInput);
+          resp.assertBadRequest();
+        });
+  }
+
+  @Test
+  @GerritConfig(name = "auth.maxAuthTokensPerAccount", value = "2")
+  public void assertCreatingMoreTokensThanAllowedFails() throws Exception {
+    assertRefUpdateEventsCount(
+        3,
+        () -> {
+          RestResponse resp =
+              userRestSession.put(
+                  String.format("/accounts/self/tokens/%s", authTokenInput.id), authTokenInput);
+          resp.assertCreated();
+
+          AuthTokenInput tokenInput2 = new AuthTokenInput();
+          tokenInput2.id = "testToken2";
+          resp =
+              userRestSession.put(
+                  String.format("/accounts/self/tokens/%s", tokenInput2.id), tokenInput2);
+          resp.assertBadRequest();
+
+          resp =
+              userRestSession.delete(String.format("/accounts/self/tokens/%s", authTokenInput.id));
+          resp.assertNoContent();
+
+          resp =
+              userRestSession.put(
+                  String.format("/accounts/self/tokens/%s", tokenInput2.id), tokenInput2);
+          resp.assertCreated();
+        });
+  }
+
+  private void addUserTokens() throws Exception {
+    @SuppressWarnings("unused")
+    var unused =
+        tokenAccessor.addPlainToken(user.id(), "userToken1", "http-pass", Optional.empty());
+  }
+
+  private void addAdminTokens() throws Exception {
+    @SuppressWarnings("unused")
+    var unused =
+        tokenAccessor.addPlainToken(admin.id(), "adminToken1", "http-pass", Optional.empty());
+  }
+
+  @MustBeClosed
+  private ExtensionRegistry.Registration registerRefUpdateCounter() {
+    return extensionRegistry.newRegistration().add(refUpdateCounter);
+  }
+
+  @FunctionalInterface
+  private interface CheckedVoidFunction {
+    void apply() throws Exception;
+  }
+
+  private void assertNoRefUpdateEvents(CheckedVoidFunction body) throws Exception {
+    assertRefUpdateEventsCount(0, body);
+  }
+
+  private void assertRefUpdateEventFired(CheckedVoidFunction body) throws Exception {
+    assertRefUpdateEventsCount(1, body);
+  }
+
+  private void assertRefUpdateEventsCount(int expectedEventsCount, CheckedVoidFunction body)
+      throws Exception {
+    try (ExtensionRegistry.Registration unused = registerRefUpdateCounter()) {
+      refUpdateCounter.reset();
+      body.apply();
+      assertThat(refUpdateCounter.getCount()).isEqualTo(expectedEventsCount);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 80c7055..b007b54 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.acceptance.rest.binding;
 
 import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
-import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
-import static java.util.stream.Collectors.toList;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
 
 import com.google.common.collect.ImmutableList;
@@ -28,17 +26,9 @@
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
-import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.extensions.common.FixReplacementInfo;
-import com.google.gerrit.extensions.common.FixSuggestionInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
 import org.junit.Test;
 
 /**
@@ -86,6 +76,7 @@
           RestCall.post("/changes/%s/private.delete"),
           RestCall.delete("/changes/%s/private"),
           RestCall.get("/changes/%s/pure_revert"),
+          RestCall.get("/changes/%s/query"),
           RestCall.post("/changes/%s/ready"),
           RestCall.post("/changes/%s/rebase"),
           RestCall.post("/changes/%s/rebase:chain"),
@@ -96,7 +87,9 @@
           RestCall.post("/changes/%s/reviewers"),
           // GET /changes/<change-id>/revisions is not implemented
           RestCall.builder(GET, "/changes/%s/revisions").expectedResponseCode(SC_NOT_FOUND).build(),
-          RestCall.get("/changes/%s/robotcomments"),
+          RestCall.builder(GET, "/changes/%s/robotcomments")
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
           RestCall.get("/changes/%s/topic"),
           RestCall.put("/changes/%s/topic"),
           RestCall.delete("/changes/%s/topic"),
@@ -164,7 +157,9 @@
           RestCall.get("/changes/%s/revisions/%s/review"),
           RestCall.post("/changes/%s/revisions/%s/review"),
           RestCall.get("/changes/%s/revisions/%s/reviewers"),
-          RestCall.get("/changes/%s/revisions/%s/robotcomments"),
+          RestCall.builder(GET, "/changes/%s/revisions/%s/robotcomments")
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
           RestCall.post("/changes/%s/revisions/%s/submit"),
           RestCall.get("/changes/%s/revisions/%s/submit_type"),
           RestCall.post("/changes/%s/revisions/%s/test.submit_rule"),
@@ -211,20 +206,6 @@
           RestCall.post("/changes/%s/revisions/%s/comments/%s/delete"));
 
   /**
-   * Robot comment REST endpoints to be tested, each URL contains placeholders for the change
-   * identifier, the revision identifier and the robot comment identifier.
-   */
-  private static final ImmutableList<RestCall> ROBOT_COMMENT_ENDPOINTS =
-      ImmutableList.of(RestCall.get("/changes/%s/revisions/%s/robotcomments/%s"));
-
-  /**
-   * Fix REST endpoints to be tested, each URL contains placeholders for the change identifier, the
-   * revision identifier and the fix identifier.
-   */
-  private static final ImmutableList<RestCall> FIX_ENDPOINTS =
-      ImmutableList.of(RestCall.post("/changes/%s/revisions/%s/fixes/%s/apply"));
-
-  /**
    * Revision file REST endpoints to be tested, each URL contains placeholders for the change
    * identifier, the revision identifier and the file identifier.
    */
@@ -385,69 +366,6 @@
   }
 
   @Test
-  public void robotCommentEndpoints() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    RobotCommentInput robotCommentInput = new RobotCommentInput();
-    robotCommentInput.robotId = "happyRobot";
-    robotCommentInput.robotRunId = "1";
-    robotCommentInput.line = 1;
-    robotCommentInput.message = "nit: trailing whitespace";
-    robotCommentInput.path = Patch.COMMIT_MSG;
-
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.robotComments =
-        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
-    reviewInput.message = "robot comment test";
-    gApi.changes().id(changeId).current().review(reviewInput);
-
-    List<RobotCommentInfo> robotCommentInfos =
-        gApi.changes().id(changeId).current().robotCommentsAsList();
-    RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
-
-    RestApiCallHelper.execute(
-        adminRestSession, ROBOT_COMMENT_ENDPOINTS, changeId, "current", robotCommentInfo.id);
-  }
-
-  @Test
-  public void fixEndpoints() throws Exception {
-    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
-
-    RobotCommentInput robotCommentInput = new RobotCommentInput();
-    robotCommentInput.robotId = "happyRobot";
-    robotCommentInput.robotRunId = "1";
-    robotCommentInput.line = 1;
-    robotCommentInput.message = "nit: trailing whitespace";
-    robotCommentInput.path = FILENAME;
-
-    FixReplacementInfo fixReplacementInfo = new FixReplacementInfo();
-    fixReplacementInfo.path = FILENAME;
-    fixReplacementInfo.replacement = "some replacement code";
-    fixReplacementInfo.range = createRange(1, 1, 1, 2);
-
-    FixSuggestionInfo fixSuggestionInfo = new FixSuggestionInfo();
-    fixSuggestionInfo.fixId = "An ID which must be overwritten.";
-    fixSuggestionInfo.description = "A description for a suggested fix.";
-    fixSuggestionInfo.replacements = ImmutableList.of(fixReplacementInfo);
-
-    robotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
-
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.robotComments =
-        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
-    reviewInput.message = "robot comment test";
-    gApi.changes().id(changeId).current().review(reviewInput);
-
-    List<RobotCommentInfo> robotCommentInfos =
-        gApi.changes().id(changeId).current().robotCommentsAsList();
-
-    List<String> fixIds = getFixIds(robotCommentInfos);
-    String fixId = Iterables.getOnlyElement(fixIds);
-
-    RestApiCallHelper.execute(adminRestSession, FIX_ENDPOINTS, changeId, "current", fixId);
-  }
-
-  @Test
   public void revisionFileEndpoints() throws Exception {
     String changeId = createChange("Subject", FILENAME, "content").getChangeId();
     RestApiCallHelper.execute(
@@ -495,24 +413,4 @@
     RestApiCallHelper.execute(
         adminRestSession, ATTENTION_SET_ENDPOINTS, changeId, user.id().toString());
   }
-
-  private static Comment.Range createRange(
-      int startLine, int startCharacter, int endLine, int endCharacter) {
-    Comment.Range range = new Comment.Range();
-    range.startLine = startLine;
-    range.startCharacter = startCharacter;
-    range.endLine = endLine;
-    range.endCharacter = endCharacter;
-    return range;
-  }
-
-  private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
-    assertThatList(robotComments).isNotNull();
-    return robotComments.stream()
-        .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
-        .filter(Objects::nonNull)
-        .flatMap(List::stream)
-        .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
-        .collect(toList());
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index 3630fef..09450d1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -55,6 +55,7 @@
           RestCall.get("/config/server/experiments"),
           RestCall.post("/config/server/index.changes"),
           RestCall.get("/config/server/info"),
+          RestCall.get("/config/server/labels"),
           RestCall.get("/config/server/preferences"),
           RestCall.put("/config/server/preferences"),
           RestCall.get("/config/server/preferences.diff"),
@@ -62,6 +63,7 @@
           RestCall.get("/config/server/preferences.edit"),
           RestCall.put("/config/server/preferences.edit"),
           RestCall.post("/config/server/reload"),
+          RestCall.get("/config/server/submit-requirements"),
           RestCall.get("/config/server/summary"),
           RestCall.post("/config/server/deactivate.stale.accounts"),
           RestCall.get("/config/server/tasks"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java
new file mode 100644
index 0000000..f5953d2
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/FlowRestApiBindingsIT.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.binding;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestExtensions;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.server.flow.Flow;
+import com.google.gerrit.server.flow.FlowAction;
+import com.google.gerrit.server.flow.FlowCreation;
+import com.google.gerrit.server.flow.FlowExpression;
+import com.google.gerrit.server.flow.FlowService;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the flow REST API.
+ *
+ * <p>These tests only verify that the flow REST endpoints are correctly bound, they do no test the
+ * functionality of the flow REST endpoints.
+ */
+public class FlowRestApiBindingsIT extends AbstractDaemonTest {
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  private static final ImmutableList<RestCall> CHANGE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/flows"),
+          RestCall.get("/changes/%s/is-flows-enabled"),
+          RestCall.post("/changes/%s/flows"));
+
+  private static final ImmutableList<RestCall> FLOW_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/flows/%s"),
+          // Deletion of flow must be tested last
+          RestCall.delete("/changes/%s/flows/%s"));
+
+  @Test
+  public void changeEndpoints() throws Exception {
+    try (Registration registration =
+        extensionRegistry.newRegistration().set(new TestExtensions.TestFlowService())) {
+      String changeId = createChange().getChangeId();
+      gApi.changes().id(changeId).edit().create();
+      RestApiCallHelper.execute(adminRestSession, CHANGE_ENDPOINTS, changeId);
+    }
+  }
+
+  @Test
+  public void flowEndpoints() throws Exception {
+    FlowService flowService = new TestExtensions.TestFlowService();
+    try (Registration registration = extensionRegistry.newRegistration().set(flowService)) {
+      PushOneCommit.Result r = createChange();
+      gApi.changes().id(r.getChangeId()).edit().create();
+      Flow flow =
+          flowService.createFlow(
+              FlowCreation.builder()
+                  .projectName(project)
+                  .changeId(r.getChange().getId())
+                  .ownerId(r.getChange().change().getOwner())
+                  .addStageExpression(
+                      FlowExpression.builder()
+                          .condition(
+                              String.format(
+                                  "com.google.gerrit[change:%s label:Verified+1]",
+                                  r.getChange().getId()))
+                          .action(
+                              FlowAction.builder()
+                                  .name("AddReviewer")
+                                  .addParameter(user.email())
+                                  .build())
+                          .build())
+                  .build());
+      RestApiCallHelper.execute(
+          adminRestSession, FLOW_ENDPOINTS, r.getChangeId(), flow.key().uuid());
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
index d2c1430..d195c7b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedChildRestApiBindingsIT.java
@@ -15,16 +15,12 @@
 package com.google.gerrit.acceptance.rest.binding;
 
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
-import static com.google.gerrit.server.change.RobotCommentResource.ROBOT_COMMENT_KIND;
 
 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.PushOneCommit;
 import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
 import com.google.gerrit.acceptance.rest.util.RestCall;
-import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -38,7 +34,6 @@
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.change.RobotCommentResource;
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -76,9 +71,6 @@
           RestCall.post("/changes/%s/revisions/%s/test-collection/"),
           RestCall.post("/changes/%s/revisions/%s/test-collection/1/update"));
 
-  private static final ImmutableSet<RestCall> ROBOTCOMMENT_TEST_CALLS =
-      ImmutableSet.of(RestCall.delete("/changes/%s/revisions/%s/robotcomments/%s"));
-
   /**
    * Module for all sys bindings.
    *
@@ -98,7 +90,6 @@
               postOnCollection(TEST_KIND).to(TestPostOnCollection.class);
               post(TEST_KIND, "update").to(TestPost.class);
               get(TEST_KIND, "detail").to(TestGet.class);
-              delete(ROBOT_COMMENT_KIND).to(TestDelete.class);
             }
           });
     }
@@ -158,14 +149,6 @@
     }
   }
 
-  @Singleton
-  static class TestDelete implements RestModifyView<RobotCommentResource, String> {
-    @Override
-    public Response<?> apply(RobotCommentResource resource, String input) throws Exception {
-      return Response.none();
-    }
-  }
-
   @Test
   public void testRevisionEndpoints() throws Exception {
     PatchSet.Id patchSetId = createChange().getPatchSetId();
@@ -177,27 +160,4 @@
           String.valueOf(patchSetId.get()));
     }
   }
-
-  @Test
-  public void testRobotCommentEndpoints() throws Exception {
-    PatchSet.Id patchSetId = createChange().getPatchSetId();
-    String robotCommentUuid = createRobotComment(patchSetId.changeId());
-    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, MyPluginSysModule.class, null, null)) {
-      RestApiCallHelper.execute(
-          adminRestSession,
-          ROBOTCOMMENT_TEST_CALLS.asList(),
-          String.valueOf(patchSetId.changeId().get()),
-          String.valueOf(patchSetId.get()),
-          robotCommentUuid);
-    }
-  }
-
-  private String createRobotComment(Change.Id changeId) throws Exception {
-    testCommentHelper.addRobotComment(
-        changeId, TestCommentHelper.createRobotCommentInput(PushOneCommit.FILE_NAME));
-    return Iterables.getOnlyElement(
-            Iterables.getOnlyElement(
-                gApi.changes().id(changeId.get()).current().robotComments().values()))
-        .id;
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 4cd8dba..a5e23e9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1085,6 +1085,8 @@
     // Delete the change from the index, to ensure the use of the backfill mechanism
     changeIndex.delete(change.getChange().getId());
 
+    testMetricMaker.reset();
+
     submit(id, new TestSubmitInput());
     assertMerged(id);
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index c34625e..298884c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -291,9 +291,12 @@
     submitWithConflict(
         change2.getChangeId(),
         String.format(
-            "Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.\n\n"
-                + "merge conflict(s):\n"
-                + "a.txt",
+            """
+            Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.
+
+            merge conflict(s):
+            * a.txt
+            """,
             change2.getCommit().name(), change2.getChange().getId()));
     RevCommit head = projectOperations.project(project).getHead("master");
     assertThat(head).isEqualTo(headAfterFirstSubmit);
@@ -419,9 +422,12 @@
     submitWithConflict(
         change2.getChangeId(),
         String.format(
-            "Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.\n\n"
-                + "merge conflict(s):\n"
-                + "fileName 2",
+            """
+            Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.
+
+            merge conflict(s):
+            * fileName 2
+            """,
             change2.getCommit().name(), change2.getChange().getId()));
     assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterChange1);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 0f245bd..a89e30a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -1388,25 +1388,6 @@
   }
 
   @Test
-  public void reviewIgnoresRobotCommentsForAttentionSet() throws Exception {
-    PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.id());
-    testCommentHelper.addRobotComment(
-        r.getChangeId(),
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(Patch.COMMIT_MSG));
-
-    requestScopeOperations.setApiUser(admin.id());
-    change(r)
-        .current()
-        .review(
-            reviewInReplyToComment(
-                Iterables.getOnlyElement(
-                        gApi.changes().id(r.getChangeId()).current().robotCommentsAsList())
-                    .id));
-    assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
-  }
-
-  @Test
   public void ownerReplyResolvedAddsNonVotedInCommentThread() throws Exception {
     PushOneCommit.Result r = createChange();
     requestScopeOperations.setApiUser(user.id());
@@ -1554,47 +1535,9 @@
   }
 
   @Test
-  public void reviewAddsAllUsersInCommentThreadWhenOriginalCommentIsARobotComment()
-      throws Exception {
-    PushOneCommit.Result result = createChange();
-    testCommentHelper.addRobotComment(
-        result.getChangeId(),
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(Patch.COMMIT_MSG));
-
-    requestScopeOperations.setApiUser(user.id());
-    // Reply to the robot comment.
-    change(result)
-        .current()
-        .review(
-            reviewInReplyToComment(
-                Iterables.getOnlyElement(
-                        gApi.changes().id(result.getChangeId()).current().robotCommentsAsList())
-                    .id));
-
-    requestScopeOperations.setApiUser(admin.id());
-    // Reply to the human comment. which was a reply to the robot comment.
-    change(result)
-        .current()
-        .review(
-            reviewInReplyToComment(
-                Iterables.getOnlyElement(
-                        gApi.changes().id(result.getChangeId()).current().commentsAsList())
-                    .id));
-
-    // The user which replied to the robot comment was added to the attention set.
-    AttentionSetUpdate attentionSet =
-        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(result, user));
-    assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
-    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
-    assertThat(attentionSet)
-        .hasReasonThat()
-        .isEqualTo("Someone else replied on a comment you posted");
-  }
-
-  @Test
   public void reviewAddsAllUsersInCommentThreadEvenOfDifferentChildBranch() throws Exception {
     Account.Id changeOwner = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
+    Change.Id changeId = changeOperations.newChange().owner(changeOwner).createV1();
     Account.Id user1 = accountOperations.newAccount().create();
     Account.Id user2 = accountOperations.newAccount().create();
     Account.Id user3 = accountOperations.newAccount().create();
@@ -2072,40 +2015,6 @@
   }
 
   @Test
-  public void robotCommentDoesNotAddOwnerOnClosedChanges() throws Exception {
-    TestAccount robot =
-        accountCreator.create(
-            "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS);
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).abandon();
-
-    requestScopeOperations.setApiUser(robot.id());
-    ReviewInput reviewInput = new ReviewInput();
-    ReviewInput.RobotCommentInput robotCommentInput =
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields("a.txt");
-    reviewInput.robotComments = ImmutableMap.of("a.txt", ImmutableList.of(robotCommentInput));
-    change(r).current().review(reviewInput);
-
-    assertThat(r.getChange().attentionSet()).isEmpty();
-  }
-
-  @Test
-  public void robotCanChangeAttentionSetExplicitly() throws Exception {
-    TestAccount robot =
-        accountCreator.create(
-            "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS);
-    PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(robot.id());
-    change(r).current().review(new ReviewInput().addUserToAttentionSet(admin.email(), "reason"));
-
-    AttentionSetUpdate attentionSet =
-        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
-    assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
-    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
-    assertThat(attentionSet).hasReasonThat().isEqualTo("reason");
-  }
-
-  @Test
   public void addUsersToAttentionSetInPrivateChanges() throws Exception {
     PushOneCommit.Result r = createChange();
     change(r).setPrivate(true);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/BUILD b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
index 1eddcce..959b016 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/change/BUILD
@@ -16,6 +16,7 @@
     labels = ["rest"],
     deps = [
         ":submit_util",
+        "//java/com/google/gerrit/entities/converter:converters",
         "//java/com/google/gerrit/mail",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeEditIT.java
index c33de03..5c89847 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeEditIT.java
@@ -35,7 +35,7 @@
 
   @Test
   public void modifyMultipleFilesInOneChangeEdit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     RestResponse response =
         adminRestSession.putRaw(
             String.format("/changes/%s/edit/%s", changeId, FILE_NAME),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index ca3a345..51ca30a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -26,35 +27,37 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewerResult;
-import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import java.lang.reflect.Type;
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 
 public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
+  @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Before
   public void setUp() throws Exception {
-    ConfigInput conf = new ConfigInput();
-    conf.enableReviewerByEmail = InheritableBoolean.TRUE;
-    gApi.projects().name(project.get()).config(conf);
+    projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
   }
 
   @Test
@@ -350,9 +353,11 @@
 
   @Test
   public void rejectWhenFeatureIsDisabled() throws Exception {
-    ConfigInput conf = new ConfigInput();
-    conf.enableReviewerByEmail = InheritableBoolean.FALSE;
-    gApi.projects().name(project.get()).config(conf);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .enableReviewerByEmail(InheritableBoolean.FALSE)
+        .update();
 
     PushOneCommit.Result r = createChange();
 
@@ -421,6 +426,35 @@
     assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
   }
 
+  @Test
+  public void removeCcByEmailWithoutPermissionFails() throws Exception {
+    PushOneCommit.Result r = createChange();
+    TestAccount newUser = createAccounts(1, name("foo")).get(0);
+
+    ReviewerInput input = new ReviewerInput();
+    input.reviewer = "nonexisting@example.com";
+    input.state = ReviewerState.CC;
+    gApi.changes().id(r.getChangeId()).addReviewer(input);
+    requestScopeOperations.setApiUser(newUser.id());
+    assertThat(gApi.changes().id(r.getChangeId()).get().removableReviewers).isEmpty();
+
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> gApi.changes().id(r.getChangeId()).reviewer(input.reviewer).remove());
+    assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
+  }
+
+  private List<TestAccount> createAccounts(int n, String emailPrefix) throws Exception {
+    List<TestAccount> result = new ArrayList<>(n);
+    for (int i = 0; i < n; i++) {
+      result.add(
+          accountCreator.create(
+              name("u" + i), emailPrefix + "-" + i + "@example.com", "Full Name " + i, null));
+    }
+    return result;
+  }
+
   private static String toRfcAddressString(AccountInfo info) {
     return Address.create(info.name, info.email).toString();
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 9b5a3c1..81e5593 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.TestExtensions.TestCommitValidationInfoListener;
 import static com.google.gerrit.acceptance.TestExtensions.TestCommitValidationListener;
 import static com.google.gerrit.acceptance.TestExtensions.TestValidationOptionsListener;
@@ -28,7 +30,6 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.common.testing.GitPersonSubject.assertThat;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
@@ -55,6 +56,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.entities.converter.ChangeInputProtoConverter;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
@@ -65,7 +67,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -77,6 +78,7 @@
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -102,15 +104,20 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.eclipse.jgit.util.Base64;
 import org.junit.Before;
 import org.junit.Test;
@@ -227,8 +234,12 @@
         gApi.changes().id(info.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
     assertThat(currentRevision.conflicts).isNotNull();
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
+    assertThat(currentRevision.conflicts.base).isNull();
     assertThat(currentRevision.conflicts.ours).isNull();
     assertThat(currentRevision.conflicts.theirs).isNull();
+    assertThat(currentRevision.conflicts.mergeStrategy).isNull();
+    assertThat(currentRevision.conflicts.noBaseReason)
+        .isEqualTo(NoMergeBaseReason.NO_MERGE_PERFORMED);
   }
 
   @Test
@@ -418,6 +429,31 @@
   }
 
   @Test
+  public void createNewChange_projectConfigRequiresSignedOffByFooter() throws Exception {
+    projectOperations.project(project).forUpdate().useSignedOffBy().update();
+
+    ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+    String message = info.revisions.get(info.currentRevision).commit.message;
+    assertThat(message)
+        .contains(
+            String.format(
+                "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.newIdent().getEmailAddress()));
+  }
+
+  @Test
+  public void createNewChange_projectConfigDoesNotRequireSignedOffByFooter() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .useSignedOffBy(InheritableBoolean.FALSE)
+        .update();
+
+    ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+    String message = info.revisions.get(info.currentRevision).commit.message;
+    assertThat(message).doesNotContain(SIGNED_OFF_BY_TAG);
+  }
+
+  @Test
   public void createNewChangeSignedOffByFooterWithChangeId() throws Exception {
     setSignedOffByFooter(true);
     try {
@@ -561,9 +597,11 @@
 
   @Test
   public void createAuthorNotAddedAsCcWithAvoidAddingOriginalAuthorAsReviewer() throws Exception {
-    ConfigInput config = new ConfigInput();
-    config.skipAddingAuthorAndCommitterAsReviewers = InheritableBoolean.TRUE;
-    gApi.projects().name(project.get()).config(config);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .skipAddingAuthorAndCommitterAsReviewers()
+        .update();
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.author = new AccountInput();
     input.author.email = user.email();
@@ -693,16 +731,41 @@
   }
 
   @Test
-  public void createMergeChange() throws Exception {
+  public void createMergeChangeNoConflictsUsingResolveStrategy() throws Exception {
+    testCreateMergeChangeNoConflicts("resolve");
+  }
+
+  @Test
+  public void createMergeChangeNoConflictsUsingRecursiveStrategy() throws Exception {
+    testCreateMergeChangeNoConflicts("recursive");
+  }
+
+  @Test
+  public void createMergeChangeNoConflictsUsingSimpleTwoWayInCoreStrategy() throws Exception {
+    testCreateMergeChangeNoConflicts("simple-two-way-in-core");
+  }
+
+  @Test
+  public void createMergeChangeNoConflictsUsingOursStrategy() throws Exception {
+    testCreateMergeChangeNoConflicts("ours");
+  }
+
+  @Test
+  public void createMergeChangeNoConflictsUsingTheirsStrategy() throws Exception {
+    testCreateMergeChangeNoConflicts("theirs");
+  }
+
+  public void testCreateMergeChangeNoConflicts(String mergeStrategy) throws Exception {
     String sourceBranch = "sourceBranch";
     String targetBranch = "targetBranch";
     ImmutableMap<String, Result> results =
         changeInTwoBranches(sourceBranch, "a.txt", targetBranch, "b.txt");
-    ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, "");
+    RevCommit baseCommit = results.get("master").getCommit();
+    ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, mergeStrategy);
     ChangeInfo change = assertCreateSucceeds(in);
 
     // Verify the message that has been posted on the change.
-    List<ChangeMessageInfo> messages = gApi.changes().id(change._number).messages();
+    List<ChangeMessageInfo> messages = gApi.changes().id(project.get(), change._number).messages();
     assertThat(messages).hasSize(1);
     assertThat(Iterables.getOnlyElement(messages).message).isEqualTo("Uploaded patch set 1.");
 
@@ -716,7 +779,17 @@
         .isEqualTo(results.get(targetBranch).getCommit().name());
     assertThat(currentRevision.conflicts.theirs)
         .isEqualTo(results.get(sourceBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(mergeStrategy);
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
+
+    if ("ours".equals(mergeStrategy) || "theirs".equals(mergeStrategy)) {
+      assertThat(currentRevision.conflicts.base).isNull();
+      assertThat(currentRevision.conflicts.noBaseReason)
+          .isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
+    } else {
+      assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
+      assertThat(currentRevision.conflicts.noBaseReason).isNull();
+    }
   }
 
   @Test
@@ -736,19 +809,54 @@
   }
 
   @Test
-  public void createMergeChange_Conflicts() throws Exception {
-    changeInTwoBranches("branchA", "shared.txt", "branchB", "shared.txt");
-    ChangeInput in = newMergeChangeInput("branchA", "branchB", "");
-    assertCreateFails(in, RestApiException.class, "merge conflict");
+  public void createMergeChangeFailsDueToConflictsUsingResolveStrategy() throws Exception {
+    testCreateMergeChangeFailsDueToConflicts("resolve");
   }
 
   @Test
-  public void createMergeChange_Conflicts_Ours() throws Exception {
+  public void createMergeChangeFailsDueToConflictsUsingRecursiveStrategy() throws Exception {
+    testCreateMergeChangeFailsDueToConflicts("recursive");
+  }
+
+  @Test
+  public void createMergeChangeFailsDueToConflictsUsingSimpleTwoWayInCoreStrategy()
+      throws Exception {
+    testCreateMergeChangeFailsDueToConflicts("simple-two-way-in-core");
+  }
+
+  private void testCreateMergeChangeFailsDueToConflicts(String mergeStrategy) throws Exception {
+    String fileName = "shared.txt";
+    changeInTwoBranches("branchA", fileName, "branchB", fileName);
+    ChangeInput in = newMergeChangeInput("branchA", "branchB", mergeStrategy);
+    assertCreateFails(
+        in,
+        RestApiException.class,
+        "simple-two-way-in-core".equals(mergeStrategy)
+            ? "merge conflict(s)"
+            : String.format(
+                """
+                merge conflict(s):
+                * %s
+                """,
+                fileName));
+  }
+
+  @Test
+  public void createMergeChangeSucceedsWithConflictsUsingOursStrategy() throws Exception {
+    testCreateMergeChangeSucceedsWithConflicts("ours");
+  }
+
+  @Test
+  public void createMergeChangeSucceedsWithConflictsUsingTheirsStrategy() throws Exception {
+    testCreateMergeChangeSucceedsWithConflicts("theirs");
+  }
+
+  private void testCreateMergeChangeSucceedsWithConflicts(String mergeStrategy) throws Exception {
     String sourceBranch = "sourceBranch";
     String targetBranch = "targetBranch";
     ImmutableMap<String, Result> results =
         changeInTwoBranches(sourceBranch, "shared.txt", targetBranch, "shared.txt");
-    ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, "ours");
+    ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, mergeStrategy);
     ChangeInfo change = assertCreateSucceeds(in);
 
     // Verify the conflicts information
@@ -757,15 +865,79 @@
     assertThat(currentRevision.commit.parents.get(0).commit)
         .isEqualTo(results.get(targetBranch).getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isNull();
     assertThat(currentRevision.conflicts.ours)
         .isEqualTo(results.get(targetBranch).getCommit().name());
     assertThat(currentRevision.conflicts.theirs)
         .isEqualTo(results.get(sourceBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(mergeStrategy);
+    assertThat(currentRevision.conflicts.noBaseReason)
+        .isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
     assertThat(currentRevision.conflicts.containsConflicts).isFalse();
   }
 
   @Test
-  public void createMergeChange_ConflictsAllowed() throws Exception {
+  public void createMergeChangeWithConflictsAllowedUsingRecursiveStrategy() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "recursive", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void createMergeChangeWithConflictsAllowedUsingRecursivetrategyAndDiff3()
+      throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "recursive", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void createMergeChangeWithConflictsAllowedUsingResolveStrategy() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "resolve", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void createMergeChangeWithConflictsAllowedUsingResolveStrategyAndDiff3() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "resolve", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void createMergeChangeWithConflictsAllowedUsingSimpleTwoWayInCoreStrategy()
+      throws Exception {
+    testCreateMergeChangeConflictsAllowed(
+        /* strategy= */ "simple-two-way-in-core", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void createMergeChangeWithConflictsAllowedUsingSimpleTwoWayInCoreStrategyAndDiff3()
+      throws Exception {
+    testCreateMergeChangeConflictsAllowed(
+        /* strategy= */ "simple-two-way-in-core", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void createMergeChangeWithConflictsAllowedUsingOursStrategy() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "ours", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void createMergeChangeWithConflictsAllowedUsingOursStrategyAndDiff3() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "ours", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void createMergeChangeWithConflictsAllowedUsingTheirsStrategy() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "theirs", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void createMergeChangeWithConflictsAllowedUsingTheirsStrategyAndDiff3() throws Exception {
+    testCreateMergeChangeConflictsAllowed(/* strategy= */ "theirs", /* useDiff3= */ true);
+  }
+
+  private void testCreateMergeChangeConflictsAllowed(String strategy, boolean useDiff3)
+      throws Exception {
     String fileName = "shared.txt";
     String sourceBranch = "sourceBranch";
     String sourceSubject = "source change";
@@ -773,6 +945,7 @@
     String targetBranch = "targetBranch";
     String targetSubject = "target change";
     String targetContent = "target content";
+
     ImmutableMap<String, Result> results =
         changeInTwoBranches(
             sourceBranch,
@@ -783,8 +956,49 @@
             targetSubject,
             fileName,
             targetContent);
+    RevCommit baseCommit = results.get("master").getCommit();
     ChangeInput in =
-        newMergeChangeInput(targetBranch, sourceBranch, "", /* allowConflicts= */ true);
+        newMergeChangeInput(targetBranch, sourceBranch, strategy, /* allowConflicts= */ true);
+
+    if ("ours".equals(strategy) || "theirs".equals(strategy)) {
+      ChangeInfo change = assertCreateSucceeds(in);
+
+      // Verify the conflicts information
+      RevisionInfo currentRevision =
+          gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+      assertThat(currentRevision.commit.parents.get(0).commit)
+          .isEqualTo(results.get(targetBranch).getCommit().name());
+      assertThat(currentRevision.conflicts).isNotNull();
+      assertThat(currentRevision.conflicts.base).isNull();
+      assertThat(currentRevision.conflicts.ours)
+          .isEqualTo(results.get(targetBranch).getCommit().name());
+      assertThat(currentRevision.conflicts.theirs)
+          .isEqualTo(results.get(sourceBranch).getCommit().name());
+      assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+      assertThat(currentRevision.conflicts.noBaseReason)
+          .isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
+      assertThat(currentRevision.conflicts.containsConflicts).isFalse();
+
+      // Verify that the file content in the created change is correct.
+      // We expect that it has conflict markers to indicate the conflict.
+      BinaryResult bin =
+          gApi.changes().id(project.get(), change._number).current().file(fileName).content();
+      ByteArrayOutputStream os = new ByteArrayOutputStream();
+      bin.writeTo(os);
+      String fileContent = new String(os.toByteArray(), UTF_8);
+      assertThat(fileContent).isEqualTo("ours".equals(strategy) ? targetContent : sourceContent);
+
+      return;
+    }
+
+    if ("simple-two-way-in-core".equals(strategy)) {
+      assertCreateFails(
+          in,
+          BadRequestException.class,
+          "merge with conflicts is not supported with merge strategy: simple-two-way-in-core");
+      return;
+    }
+
     ChangeInfo change = assertCreateSucceedsWithConflicts(in);
 
     // Verify the conflicts information
@@ -793,37 +1007,43 @@
     assertThat(currentRevision.commit.parents.get(0).commit)
         .isEqualTo(results.get(targetBranch).getCommit().name());
     assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
     assertThat(currentRevision.conflicts.ours)
         .isEqualTo(results.get(targetBranch).getCommit().name());
     assertThat(currentRevision.conflicts.theirs)
         .isEqualTo(results.get(sourceBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
     assertThat(currentRevision.conflicts.containsConflicts).isTrue();
 
     // Verify that the file content in the created change is correct.
     // We expect that it has conflict markers to indicate the conflict.
-    BinaryResult bin = gApi.changes().id(change._number).current().file(fileName).content();
+    BinaryResult bin =
+        gApi.changes().id(project.get(), change._number).current().file(fileName).content();
     ByteArrayOutputStream os = new ByteArrayOutputStream();
     bin.writeTo(os);
     String fileContent = new String(os.toByteArray(), UTF_8);
-    String sourceSha1 = abbreviateName(projectOperations.project(project).getHead(sourceBranch), 6);
-    String targetSha1 = abbreviateName(projectOperations.project(project).getHead(targetBranch), 6);
     assertThat(fileContent)
         .isEqualTo(
-            "<<<<<<< TARGET BRANCH ("
-                + targetSha1
-                + " "
-                + targetSubject
-                + ")\n"
-                + targetContent
-                + "\n"
-                + "=======\n"
-                + sourceContent
-                + "\n"
-                + ">>>>>>> SOURCE BRANCH ("
-                + sourceSha1
-                + " "
-                + sourceSubject
-                + ")\n");
+            String.format(
+                """
+                <<<<<<< TARGET BRANCH (%s %s)
+                %s
+                %s=======
+                %s
+                >>>>>>> SOURCE BRANCH (%s %s)
+                """,
+                projectOperations.project(project).getHead(targetBranch).getName(),
+                targetSubject,
+                targetContent,
+                (useDiff3
+                    ? String.format(
+                        "||||||| BASE          (%s %s)\n",
+                        baseCommit.getName(), baseCommit.getShortMessage())
+                    : ""),
+                sourceContent,
+                projectOperations.project(project).getHead(sourceBranch).getName(),
+                sourceSubject));
 
     // Verify the message that has been posted on the change.
     List<ChangeMessageInfo> messages = gApi.changes().id(change._number).messages();
@@ -838,26 +1058,654 @@
   }
 
   @Test
-  public void createMergeChange_ConflictAllowedNotSupportedByMergeStrategy() throws Exception {
+  public void createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingRecursiveStrategy()
+      throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "recursive", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingRecursiveStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "recursive", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingResolveStrategy()
+      throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "resolve", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingResolveStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "resolve", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingSimpleTwoWayInCoreStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "simple-two-way-in-core", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingSimpleTwoWayInCoreStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "simple-two-way-in-core", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingOursStrategy()
+      throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "ours", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingOursStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "ours", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingTheirsStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "theirs", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoInitialCommitsWithConflictsAllowedUsingUsingTheirsStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+        /* strategy= */ "theirs", /* useDiff3= */ true);
+  }
+
+  private void testCreateMergeChangeBetweenTwoInitialCommitsConflictsAllowed(
+      String strategy, boolean useDiff3) throws Exception {
     String fileName = "shared.txt";
     String sourceBranch = "sourceBranch";
+    String sourceSubject = "source change";
+    String sourceContent = "source content";
     String targetBranch = "targetBranch";
-    changeInTwoBranches(
-        sourceBranch,
-        "source change",
-        fileName,
-        "source content",
-        targetBranch,
-        "target change",
-        fileName,
-        "target content");
-    String mergeStrategy = "simple-two-way-in-core";
+    String targetSubject = "target change";
+    String targetContent = "target content";
+
+    Project.NameKey projectWithoutInitialCommit =
+        projectOperations.newProject().createEmptyCommit(false).create();
+
+    // Create sourceBranch with an initial commit.
+    TestRepository<InMemoryRepository> testRepo =
+        cloneProject(projectWithoutInitialCommit, getCloneAsAccount(configRule.description()));
+    RevCommit initialCommitSource =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message(sourceSubject)
+                .insertChangeId()
+                .add(fileName, sourceContent)
+                .create());
+    testRepo.reset(initialCommitSource);
+    PushResult r = pushHead(testRepo, "refs/heads/" + sourceBranch);
+    assertThat(r.getRemoteUpdate("refs/heads/" + sourceBranch).getStatus()).isEqualTo(Status.OK);
+
+    // Create targetBranch with another initial commit.
+    RevCommit initialCommitTarget =
+        testRepo.parseBody(
+            testRepo
+                .commit()
+                .message(targetSubject)
+                .insertChangeId()
+                .add(fileName, targetContent)
+                .create());
+    testRepo.reset(initialCommitTarget);
+    r = pushHead(testRepo, "refs/heads/" + targetBranch);
+    assertThat(r.getRemoteUpdate("refs/heads/" + targetBranch).getStatus()).isEqualTo(Status.OK);
+
+    // Merge sourceBranch into targetBranch with conflicts allowed.
     ChangeInput in =
-        newMergeChangeInput(targetBranch, sourceBranch, mergeStrategy, /* allowConflicts= */ true);
-    assertCreateFails(
-        in,
-        BadRequestException.class,
-        "merge with conflicts is not supported with merge strategy: " + mergeStrategy);
+        newMergeChangeInput(
+            projectWithoutInitialCommit,
+            targetBranch,
+            sourceBranch,
+            /* strategy= */ strategy,
+            /* allowConflicts= */ true);
+
+    if ("ours".equals(strategy) || "theirs".equals(strategy)) {
+      ChangeInfo change = assertCreateSucceeds(in);
+
+      // Verify the conflicts information.
+      RevisionInfo currentRevision =
+          gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+      assertThat(currentRevision.commit.parents.get(0).commit)
+          .isEqualTo(initialCommitTarget.name());
+      assertThat(currentRevision.conflicts).isNotNull();
+      assertThat(currentRevision.conflicts.base).isNull();
+      assertThat(currentRevision.conflicts.ours).isEqualTo(initialCommitTarget.name());
+      assertThat(currentRevision.conflicts.theirs).isEqualTo(initialCommitSource.name());
+      assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+      assertThat(currentRevision.conflicts.noBaseReason)
+          .isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
+      assertThat(currentRevision.conflicts.containsConflicts).isFalse();
+
+      // Verify that the file content in the created change is correct.
+      // We expect that it has conflict markers to indicate the conflict.
+      BinaryResult bin =
+          gApi.changes()
+              .id(projectWithoutInitialCommit.get(), change._number)
+              .current()
+              .file(fileName)
+              .content();
+      ByteArrayOutputStream os = new ByteArrayOutputStream();
+      bin.writeTo(os);
+      String fileContent = new String(os.toByteArray(), UTF_8);
+      assertThat(fileContent).isEqualTo("ours".equals(strategy) ? targetContent : sourceContent);
+
+      return;
+    }
+
+    if ("simple-two-way-in-core".equals(strategy)) {
+      assertCreateFails(
+          in,
+          BadRequestException.class,
+          "merge with conflicts is not supported with merge strategy: simple-two-way-in-core");
+      return;
+    }
+
+    ChangeInfo change = assertCreateSucceedsWithConflicts(in);
+
+    // Verify the conflicts information.
+    RevisionInfo currentRevision =
+        gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+    assertThat(currentRevision.commit.parents.get(0).commit).isEqualTo(initialCommitTarget.name());
+    assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isNull();
+    assertThat(currentRevision.conflicts.ours).isEqualTo(initialCommitTarget.name());
+    assertThat(currentRevision.conflicts.theirs).isEqualTo(initialCommitSource.name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+    assertThat(currentRevision.conflicts.noBaseReason)
+        .isEqualTo(NoMergeBaseReason.NO_COMMON_ANCESTOR);
+    assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+
+    // Verify that the file content in the created change is correct.
+    // We expect that it has conflict markers to indicate the conflict.
+    BinaryResult bin =
+        gApi.changes()
+            .id(projectWithoutInitialCommit.get(), change._number)
+            .current()
+            .file(fileName)
+            .content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String fileContent = new String(os.toByteArray(), UTF_8);
+    assertThat(fileContent)
+        .isEqualTo(
+            String.format(
+                """
+                <<<<<<< TARGET BRANCH (%s %s)
+                %s
+                %s=======
+                %s
+                >>>>>>> SOURCE BRANCH (%s %s)
+                """,
+                projectOperations
+                    .project(projectWithoutInitialCommit)
+                    .getHead(targetBranch)
+                    .getName(),
+                targetSubject,
+                targetContent,
+                (useDiff3 ? "||||||| BASE          (no common ancestor)\n" : ""),
+                sourceContent,
+                projectOperations
+                    .project(projectWithoutInitialCommit)
+                    .getHead(sourceBranch)
+                    .getName(),
+                sourceSubject));
+
+    // Verify the message that has been posted on the change.
+    List<ChangeMessageInfo> messages =
+        gApi.changes().id(projectWithoutInitialCommit.get(), change._number).messages();
+    assertThat(messages).hasSize(1);
+    assertThat(Iterables.getOnlyElement(messages).message)
+        .isEqualTo(
+            String.format(
+                """
+                Uploaded patch set 1.
+
+                The following files contain Git conflicts:
+                * %s
+                """,
+                fileName));
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingRecursiveStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "recursive", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingRecursiveStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "recursive", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingResolveStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "resolve", /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingResolveStrategyAndDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "resolve", /* useDiff3= */ true);
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingOursStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "ours", /* useDiff3= */ false);
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingTheirsStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "theirs", /* useDiff3= */ false);
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesWithConflictsAllowedUsingSimpleTwoWayInCoreStrategy()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+        /* strategy= */ "simple-two-way-in-core", /* useDiff3= */ false);
+  }
+
+  private void testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleMergeBasesConflictsAllowed(
+      String strategy, boolean useDiff3) throws Exception {
+    String sourceBranch = "sourceBranch";
+    String targetBranch = "targetBranch";
+
+    // Create source and target branch with non-conflicting commits.
+    // Later these commits will become the base commits for the criss-cross-merge.
+    ImmutableMap<String, Result> results =
+        changeInTwoBranches(
+            sourceBranch,
+            "base 1",
+            "a.txt",
+            "a content",
+            targetBranch,
+            "base 2",
+            "b.txt",
+            "b content");
+    RevCommit baseCommitInSource = results.get(sourceBranch).getCommit();
+    RevCommit baseCommitInTarget = results.get(targetBranch).getCommit();
+
+    // Create merge commits in both branches (1. merge the target branch into the source branch, 2.
+    // merge the source branch into the target branch).
+    PushOneCommit mergeCommitInSource = pushFactory.create(user.newIdent(), testRepo);
+    mergeCommitInSource.setParents(ImmutableList.of(baseCommitInSource, baseCommitInTarget));
+    mergeCommitInSource.to("refs/heads/" + sourceBranch).assertOkStatus();
+    PushOneCommit mergeCommitInTarget = pushFactory.create(user.newIdent(), testRepo);
+    mergeCommitInTarget.setParents(ImmutableList.of(baseCommitInTarget, baseCommitInSource));
+    mergeCommitInTarget.to("refs/heads/" + targetBranch).assertOkStatus();
+
+    // Create conflicting commits in both branches.
+    String fileName = "shared.txt";
+    String sourceSubject = "source change";
+    String sourceContent = "source content";
+    String targetSubject = "target change";
+    String targetContent = "target content";
+    PushOneCommit pushConflictingCommitInSource =
+        pushFactory.create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent);
+    pushConflictingCommitInSource.setParent(
+        projectOperations.project(project).getHead(sourceBranch));
+    PushOneCommit.Result pushConflictingCommitInSourceResult =
+        pushConflictingCommitInSource.to("refs/heads/" + sourceBranch);
+    pushConflictingCommitInSourceResult.assertOkStatus();
+    PushOneCommit pushConflictingCommitInTarget =
+        pushFactory.create(user.newIdent(), testRepo, targetSubject, fileName, targetContent);
+    pushConflictingCommitInTarget.setParent(
+        projectOperations.project(project).getHead(targetBranch));
+    PushOneCommit.Result pushConflictingCommitInTargetResult =
+        pushConflictingCommitInTarget.to("refs/heads/" + targetBranch);
+    pushConflictingCommitInTargetResult.assertOkStatus();
+
+    // Merge the source branch into the target with conflicts allowed. This is a criss-cross-merge:
+    //
+    //                   (criss-cross-merge)
+    //                     /             \
+    // (conflictingCommitInSource) (conflictingCommitInTarget)
+    //                    |               |
+    //       (mergeCommitInSource) (mergeCommitInTarget)
+    //                    |       X       |
+    //       (baseCommitInSource)  (baseCommitInTarget)
+    //                     \           /
+    //                    (initialCommit)
+    ChangeInput mergeInput =
+        newMergeChangeInput(targetBranch, sourceBranch, strategy, /* allowConflicts= */ true);
+
+    // The resolve/simple-two-way-in-core strategy doesn't support criss-cross-merges.
+    if ("resolve".equals(strategy) || "simple-two-way-in-core".equals(strategy)) {
+      assertCreateFails(
+          mergeInput,
+          ResourceConflictException.class,
+          "Cannot create merge commit: No merge base could be determined."
+              + " Reason=MULTIPLE_MERGE_BASES_NOT_SUPPORTED.");
+      return;
+    }
+
+    // The ours/theirs strategy never results in conflicts.
+    if ("ours".equals(strategy) || "theirs".equals(strategy)) {
+      ChangeInfo change = assertCreateSucceeds(mergeInput);
+
+      // Verify the conflicts information
+      RevisionInfo currentRevision =
+          gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+      assertThat(currentRevision.commit.parents.get(0).commit)
+          .isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
+      assertThat(currentRevision.commit.parents.get(1).commit)
+          .isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
+      assertThat(currentRevision.conflicts).isNotNull();
+      assertThat(currentRevision.conflicts.base).isNull();
+      assertThat(currentRevision.conflicts.ours)
+          .isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
+      assertThat(currentRevision.conflicts.theirs)
+          .isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
+      assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+      assertThat(currentRevision.conflicts.noBaseReason)
+          .isEqualTo(NoMergeBaseReason.ONE_SIDED_MERGE_STRATEGY);
+      assertThat(currentRevision.conflicts.containsConflicts).isFalse();
+
+      // Verify that the file content in the created change is correct.
+      // We expect that it doesn't have conflict markers and the content from "ours" version was
+      // used.
+      BinaryResult bin =
+          gApi.changes().id(project.get(), change._number).current().file(fileName).content();
+      ByteArrayOutputStream os = new ByteArrayOutputStream();
+      bin.writeTo(os);
+      String fileContent = new String(os.toByteArray(), UTF_8);
+      assertThat(fileContent).isEqualTo("ours".equals(strategy) ? targetContent : sourceContent);
+
+      return;
+    }
+
+    assume().that(strategy).isEqualTo("recursive");
+    ChangeInfo change = assertCreateSucceedsWithConflicts(mergeInput);
+
+    // Verify the conflicts information
+    RevisionInfo currentRevision =
+        gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+    assertThat(currentRevision.commit.parents.get(0).commit)
+        .isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
+    assertThat(currentRevision.commit.parents.get(1).commit)
+        .isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
+    assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isNull();
+    assertThat(currentRevision.conflicts.ours)
+        .isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
+    assertThat(currentRevision.conflicts.theirs)
+        .isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo(strategy);
+    assertThat(currentRevision.conflicts.noBaseReason).isEqualTo(NoMergeBaseReason.COMPUTED_BASE);
+    assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+
+    // Verify that the file content in the created change is correct.
+    // We expect that it has conflict markers to indicate the conflict.
+    BinaryResult bin =
+        gApi.changes().id(project.get(), change._number).current().file(fileName).content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String fileContent = new String(os.toByteArray(), UTF_8);
+    assertThat(fileContent)
+        .isEqualTo(
+            String.format(
+                """
+                <<<<<<< TARGET BRANCH (%s %s)
+                %s
+                %s=======
+                %s
+                >>>>>>> SOURCE BRANCH (%s %s)
+                """,
+                pushConflictingCommitInTargetResult.getCommit().name(),
+                targetSubject,
+                targetContent,
+                (useDiff3 ? "||||||| BASE          (computed base)\n" : ""),
+                sourceContent,
+                pushConflictingCommitInSourceResult.getCommit().name(),
+                sourceSubject));
+
+    // Verify the message that has been posted on the change.
+    List<ChangeMessageInfo> messages = gApi.changes().id(project.get(), change._number).messages();
+    assertThat(messages).hasSize(1);
+    assertThat(Iterables.getOnlyElement(messages).message)
+        .isEqualTo(
+            String.format(
+                """
+                Uploaded patch set 1.
+
+                The following files contain Git conflicts:
+                * %s
+                """,
+                fileName));
+  }
+
+  @Test
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesWithConflictsAllowed()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesConflictsAllowed(
+        /* useDiff3= */ false);
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void
+      createMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesWithConflictsAllowedUsingDiff3()
+          throws Exception {
+    testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesConflictsAllowed(
+        /* useDiff3= */ true);
+  }
+
+  private void
+      testCreateMergeChangeBetweenTwoCommitsThatHaveMultipleConflictingMergeBasesConflictsAllowed(
+          boolean useDiff3) throws Exception {
+    String sourceBranch = "sourceBranch";
+    String targetBranch = "targetBranch";
+
+    // Create source and target branch with conflicting commits.
+    // Later these commits will become the base commits for the criss-cross-merge.
+    String baseFile = "base.txt";
+    String baseContentSource = "base source";
+    String baseContentTarget = "base target";
+    ImmutableMap<String, Result> results =
+        changeInTwoBranches(
+            sourceBranch,
+            "base 1",
+            baseFile,
+            baseContentSource,
+            targetBranch,
+            "base 2",
+            baseFile,
+            baseContentTarget);
+    RevCommit baseCommitInSource = results.get(sourceBranch).getCommit();
+    RevCommit baseCommitInTarget = results.get(targetBranch).getCommit();
+
+    // Create merge commits in both branches (1. merge the target branch into the source branch, 2.
+    // merge the source branch into the target branch).
+    PushOneCommit mergeCommitInSource =
+        pushFactory.create(
+            user.newIdent(), testRepo, "Merge in Source", baseFile, baseContentSource);
+    mergeCommitInSource.setParents(ImmutableList.of(baseCommitInSource, baseCommitInTarget));
+    mergeCommitInSource.to("refs/heads/" + sourceBranch).assertOkStatus();
+    PushOneCommit mergeCommitInTarget =
+        pushFactory.create(
+            user.newIdent(), testRepo, "Merge in Target", baseFile, baseContentTarget);
+    mergeCommitInTarget.setParents(ImmutableList.of(baseCommitInTarget, baseCommitInSource));
+    mergeCommitInTarget.to("refs/heads/" + targetBranch).assertOkStatus();
+
+    // Create conflicting commits in both branches.
+    String fileName = "shared.txt";
+    String sourceSubject = "source change";
+    String sourceContent = "source content";
+    String targetSubject = "target change";
+    String targetContent = "target content";
+    PushOneCommit pushConflictingCommitInSource =
+        pushFactory.create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent);
+    pushConflictingCommitInSource.setParent(
+        projectOperations.project(project).getHead(sourceBranch));
+    PushOneCommit.Result pushConflictingCommitInSourceResult =
+        pushConflictingCommitInSource.to("refs/heads/" + sourceBranch);
+    pushConflictingCommitInSourceResult.assertOkStatus();
+    PushOneCommit pushConflictingCommitInTarget =
+        pushFactory.create(user.newIdent(), testRepo, targetSubject, fileName, targetContent);
+    pushConflictingCommitInTarget.setParent(
+        projectOperations.project(project).getHead(targetBranch));
+    PushOneCommit.Result pushConflictingCommitInTargetResult =
+        pushConflictingCommitInTarget.to("refs/heads/" + targetBranch);
+    pushConflictingCommitInTargetResult.assertOkStatus();
+
+    // Merge the source branch into the target with conflicts allowed. This is a criss-cross-merge:
+    //
+    //                   (criss-cross-merge)
+    //                     /             \
+    // (conflictingCommitInSource) (conflictingCommitInTarget)
+    //                    |               |
+    //       (mergeCommitInSource) (mergeCommitInTarget)
+    //                    |       X       |
+    //       (baseCommitInSource)  (baseCommitInTarget)
+    //                     \           /
+    //                    (initialCommit)
+    ChangeInput mergeInput =
+        newMergeChangeInput(targetBranch, sourceBranch, "recursive", /* allowConflicts= */ true);
+
+    ChangeInfo change = assertCreateSucceedsWithConflicts(mergeInput);
+
+    // Verify the conflicts information
+    RevisionInfo currentRevision =
+        gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+    assertThat(currentRevision.commit.parents.get(0).commit)
+        .isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
+    assertThat(currentRevision.commit.parents.get(1).commit)
+        .isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
+    assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isNull();
+    assertThat(currentRevision.conflicts.ours)
+        .isEqualTo(pushConflictingCommitInTargetResult.getCommit().name());
+    assertThat(currentRevision.conflicts.theirs)
+        .isEqualTo(pushConflictingCommitInSourceResult.getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isEqualTo(NoMergeBaseReason.COMPUTED_BASE);
+    assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+
+    // Verify that the content of the file that was conflicting in the bases is correct.
+    // We expect that it has conflict markers to indicate the conflict and that the base version
+    // that is computed by the recursive merge is a auto merge of the two bases, containing conflict
+    // markers as well.
+    String computedBaseContent =
+        String.format(
+            """
+            <<<<<<< OURS
+            %s
+            =======
+            %s
+            >>>>>>> THEIRS
+            """,
+            baseContentTarget, baseContentSource);
+    BinaryResult bin =
+        gApi.changes().id(project.get(), change._number).current().file(baseFile).content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String fileContent = new String(os.toByteArray(), UTF_8);
+    assertThat(fileContent)
+        .isEqualTo(
+            String.format(
+                """
+                <<<<<<< TARGET BRANCH (%s %s)
+                %s
+                %s=======
+                %s
+                >>>>>>> SOURCE BRANCH (%s %s)
+                """,
+                pushConflictingCommitInTargetResult.getCommit().name(),
+                targetSubject,
+                baseContentTarget,
+                (useDiff3 ? "||||||| BASE          (computed base)\n" + computedBaseContent : ""),
+                baseContentSource,
+                pushConflictingCommitInSourceResult.getCommit().name(),
+                sourceSubject));
+
+    // Verify that the file content in the created change is correct.
+    // We expect that it has conflict markers to indicate the conflict.
+    bin = gApi.changes().id(project.get(), change._number).current().file(fileName).content();
+    os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    fileContent = new String(os.toByteArray(), UTF_8);
+    assertThat(fileContent)
+        .isEqualTo(
+            String.format(
+                """
+                <<<<<<< TARGET BRANCH (%s %s)
+                %s
+                %s=======
+                %s
+                >>>>>>> SOURCE BRANCH (%s %s)
+                """,
+                pushConflictingCommitInTargetResult.getCommit().name(),
+                targetSubject,
+                targetContent,
+                (useDiff3 ? "||||||| BASE          (computed base)\n" : ""),
+                sourceContent,
+                pushConflictingCommitInSourceResult.getCommit().name(),
+                sourceSubject));
+
+    // Verify the message that has been posted on the change.
+    List<ChangeMessageInfo> messages = gApi.changes().id(project.get(), change._number).messages();
+    assertThat(messages).hasSize(1);
+    assertThat(Iterables.getOnlyElement(messages).message)
+        .isEqualTo(
+            String.format(
+                """
+                Uploaded patch set 1.
+
+                The following files contain Git conflicts:
+                * %s
+                * %s
+                """,
+                baseFile, fileName));
   }
 
   @Test
@@ -917,6 +1765,71 @@
   }
 
   @Test
+  public void updateCommitMessageOfMergeChangeWithConflicts() throws Exception {
+    // Create a change with a merge commit that contains conflicts
+    String sourceBranch = "source";
+    String targetBranch = "target";
+    ImmutableMap<String, Result> results =
+        changeInTwoBranches(
+            sourceBranch,
+            "source change",
+            "shared.txt",
+            "source content",
+            targetBranch,
+            "target change",
+            "shared.txt",
+            "target content");
+    RevCommit baseCommit = results.get("master").getCommit();
+    ChangeInput in =
+        newMergeChangeInput(targetBranch, sourceBranch, "", /* allowConflicts= */ true);
+    in.subject = "Merge " + sourceBranch + " to " + targetBranch;
+    ChangeInfo change = assertCreateSucceedsWithConflicts(in);
+
+    // Verify the conflicts information
+    RevisionInfo currentRevision =
+        gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+    assertThat(currentRevision._number).isEqualTo(1);
+    assertThat(currentRevision.commit.parents.get(0).commit)
+        .isEqualTo(results.get(targetBranch).getCommit().name());
+    assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
+    assertThat(currentRevision.conflicts.ours)
+        .isEqualTo(results.get(targetBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.theirs)
+        .isEqualTo(results.get(sourceBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
+    assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+
+    // Update the commit message
+    gApi.changes()
+        .id(change.id)
+        .setMessage(
+            "Merge "
+                + sourceBranch
+                + " branch into "
+                + targetBranch
+                + " branch\n\nChange-Id: "
+                + change.changeId);
+
+    // Verify that the conflicts information has been copied
+    currentRevision =
+        gApi.changes().id(change.id).get(CURRENT_REVISION, CURRENT_COMMIT).getCurrentRevision();
+    assertThat(currentRevision._number).isEqualTo(2);
+    assertThat(currentRevision.commit.parents.get(0).commit)
+        .isEqualTo(results.get(targetBranch).getCommit().name());
+    assertThat(currentRevision.conflicts).isNotNull();
+    assertThat(currentRevision.conflicts.base).isEqualTo(baseCommit.name());
+    assertThat(currentRevision.conflicts.ours)
+        .isEqualTo(results.get(targetBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.theirs)
+        .isEqualTo(results.get(sourceBranch).getCommit().name());
+    assertThat(currentRevision.conflicts.mergeStrategy).isEqualTo("recursive");
+    assertThat(currentRevision.conflicts.noBaseReason).isNull();
+    assertThat(currentRevision.conflicts.containsConflicts).isTrue();
+  }
+
+  @Test
   public void invalidSource() throws Exception {
     changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
     ChangeInput in = newMergeChangeInput("branchA", "invalid", "");
@@ -1151,8 +2064,11 @@
 
   @Test
   public void createPatchApplyingChange_withParentCommit_success() throws Exception {
+    Ref previousHead = testRepo.getRepository().exactRef("HEAD");
     createBranch(BranchNameKey.create(project, "other"));
     Result baseChange = createChange("refs/heads/other");
+    /* Reset HEAD before creating change against master as to not create an implicit merge. */
+    testRepo.reset(previousHead.getObjectId());
     PushOneCommit.Result ignoredCommit = createChange();
     ignoredCommit.assertOkStatus();
     ChangeInput input = newPatchApplyingChangeInput("other", PATCH_INPUT);
@@ -1385,6 +2301,8 @@
   public void createChangeWithSourceBranch() throws Exception {
     changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
 
+    requestScopeOperations.setApiUser(user.id());
+
     // create a merge change from branchA to master in gerrit
     ChangeInput in = new ChangeInput();
     in.project = project.get();
@@ -1607,14 +2525,24 @@
   }
 
   private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef, String strategy) {
-    return newMergeChangeInput(targetBranch, sourceRef, strategy, /* allowConflicts= */ false);
+    return newMergeChangeInput(
+        project, targetBranch, sourceRef, strategy, /* allowConflicts= */ false);
   }
 
   private ChangeInput newMergeChangeInput(
       String targetBranch, String sourceRef, String strategy, boolean allowConflicts) {
+    return newMergeChangeInput(project, targetBranch, sourceRef, strategy, allowConflicts);
+  }
+
+  private ChangeInput newMergeChangeInput(
+      Project.NameKey projectName,
+      String targetBranch,
+      String sourceRef,
+      String strategy,
+      boolean allowConflicts) {
     // create a merge change from branchA to master in gerrit
     ChangeInput in = new ChangeInput();
-    in.project = project.get();
+    in.project = projectName.get();
     in.branch = targetBranch;
     in.subject = "merge " + sourceRef + " to " + targetBranch;
     in.status = ChangeStatus.NEW;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/GetPatchIT.java b/javatests/com/google/gerrit/acceptance/rest/change/GetPatchIT.java
index 48afab5..61affab 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/GetPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/GetPatchIT.java
@@ -21,6 +21,7 @@
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -121,4 +122,32 @@
       assertThat(resultingPatch).isEqualTo(patch);
     }
   }
+
+  @Test
+  public void patchWithContext() throws Exception {
+    String fileName = "a_new_file.txt";
+    String fileContent = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n";
+    PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    gApi.changes().id(result.getChangeId()).current().submit();
+
+    PushOneCommit.Result result2 =
+        createChange(
+            "Amend a file",
+            fileName,
+            "1\n2\n3\n4\n5\n-\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n");
+    String triplet = project.get() + "~master~" + result2.getChangeId();
+
+    var apiResult =
+        userRestSession.get("/changes/" + triplet + "/revisions/1/patch?raw&context=10");
+    apiResult.assertOK();
+    String resultingPatch =
+        RawParseUtils.decode(
+            apiResult.getRawContent().array(),
+            apiResult.getRawContent().arrayOffset(),
+            apiResult.getRawContent().limit());
+    assertThat(resultingPatch).contains("@@ -1,16 +1,16 @@");
+    assertThat(resultingPatch).contains("-6");
+    assertThat(resultingPatch).contains("+-\n");
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
index f5cca1b..eaa67c1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
@@ -127,9 +126,7 @@
 
   private void setPrivateByDefault(Project.NameKey proj, InheritableBoolean value)
       throws Exception {
-    ConfigInput input = new ConfigInput();
-    input.privateByDefault = value;
-    gApi.projects().name(proj.get()).config(input);
+    projectOperations.project(proj).forUpdate().privateByDefault(value).update();
   }
 
   private PushOneCommit.Result createChange(Project.NameKey proj) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 37684de..3d05aa9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -132,7 +132,7 @@
             .file("file")
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index dccc057..0f41219 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -286,6 +286,7 @@
       name = "experiments.disabled",
       // The test intentionally create an implicit merge change.
       value = "GerritBackendFeature__reject_implicit_merges_on_merge")
+  @GerritConfig(name = "repository.*.defaultConfig", value = "receive.rejectImplicitMerges=false")
   public void submitWithMergedAncestorsOnOtherBranch() throws Throwable {
     RevCommit initialHead = projectOperations.project(project).getHead("master");
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index 80fbe99..609043d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -134,7 +134,7 @@
             .file("file")
             .content("content")
             .owner(testUser)
-            .create();
+            .createV1();
 
     // Change preferred email for the user
     String emailTwo = "email2@example.com";
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 525f5d5..3adace4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -27,9 +27,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.inject.Inject;
@@ -206,9 +204,7 @@
   }
 
   private void setWorkInProgressByDefaultForProject(Project.NameKey p) throws Exception {
-    ConfigInput input = new ConfigInput();
-    input.workInProgressByDefault = InheritableBoolean.TRUE;
-    gApi.projects().name(p.get()).config(input);
+    projectOperations.project(p).forUpdate().workInProgressByDefault().update();
   }
 
   private void setWorkInProgressByDefaultForUser() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 479baf3..d3f7123 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -72,8 +72,7 @@
   // change
   @GerritConfig(name = "change.updateDelay", value = "50s")
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
-  @GerritConfig(name = "change.enableRobotComments", value = "false")
-
+  @GerritConfig(name = "change.allowMarkdownBase64ImagesInComments", value = "false")
   // download
   @GerritConfig(
       name = "download.archive",
@@ -113,7 +112,7 @@
     // change
     assertThat(i.change.updateDelay).isEqualTo(50);
     assertThat(i.change.disablePrivateChanges).isTrue();
-    assertThat(i.change.enableRobotComments).isNull();
+    assertThat(i.change.allowMarkdownBase64ImagesInComments).isNull();
 
     // download
     assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
@@ -205,6 +204,7 @@
 
     // submit requirement columns in dashboard
     assertThat(i.submitRequirementDashboardColumns).isEmpty();
+    assertThat(i.dashboardShowAllLabels).isNull();
   }
 
   @Test
@@ -224,6 +224,13 @@
   }
 
   @Test
+  @GerritConfig(name = "dashboard.showAllLabels", value = "true")
+  public void serverConfigWithDashboardShowAllLabels() throws Exception {
+    ServerInfo i = gApi.config().server().getInfo();
+    assertThat(i.dashboardShowAllLabels).isTrue();
+  }
+
+  @Test
   @GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER")
   public void mergeabilityComputationBehavior_neverCompute() throws Exception {
     ServerInfo i = gApi.config().server().getInfo();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 643c1a6..089e901 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -374,6 +374,106 @@
   }
 
   @Test
+  public void createWithRevisionAndSourceRef() throws Exception {
+    String sourceRef = "refs/sandbox/master";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref(sourceRef).group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref(sourceRef).group(REGISTERED_USERS))
+        .update();
+    // create a new branch outside refs/heads/ to use as the source
+    pushTo(sourceRef);
+    RevCommit revision = projectOperations.project(project).getHead(sourceRef);
+    // update it so that points to a different revision than the revision on which we create the new
+    // branch
+    pushTo(sourceRef);
+    assertThat(projectOperations.project(project).getHead(sourceRef)).isNotEqualTo(revision);
+
+    BranchInput input = new BranchInput();
+    input.revision = revision.name();
+    input.sourceRef = sourceRef;
+    BranchInfo created = branch(testBranch).create(input).get();
+    assertThat(created.ref).isEqualTo(testBranch.branch());
+    assertThat(created.revision).isEqualTo(revision.name());
+    assertThat(projectOperations.project(project).getHead(testBranch.branch())).isEqualTo(revision);
+  }
+
+  @Test
+  public void cannotCreateWithInvisibleRevisionAndVisibleSourceRef() throws Exception {
+    String sourceRef = "refs/sandbox/master";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    // create a new branch outside refs/heads/ to use as the source
+    pushTo(sourceRef);
+    RevCommit secretCommit = projectOperations.project(project).getHead(sourceRef);
+    // update it so that points to a different revision than the revision on which we create the new
+    // branch
+    pushTo(sourceRef);
+    assertThat(projectOperations.project(project).getHead(sourceRef)).isNotEqualTo(secretCommit);
+
+    // hide the source branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(sourceRef).group(REGISTERED_USERS))
+        .update();
+
+    TestAccount unprivileged =
+        accountCreator.create("unprivileged", "unprivileged@example.com", "unprivileged", null);
+    requestScopeOperations.setApiUser(unprivileged.id());
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name(project.get()).branch(sourceRef).get());
+
+    BranchInput input = new BranchInput();
+    input.revision = secretCommit.name();
+    input.sourceRef = "refs/heads/master";
+    assertThrows(UnprocessableEntityException.class, () -> branch(testBranch).create(input));
+  }
+
+  @Test
+  public void cannotCreateWithRevisionAndInvisibleSourceRef() throws Exception {
+    String sourceRef = "refs/sandbox/master";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE).ref("refs/*").group(REGISTERED_USERS))
+        .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+    // create a new branch outside refs/heads/ to use as the source
+    pushTo(sourceRef);
+    RevCommit secretCommit = projectOperations.project(project).getHead(sourceRef);
+    // update it so that points to a different revision than the revision on which we create the new
+    // branch
+    pushTo(sourceRef);
+    assertThat(projectOperations.project(project).getHead(sourceRef)).isNotEqualTo(secretCommit);
+
+    // hide the source branch
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref(sourceRef).group(REGISTERED_USERS))
+        .update();
+
+    TestAccount unprivileged =
+        accountCreator.create("unprivileged", "unprivileged@example.com", "unprivileged", null);
+    requestScopeOperations.setApiUser(unprivileged.id());
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> gApi.projects().name(project.get()).branch(sourceRef).get());
+
+    BranchInput input = new BranchInput();
+    input.revision = secretCommit.name();
+    input.sourceRef = sourceRef;
+    assertThrows(UnprocessableEntityException.class, () -> branch(testBranch).create(input));
+  }
+
+  @Test
   public void createEmptyCommitAndRevisionAreMutuallyExclusive() throws Exception {
     BranchInput input = new BranchInput();
     input.createEmptyCommit = true;
@@ -382,7 +482,7 @@
         assertThrows(BadRequestException.class, () -> branch(testBranch).create(input));
     assertThat(thrown)
         .hasMessageThat()
-        .contains("create_empty_commit and revision are mutually exclusive");
+        .contains("create_empty_commit and revision/source_ref are mutually exclusive");
   }
 
   @Test
@@ -574,15 +674,46 @@
     branchInput.ref = "public";
     branchInput.revision = secretCommit.name();
     assertThrows(
-        AuthException.class,
+        UnprocessableEntityException.class,
         () -> gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput));
 
     branchInput.revision = "refs/heads/secret/main";
     assertThrows(
-        AuthException.class,
+        UnprocessableEntityException.class,
         () -> gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput));
   }
 
+  @Test
+  public void createBranchAsAdminIsPossibleWithoutExplicitReadPermission() throws Exception {
+    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/heads/foo");
+    result.assertOkStatus();
+    RevCommit fooCommit = result.getCommit();
+
+    // Remove read access
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
+        .update();
+
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = "bar";
+    branchInput.revision = fooCommit.name();
+    BranchInfo created =
+        gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput).get();
+    assertThat(created.ref).isEqualTo(RefNames.fullName(branchInput.ref));
+    assertThat(created.revision).isEqualTo(fooCommit.name());
+    assertThat(projectOperations.project(project).getHead(branchInput.ref)).isEqualTo(fooCommit);
+
+    branchInput.ref = "baz";
+    branchInput.revision = "refs/heads/foo";
+    created = gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput).get();
+    assertThat(created.ref).isEqualTo(RefNames.fullName(branchInput.ref));
+    assertThat(created.revision).isEqualTo(fooCommit.name());
+    assertThat(projectOperations.project(project).getHead(branchInput.ref)).isEqualTo(fooCommit);
+  }
+
   private void blockCreateReference() throws Exception {
     projectOperations
         .project(project)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
index 6b61252..37f45f2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -248,7 +248,7 @@
 
     assertThat(createdLabel.name).isEqualTo("Foo");
     assertThat(createdLabel.projectName).isEqualTo(project.get());
-    assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName());
+    assertThat(createdLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName());
     assertThat(createdLabel.values).containsExactlyEntriesIn(input.values);
     assertThat(createdLabel.defaultValue).isEqualTo(0);
     assertThat(createdLabel.branches).isNull();
@@ -270,6 +270,35 @@
   }
 
   @Test
+  public void cannotCreateWithDeprecatedFunction() throws Exception {
+    for (LabelFunction deprecatedLabelFunction :
+        ImmutableList.of(
+            LabelFunction.ANY_WITH_BLOCK,
+            LabelFunction.MAX_NO_BLOCK,
+            LabelFunction.MAX_WITH_BLOCK)) {
+      LabelDefinitionInput input = new LabelDefinitionInput();
+      input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+      input.function = deprecatedLabelFunction.getFunctionName();
+
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.projects().name(project.get()).label("Foo").create(input));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Function %s is deprecated. The function can only be set to %s. Use submit"
+                      + " requirements instead of label functions.",
+                  input.function,
+                  ImmutableList.of(
+                      LabelFunction.NO_BLOCK.getFunctionName(),
+                      LabelFunction.NO_OP.getFunctionName(),
+                      LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+    }
+  }
+
+  @Test
   public void functionEmptyAfterTrim() throws Exception {
     LabelDefinitionInput input = new LabelDefinitionInput();
     input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 217b04b..1343ad5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -21,8 +21,11 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
 import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -54,8 +57,12 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.restapi.project.DeleteRef;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
 import java.util.Collections;
@@ -72,6 +79,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -81,6 +89,7 @@
 public class CreateProjectIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private DeleteRef deleteRef;
 
   @Test
   public void createProjectHttp() throws Exception {
@@ -558,6 +567,203 @@
     assertThat(cfg.defaultSubmitType.inheritedValue).isEqualTo(SubmitType.MERGE_ALWAYS);
   }
 
+  @Test
+  public void initOnly_noOpIfProjectHasAlreadyBeenInitialized() throws Exception {
+    Project.NameKey projectName = Project.nameKey(name("newProject"));
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = projectName.get();
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertHead(projectName.get(), "refs/heads/master");
+    assertThat(readProjectConfig(projectName.get()))
+        .hasValue("[access]\n\tinheritFrom = All-Projects\n[submit]\n\taction = inherit\n");
+    assertEmptyCommit(projectName.get(), "refs/heads/master", "refs/heads/stable");
+    RevCommit headRefsMetaConfig =
+        projectOperations.project(projectName).getHead("refs/meta/config");
+    RevCommit headMaster = projectOperations.project(projectName).getHead("refs/heads/master");
+    RevCommit headStable = projectOperations.project(projectName).getHead("refs/heads/stable");
+
+    // Test that rerunning the project init doesn't change anything.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    RestResponse r = adminRestSession.put("/projects/" + projectName.get(), projectInput);
+    r.assertOK();
+    assertHead(projectName.get(), "refs/heads/master");
+    assertThat(readProjectConfig(projectName.get()))
+        .hasValue("[access]\n\tinheritFrom = All-Projects\n[submit]\n\taction = inherit\n");
+    assertEmptyCommit(projectName.get(), "refs/heads/master", "refs/heads/stable");
+    assertThat(projectOperations.project(projectName).getHead("refs/meta/config"))
+        .isEqualTo(headRefsMetaConfig);
+    assertThat(projectOperations.project(projectName).getHead("refs/heads/master"))
+        .isEqualTo(headMaster);
+    assertThat(projectOperations.project(projectName).getHead("refs/heads/stable"))
+        .isEqualTo(headStable);
+  }
+
+  @Test
+  public void initOnly_cannotResetHead() throws Exception {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = name("newProject");
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertHead(projectInput.name, "refs/heads/master");
+
+    // Set HEAD to another branch.
+    try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO);
+        Repository repo = repoManager.openRepository(Project.nameKey(projectInput.name))) {
+      RefUpdate u = repo.updateRef(Constants.HEAD);
+      u.disableRefLog();
+      u.link("refs/heads/stable");
+    }
+    assertHead(projectInput.name, "refs/heads/stable");
+
+    // Rerunning the project doesn't override the existing head.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    RestResponse r = adminRestSession.put("/projects/" + projectInput.name, projectInput);
+    r.assertConflict();
+    assertHead(projectInput.name, "refs/heads/stable");
+  }
+
+  @Test
+  public void initOnly_createMissingRefsMetaConfig() throws Exception {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = name("newProject");
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertThat(readProjectConfig(projectInput.name))
+        .hasValue("[access]\n\tinheritFrom = All-Projects\n[submit]\n\taction = inherit\n");
+
+    // Delete refs/meta/config.
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      deleteRef.deleteSingleRef(
+          projectCache.get(Project.nameKey(projectInput.name)).get(), "refs/meta/config", R_HEADS);
+    }
+
+    // Rerunning the project init creates refs/meta/config.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    RestResponse r = adminRestSession.put("/projects/" + projectInput.name, projectInput);
+    r.assertOK();
+    assertThat(readProjectConfig(projectInput.name))
+        .hasValue("[access]\n\tinheritFrom = All-Projects\n[submit]\n\taction = inherit\n");
+  }
+
+  @Test
+  public void initOnly_cannotResetProjectConfig() throws Exception {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = name("newProject");
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertThat(readProjectConfig(projectInput.name))
+        .hasValue("[access]\n\tinheritFrom = All-Projects\n[submit]\n\taction = inherit\n");
+
+    // Update project.config
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(Project.nameKey(projectInput.name))) {
+      ProjectConfig config = projectConfigFactory.read(md);
+      config.updateProject(p -> p.setParent(project));
+      config.commit(md, false);
+    }
+    assertThat(readProjectConfig(projectInput.name))
+        .hasValue(
+            String.format("[access]\n\tinheritFrom = %s\n[submit]\n\taction = inherit\n", project));
+
+    // Rerunning the project init doesn't update refs/meta/config.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    RestResponse r = adminRestSession.put("/projects/" + projectInput.name, projectInput);
+    r.assertConflict();
+    assertThat(readProjectConfig(projectInput.name))
+        .hasValue(
+            String.format("[access]\n\tinheritFrom = %s\n[submit]\n\taction = inherit\n", project));
+  }
+
+  @Test
+  public void initOnly_createMissingBranches() throws Exception {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = name("newProject");
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertEmptyCommit(projectInput.name, "refs/heads/master", "refs/heads/stable");
+
+    // Delete the branches.
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      deleteRef.deleteSingleRef(
+          projectCache.get(Project.nameKey(projectInput.name)).get(), "refs/heads/master", R_HEADS);
+      deleteRef.deleteSingleRef(
+          projectCache.get(Project.nameKey(projectInput.name)).get(), "refs/heads/stable", R_HEADS);
+    }
+
+    // Rerunning the project init creates the branches.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    RestResponse r = adminRestSession.put("/projects/" + projectInput.name, projectInput);
+    r.assertOK();
+    assertEmptyCommit(projectInput.name, "refs/heads/master", "refs/heads/stable");
+  }
+
+  @Test
+  public void initOnly_cannotCreateAdditionalBranches() throws Exception {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = name("newProject");
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertEmptyCommit(projectInput.name, "refs/heads/master", "refs/heads/stable");
+
+    // Rerunning the project init doesn't create additional branches.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    projectInput.branches =
+        ImmutableList.of("refs/heads/master", "refs/heads/stable", "refs/heads/test");
+    RestResponse r = adminRestSession.put("/projects/" + projectInput.name, projectInput);
+    r.assertConflict();
+    assertEmptyCommit(projectInput.name, "refs/heads/master", "refs/heads/stable");
+    assertThat(
+            projectOperations
+                .project(Project.nameKey(projectInput.name))
+                .hasHead("refs/heads/test"))
+        .isFalse();
+  }
+
+  @Test
+  public void initOnly_requiresCreateProjectCapability() throws Exception {
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = name("newProject");
+    projectInput.createEmptyCommit = true;
+    projectInput.branches = ImmutableList.of("refs/heads/master", "refs/heads/stable");
+
+    gApi.projects().create(projectInput);
+    assertHead(projectInput.name, "refs/heads/master");
+    assertThat(readProjectConfig(projectInput.name))
+        .hasValue("[access]\n\tinheritFrom = All-Projects\n[submit]\n\taction = inherit\n");
+    assertEmptyCommit(projectInput.name, "refs/heads/master", "refs/heads/stable");
+
+    // Test that rerunning the project init is rejected for non-admin user.
+    // We must make the create project request via REST since the gApi refuses to do the request if
+    // the project already exists.
+    projectInput.initOnly = true;
+    RestResponse r = userRestSession.put("/projects/" + projectInput.name, projectInput);
+    r.assertForbidden();
+  }
+
   private void assertEmptyCommit(String projectName, String... refs) throws Exception {
     Project.NameKey projectKey = Project.nameKey(projectName);
     try (Repository repo = repoManager.openRepository(projectKey);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteChangesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteChangesIT.java
new file mode 100644
index 0000000..d0a3012
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteChangesIT.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.api.projects.DeleteChangesResult.FAILURE;
+import static com.google.gerrit.extensions.api.projects.DeleteChangesResult.NOT_UNIQUE;
+import static com.google.gerrit.extensions.api.projects.DeleteChangesResult.SUCCESS;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.projects.DeleteChangesInput;
+import com.google.gerrit.extensions.api.projects.DeleteChangesResult;
+import com.google.gerrit.extensions.api.projects.ProjectApi;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+@NoHttpd
+public class DeleteChangesIT extends AbstractDaemonTest {
+
+  private ProjectApi project() throws Exception {
+    return gApi.projects().name(project.get());
+  }
+
+  @Test
+  public void deleteChangesFailure() throws Exception {
+    DeleteChangesInput deleteInput = new DeleteChangesInput();
+    deleteInput.changes = List.of("Non-existing-change-1", "Non-existing-change-2");
+
+    Map<DeleteChangesResult, Collection<String>> response = project().deleteChanges(deleteInput);
+    assertThat(response).containsKey(FAILURE);
+    assertThat(response.get(FAILURE))
+        .containsExactly("Non-existing-change-1", "Non-existing-change-2");
+  }
+
+  @Test
+  public void deleteChangesSuccess() throws Exception {
+    PushOneCommit.Result c1 = createChange();
+    PushOneCommit.Result c2 = createChange();
+    assertThat(c1.getChange().change().getProject().get()).isNotNull();
+    assertThat(c2.getChange().change().getProject().get()).isNotNull();
+    DeleteChangesInput deleteInput = new DeleteChangesInput();
+    deleteInput.changes = List.of(c1.getChangeId(), c2.getChangeId());
+    Map<DeleteChangesResult, Collection<String>> response = project().deleteChanges(deleteInput);
+    assertThat(response).containsKey(SUCCESS);
+    assertThat(response.get(SUCCESS)).containsExactly(c1.getChangeId(), c2.getChangeId());
+    assertThat(query(c1.getChangeId())).isEmpty();
+    assertThat(query(c2.getChangeId())).isEmpty();
+  }
+
+  @Test
+  public void deleteChangesSuccessFailureNotUnique() throws Exception {
+    TestRepository<InMemoryRepository> repo = cloneProject(project);
+    PushOneCommit.Result c1 = createChange(repo, "master", "Add a file", "foo", "content", null);
+    PushOneCommit.Result c2 = createChange();
+
+    // cherry pick a change to make a duplicate change for NOT_UNIQUE case.
+    String newBranch = "Test-branch";
+    createBranch(BranchNameKey.create(project, newBranch));
+    CherryPickInput cpi = new CherryPickInput();
+    cpi.destination = newBranch;
+    gApi.changes().id(c1.getChangeId()).current().cherryPick(cpi);
+
+    DeleteChangesInput deleteInput = new DeleteChangesInput();
+    deleteInput.changes = List.of(c1.getChangeId(), c2.getChangeId(), "Non-existing-change-1");
+    Map<DeleteChangesResult, Collection<String>> response = project().deleteChanges(deleteInput);
+    assertThat(response).containsKey(FAILURE);
+    assertThat(response.get(FAILURE)).containsExactly("Non-existing-change-1");
+    assertThat(response).containsKey(SUCCESS);
+    assertThat(response.get(SUCCESS)).containsExactly(c2.getChangeId());
+    assertThat(response).containsKey(NOT_UNIQUE);
+    assertThat(response.get(NOT_UNIQUE)).containsExactly(c1.getChangeId());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
index a05f099..e746973 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetBranchIT.java
@@ -115,7 +115,7 @@
   @Test
   public void getChangeRef() throws Exception {
     // create a change
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
 
     // a user without the 'Access Database' capability can see the change ref
     requestScopeOperations.setApiUser(user.id());
@@ -127,7 +127,8 @@
   public void getChangeRefOfNonVisibleChange() throws Exception {
     // create a change
     String branchName = "master";
-    Change.Id changeId = changeOperations.newChange().project(project).branch(branchName).create();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).branch(branchName).createV1();
 
     // block read access to the branch
     projectOperations
@@ -150,7 +151,7 @@
     TestAccount user2 = accountCreator.user2();
 
     // create a change
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
 
     // create a change edit by 'user'
     requestScopeOperations.setApiUser(user.id());
@@ -173,7 +174,8 @@
   public void cannotGetChangeEditRefOfNonVisibleChange() throws Exception {
     // create a change
     String branchName = "master";
-    Change.Id changeId = changeOperations.newChange().project(project).branch(branchName).create();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).branch(branchName).createV1();
 
     // create a change edit by 'user'
     requestScopeOperations.setApiUser(user.id());
@@ -197,7 +199,7 @@
   @Test
   public void getChangeMetaRef() throws Exception {
     // create a change
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
 
     // A user without the 'Access Database' capability can see the change meta ref.
     // This may be surprising, as 'Access Database' guards access to meta refs and the change meta
@@ -509,7 +511,7 @@
             .branch("master")
             .file(file)
             .content("base content")
-            .create();
+            .createV1();
     approve(Integer.toString(baseChange.get()));
     gApi.changes().id(baseChange.get()).current().submit();
 
@@ -527,7 +529,7 @@
             .branch("master")
             .file(file)
             .content("master content")
-            .create();
+            .createV1();
     approve(Integer.toString(changeInMaster.get()));
     gApi.changes().id(changeInMaster.get()).current().submit();
 
@@ -539,7 +541,7 @@
             .branch(branchName)
             .file(file)
             .content("other content")
-            .create();
+            .createV1();
     approve(Integer.toString(changeInOtherBranch.get()));
     gApi.changes().id(changeInOtherBranch.get()).current().submit();
 
@@ -555,7 +557,7 @@
             .tipOfBranch(branchName)
             .file(file)
             .content("merged content")
-            .create();
+            .createV1();
 
     String mergeRevision =
         changeOperations.change(mergeChange).currentPatchset().get().commitId().name();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index b399841..4755f27 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -362,7 +362,9 @@
         ImmutableList.of(branch1, branch2, branch3), list().withSubstring("somebranch").get());
 
     // Using regex.
-    assertRefs(ImmutableList.of(master), list().withRegex(".*ast.*r").get());
+    assertRefs(
+        ImmutableList.of(branch("HEAD", "master", false), master),
+        list().withRegex(".*ast.*r").get());
     assertRefs(ImmutableList.of(), list().withRegex(".*AST.*R").get());
 
     // Conflicting options
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
index 2667851..284d175 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
 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.allowLabel;
+import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
@@ -24,12 +26,16 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
@@ -45,6 +51,7 @@
 public class ListLabelsIT extends AbstractDaemonTest {
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ProjectOperations projectOperations;
+  @Inject private ExtensionRegistry extensionRegistry;
 
   @Test
   public void anonymous() throws Exception {
@@ -240,6 +247,30 @@
   }
 
   @Test
+  public void withInheritedLabelsIncludesGlobalLabel() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .build();
+
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      List<LabelDefinitionInfo> labels =
+          gApi.projects().name(project.get()).labels().withInherited(true).get();
+      assertThat(labelNames(labels))
+          .containsExactly(globalLabelType.getName(), LabelId.CODE_REVIEW)
+          .inOrder();
+      assertThat(labels.get(0).projectName).isNull();
+      LabelAssert.assertCodeReviewLabel(labels.get(1));
+    }
+  }
+
+  @Test
   public void withInheritedLabelsAndOverriddenLabel() throws Exception {
     configLabel(LabelId.CODE_REVIEW, LabelFunction.NO_OP);
 
@@ -273,6 +304,36 @@
   }
 
   @Test
+  public void globalLabels() throws Exception {
+    LabelType globalLabelType =
+        LabelType.builder(
+                "Global-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Approved"),
+                    LabelValue.create((short) 0, "No vote"),
+                    LabelValue.create((short) -1, "Rejected")))
+            .setDescription("A global label")
+            .setFunction(LabelFunction.NO_OP)
+            .build();
+
+    try (Registration registration = extensionRegistry.newRegistration().add(globalLabelType)) {
+      ImmutableList<LabelDefinitionInfo> labels = gApi.config().server().listGlobalLabels().get();
+      assertThat(labelNames(labels)).containsExactly(globalLabelType.getName());
+
+      LabelDefinitionInfo globalLabel = Iterables.getOnlyElement(labels);
+      assertThat(globalLabel.projectName).isNull();
+      assertThat(globalLabel.description).isEqualTo(globalLabelType.getDescription().get());
+      assertThat(globalLabel.values)
+          .containsExactlyEntriesIn(
+              globalLabelType.getValues().stream()
+                  .collect(
+                      toImmutableMap(
+                          labelValue -> LabelValue.formatValue(labelValue.getValue()),
+                          LabelValue::getText)));
+    }
+  }
+
+  @Test
   public void voteableOnRefNotFound() throws Exception {
     // Grant permission to read refs/meta/config
     projectOperations
@@ -318,6 +379,27 @@
   }
 
   @Test
+  public void voteableOnRefForChangeOwners() throws Exception {
+    configLabel("foo", LabelFunction.NO_OP);
+    configLabel("bar", LabelFunction.NO_OP);
+
+    // Grant permissions to read config and vote on 'foo' label with full range (-2 to +2)
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+        .add(allowLabel("foo").ref("refs/heads/master").group(CHANGE_OWNER).range(-2, 2))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    List<LabelDefinitionInfo> labels =
+        gApi.projects().name(project.get()).labels().withVoteableOnRef("refs/heads/master").get();
+
+    assertThat(labelNames(labels)).containsExactly("foo");
+  }
+
+  @Test
   public void voteableOnRefRespectsBranchRestrictions() throws Exception {
     projectOperations
         .project(project)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
index cbaba2e..c21a340 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PostLabelsIT.java
@@ -240,6 +240,68 @@
   }
 
   @Test
+  public void createLabelWithoutFunction() throws Exception {
+    LabelDefinitionInput fooInput = new LabelDefinitionInput();
+    fooInput.name = " Foo";
+    fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+    BatchLabelInput input = new BatchLabelInput();
+    input.create = ImmutableList.of(fooInput);
+
+    gApi.projects().name(allProjects.get()).labels(input);
+    assertThat(gApi.projects().name(allProjects.get()).label("Foo").get().function)
+        .isEqualTo(LabelFunction.NO_OP.getFunctionName());
+  }
+
+  @Test
+  public void cannotCreateLabelWithDeprecatedFunction() throws Exception {
+    for (LabelFunction deprecatedLabelFunction :
+        ImmutableList.of(
+            LabelFunction.ANY_WITH_BLOCK,
+            LabelFunction.MAX_NO_BLOCK,
+            LabelFunction.MAX_WITH_BLOCK)) {
+      LabelDefinitionInput fooInput = new LabelDefinitionInput();
+      fooInput.name = " Foo";
+      fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+      fooInput.function = deprecatedLabelFunction.getFunctionName();
+
+      BatchLabelInput input = new BatchLabelInput();
+      input.create = ImmutableList.of(fooInput);
+
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.projects().name(allProjects.get()).labels(input));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Function %s of label %s is deprecated. The function can only be set to %s. Use"
+                      + " submit requirements instead of label functions.",
+                  fooInput.function,
+                  fooInput.name,
+                  ImmutableList.of(
+                      LabelFunction.NO_BLOCK.getFunctionName(),
+                      LabelFunction.NO_OP.getFunctionName(),
+                      LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+    }
+  }
+
+  @Test
+  public void createLabels_emptyCopyCondition() throws Exception {
+    LabelDefinitionInput fooInput = new LabelDefinitionInput();
+    fooInput.name = " Foo";
+    fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+    fooInput.copyCondition = "";
+
+    BatchLabelInput input = new BatchLabelInput();
+    input.create = ImmutableList.of(fooInput);
+
+    gApi.projects().name(allProjects.get()).labels(input);
+    assertThat(gApi.projects().name(allProjects.get()).label("Foo").get().copyCondition).isNull();
+  }
+
+  @Test
   public void cannotSetCommitMessageOnLabelDefinitionInputForCreate() throws Exception {
     LabelDefinitionInput labelInput = new LabelDefinitionInput();
     labelInput.name = "Foo";
@@ -274,7 +336,7 @@
     configLabel("Bar", LabelFunction.NO_OP);
 
     LabelDefinitionInput fooUpdate = new LabelDefinitionInput();
-    fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName();
+    fooUpdate.function = LabelFunction.PATCH_SET_LOCK.getFunctionName();
     LabelDefinitionInput barUpdate = new LabelDefinitionInput();
     barUpdate.name = "Baz";
 
@@ -297,7 +359,7 @@
     configLabel("Bar", LabelFunction.NO_OP);
 
     LabelDefinitionInput fooUpdate = new LabelDefinitionInput();
-    fooUpdate.function = LabelFunction.MAX_WITH_BLOCK.getFunctionName();
+    fooUpdate.function = LabelFunction.PATCH_SET_LOCK.getFunctionName();
     LabelDefinitionInput barUpdate = new LabelDefinitionInput();
     barUpdate.name = "Baz";
 
@@ -315,6 +377,38 @@
   }
 
   @Test
+  public void cannotUpdateLabelToDeprecatedFunction() throws Exception {
+    for (LabelFunction deprecatedLabelFunction :
+        ImmutableList.of(
+            LabelFunction.ANY_WITH_BLOCK,
+            LabelFunction.MAX_NO_BLOCK,
+            LabelFunction.MAX_WITH_BLOCK)) {
+      LabelDefinitionInput labelInput = new LabelDefinitionInput();
+      labelInput.function = deprecatedLabelFunction.getFunctionName();
+
+      BatchLabelInput input = new BatchLabelInput();
+      input.update = ImmutableMap.of(LabelId.CODE_REVIEW, labelInput);
+
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () -> gApi.projects().name(allProjects.get()).labels(input));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Function %s of label %s is deprecated. The function can only be set to %s. Use"
+                      + " submit requirements instead of label functions.",
+                  labelInput.function,
+                  LabelId.CODE_REVIEW,
+                  ImmutableList.of(
+                      LabelFunction.NO_BLOCK.getFunctionName(),
+                      LabelFunction.NO_OP.getFunctionName(),
+                      LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+    }
+  }
+
+  @Test
   public void cannotSetCommitMessageOnLabelDefinitionInputForUpdate() throws Exception {
     LabelDefinitionInput labelInput = new LabelDefinitionInput();
     labelInput.commitMessage = "Update label";
@@ -336,7 +430,7 @@
 
     LabelDefinitionInput fooInput = new LabelDefinitionInput();
     fooInput.name = "Foo";
-    fooInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+    fooInput.function = LabelFunction.NO_OP.getFunctionName();
     fooInput.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
 
     BatchLabelInput input = new BatchLabelInput();
@@ -355,12 +449,12 @@
 
     LabelDefinitionInput fooCreateInput = new LabelDefinitionInput();
     fooCreateInput.name = "Foo";
-    fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+    fooCreateInput.function = LabelFunction.NO_BLOCK.getFunctionName();
     fooCreateInput.values =
         ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
 
     LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput();
-    fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName();
+    fooUpdateInput.function = LabelFunction.PATCH_SET_LOCK.getFunctionName();
 
     BatchLabelInput input = new BatchLabelInput();
     input.delete = ImmutableList.of("Foo");
@@ -395,12 +489,12 @@
   public void createAndUpdateLabel() throws Exception {
     LabelDefinitionInput fooCreateInput = new LabelDefinitionInput();
     fooCreateInput.name = "Foo";
-    fooCreateInput.function = LabelFunction.MAX_NO_BLOCK.getFunctionName();
+    fooCreateInput.function = LabelFunction.NO_OP.getFunctionName();
     fooCreateInput.values =
         ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
 
     LabelDefinitionInput fooUpdateInput = new LabelDefinitionInput();
-    fooUpdateInput.function = LabelFunction.ANY_WITH_BLOCK.getFunctionName();
+    fooUpdateInput.function = LabelFunction.PATCH_SET_LOCK.getFunctionName();
 
     BatchLabelInput input = new BatchLabelInput();
     input.create = ImmutableList.of(fooCreateInput);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
index 4760eb5..eca7bfb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
@@ -194,6 +194,35 @@
   }
 
   @Test
+  public void cannotUpdateToDeprecatedFunction() throws Exception {
+    for (LabelFunction deprecatedLabelFunction :
+        ImmutableList.of(
+            LabelFunction.ANY_WITH_BLOCK,
+            LabelFunction.MAX_NO_BLOCK,
+            LabelFunction.MAX_WITH_BLOCK)) {
+      LabelDefinitionInput input = new LabelDefinitionInput();
+      input.function = deprecatedLabelFunction.getFunctionName();
+
+      BadRequestException exception =
+          assertThrows(
+              BadRequestException.class,
+              () ->
+                  gApi.projects().name(allProjects.get()).label(LabelId.CODE_REVIEW).update(input));
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              String.format(
+                  "Function %s is deprecated. The function can only be set to %s. Use submit"
+                      + " requirements instead of label functions.",
+                  input.function,
+                  ImmutableList.of(
+                      LabelFunction.NO_BLOCK.getFunctionName(),
+                      LabelFunction.NO_OP.getFunctionName(),
+                      LabelFunction.PATCH_SET_LOCK.getFunctionName())));
+    }
+  }
+
+  @Test
   public void functionIsTrimmed() throws Exception {
     LabelDefinitionInput input = new LabelDefinitionInput();
     input.function = " " + LabelFunction.NO_OP.getFunctionName() + " ";
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 8ee0b9b..c019822 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -45,7 +45,9 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
+import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
@@ -59,19 +61,51 @@
   private static final ImmutableList<String> testTags =
       ImmutableList.of("tag-A", "tag-B", "tag-C", "tag-D", "tag-E", "tag-F", "tag-G", "tag-H");
 
-  private static final String SIGNED_ANNOTATION =
-      "annotation\n"
-          + "-----BEGIN PGP SIGNATURE-----\n"
-          + "Version: GnuPG v1\n"
-          + "\n"
-          + "iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
-          + "9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
-          + "htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
-          + "4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
-          + "IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
-          + "+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
-          + "=XFeC\n"
-          + "-----END PGP SIGNATURE-----";
+  private static final String ANNOTATION = "annotation";
+
+  private static final String SIGNED_ANNOTATION_PGP =
+      String.format(
+          """
+          %s
+          -----BEGIN PGP SIGNATURE-----
+          Version: GnuPG v1
+
+          iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa
+          9tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7
+          htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V
+          4ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG
+          IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY
+          +SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=
+          =XFeC
+          -----END PGP SIGNATURE-----
+          """,
+          ANNOTATION);
+
+  private static final String SIGNED_ANNOTATION_SSH =
+      String.format(
+          """
+          %s
+          -----BEGIN SSH SIGNATURE-----
+          U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBALgr/qW5dQLYVBZB/osBxO
+          TtD0aY+HbwhEap54yhqlmjE+XWhZNMc0u38Z+OYFOv5skOlB5oRVH/jqS1lFuGpOAfw/Vo
+          TPC/fhZMIX1Ec1VGV/7dHg11WPVknewA8joWP3222Ynox2mT1LbD+WwuxKUGoTSiprGHqa
+          mfjy77Lmrr3gJKDuKKOH8aVtfvP21PS9FMMV8ps8wtRAHZ53IgDMj4SSyYGeehtJjDZYMf
+          ritguHyKO8WvXlClm+tZtISJIWW9Ke4R1VcU3comh+fYPmovBgxVV+cJeMREHC5pSSFOgG
+          8w61C7BY9Uk0S/xsLEfb9PbTLapX34+CLr218xEcRU+ylVnQt1jm0NUGkgiju/NMqVHEQj
+          UCbZnlMBW/LOFRqQWXoQBy9mvqdYV5tloExmFVgi4mvyeM/eH+yKjiCo/+Muznbrfaexut
+          FZm/uZhM8aB142vM7zeDpD/cdPYlniLODOjrAnuSQmowhkTyQqDCvsmU7EcSwTAEGXiTCE
+          QQAAAANnaXQAAAAAAAAABnNoYTUxMgAAAZQAAAAMcnNhLXNoYTItNTEyAAABgC7W/YE8Zy
+          qyM9IUz3+BcAWc+1dp9lNuN2tzykqEx/Vk4fEiv547mu0xSeD2v6RMaMyMbJUnyBjZ929j
+          pBzWsYh6hBoYc2J+6hj7yrglmQ6up/vgPtrZlJ3ms2sz4B/BKX3VxFSzd1EBscOs3+KiCd
+          RlKP3fwlwyQ10oybfxxeeI+B0II22TAAZzMhYCiC6Fc7MYhmofrEYhQMD6fBRSr2lJWgNX
+          O9lL6gXLkecLT7dbzkjSNjdBFhF0T9FTz+2ZwzE3iqg2NsMdWzRnctvuqPWY/wxQauzGnd
+          zyLm8wAJ9ZTd9GmWJiWRddaNnUjaznAZ3Vh0eyLPRsZkcuLiWOkilMTOOnlXkLyqWRenDb
+          P3m4GusrrbvQo0FeT4BdF2wCIC4cxRx+7LcZ1IMFplzlcmEu6ZP9smS25FVMWMqqaGruSD
+          fgf+xSD+mpJ/ToObSyoNY0p3eZ+V2iGYrHHNJbgcj4F36aZXUYrJIhH9DgeCRSsDI+9N3X
+          CShEWktrOjnA7w==
+          -----END SSH SIGNATURE-----
+          """,
+          ANNOTATION);
 
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
@@ -212,6 +246,10 @@
     assertThat(result.ref).isEqualTo(R_TAGS + tag2.ref);
     assertThat(result.revision).isEqualTo(tag2.revision);
 
+    // Do the test as a non-admin user, since admins can always see all tags, even if read on all
+    // branches is blocked.
+    requestScopeOperations.setApiUser(user.id());
+
     List<TagInfo> tags = getTags().get();
     assertThat(tags).hasSize(2);
     assertThat(tags.get(0).ref).isEqualTo(R_TAGS + tag1.ref);
@@ -346,16 +384,99 @@
   }
 
   @Test
-  public void createSignedTagNotSupported() throws Exception {
+  public void createTagSignedWithSshKey() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_SIGNED_TAG).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
+
     TagInput input = new TagInput();
     input.ref = "test";
-    input.message = SIGNED_ANNOTATION;
+    input.message = SIGNED_ANNOTATION_SSH;
+    input.revision = projectOperations.project(project).getHead("master").name();
+
+    TagInfo tagInfo = tag(input.ref).create(input).get();
+    assertThat(tagInfo.ref).isEqualTo("refs/tags/" + input.ref);
+    assertThat(tagInfo.object).isEqualTo(input.revision);
+    assertThat(tagInfo.message).isEqualTo(ANNOTATION);
+    assertThat(tagInfo.tagger.name).isEqualTo(admin.fullName());
+    assertThat(tagInfo.tagger.email).isEqualTo(admin.email());
+    assertThat(tagInfo.created).isEqualTo(tagInfo.tagger.date);
+  }
+
+  @Test
+  public void cannotCeateTagSignedWithPgpKey() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_SIGNED_TAG).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.message = SIGNED_ANNOTATION_PGP;
+    input.revision = projectOperations.project(project).getHead("master").name();
+
     MethodNotAllowedException thrown =
         assertThrows(MethodNotAllowedException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(
+            String.format(
+                "Cannot create signed tag \"%s\": PGP signature not supported", R_TAGS + "test"));
+  }
+
+  @Test
+  public void createSignedTagNotAllowed() throws Exception {
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.message = SIGNED_ANNOTATION_SSH;
+    AuthException thrown = assertThrows(AuthException.class, () -> tag(input.ref).create(input));
     assertThat(thrown).hasMessageThat().contains("Cannot create signed tag \"" + R_TAGS + "test\"");
   }
 
   @Test
+  @UseClockStep
+  public void createTagWithDate() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.message = "annotated";
+    input.date = Timestamp.from(TimeUtil.now());
+
+    TagInfo tagInfo = tag(input.ref).create(input).get();
+    assertThat(tagInfo.message).isEqualTo(input.message);
+    assertThat(tagInfo.tagger.name).isEqualTo(admin.fullName());
+    assertThat(tagInfo.tagger.email).isEqualTo(admin.email());
+    assertThat(tagInfo.tagger.date).isEqualTo(input.date);
+    assertThat(tagInfo.created).isEqualTo(tagInfo.tagger.date);
+  }
+
+  @Test
+  public void createTagWithDateInTheFuture() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.CREATE_TAG).ref(R_TAGS + "*").group(REGISTERED_USERS))
+        .update();
+
+    TagInput input = new TagInput();
+    input.ref = "test";
+    input.message = "annotated";
+    input.date = Timestamp.valueOf("9999-12-31 23:59:59.999");
+
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> tag(input.ref).create(input));
+    assertThat(thrown).hasMessageThat().contains("date cannot be in the future");
+  }
+
+  @Test
   public void mismatchedInput() throws Exception {
     TagInput input = new TagInput();
     input.ref = "test";
diff --git a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java
index 8ed8857..dc93a44 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensitivityMigratorIT.java
@@ -181,11 +181,10 @@
 
       extIdNotes = externalIdNotesFactory.load(allUsersRepo);
       ExternalId extId =
-          externalIdFactory.create(
+          externalIdFactory.createWithEmail(
               externalIdKeyFactory.create(SCHEME_USERNAME, "JonDoe", true),
               accountId,
-              "test@email.com",
-              "w1m9Bg85GQ4hijLNxW+6xAfj4r9wyk9rzVQelIHxuQ");
+              "test@email.com");
       extIdNotes.upsert(extId);
       extIdNotes.commit(md);
 
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 4ab651d..a3b7d63 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -995,7 +995,7 @@
 
   @Test
   public void putDraft_humanInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
 
@@ -1008,20 +1008,6 @@
   }
 
   @Test
-  public void putDraft_robotInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentRobotCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-
-    DraftInput draft = CommentsUtil.newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
-    draft.inReplyTo = parentRobotCommentUuid;
-    String createdDraftUuid = addDraft(changeId, draft).id;
-    TestHumanComment actual =
-        changeOperations.change(changeId).draftComment(createdDraftUuid).get();
-    assertThat(actual.parentUuid()).hasValue(parentRobotCommentUuid);
-  }
-
-  @Test
   public void putDraft_idMismatch() throws Exception {
     String file = "file";
     PushOneCommit.Result r = createChange();
@@ -1068,7 +1054,7 @@
 
   @Test
   public void putDraft_invalidInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     DraftInput draft = CommentsUtil.newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
     draft.inReplyTo = "invalid";
     BadRequestException exception =
@@ -1091,7 +1077,7 @@
 
   @Test
   public void putDraft_updateInvalidInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
     CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
 
@@ -1106,7 +1092,7 @@
 
   @Test
   public void putDraft_updateHumanInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
     DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
@@ -1120,23 +1106,8 @@
   }
 
   @Test
-  public void putDraft_updateRobotInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentRobotCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-    DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
-    CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
-
-    DraftInput updateDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "bar");
-    updateDraftInput.inReplyTo = parentRobotCommentUuid;
-    updateDraft(changeId, updateDraftInput, originalDraft.id);
-    assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().parentUuid())
-        .hasValue(parentRobotCommentUuid);
-  }
-
-  @Test
   public void putDraft_updateTag() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     DraftInput originalDraftInput = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 0, "foo");
     CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
 
@@ -1151,7 +1122,7 @@
   @Test
   public void updatedDraftStillPointsToParentComment() throws Exception {
     Account.Id accountId = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
     String parentCommentUuid =
         changeOperations.change(changeId).patchset(patchsetId).newComment().create();
@@ -2123,31 +2094,8 @@
   }
 
   @Test
-  public void canCreateHumanCommentWithRobotCommentAsParentAndUnsetUnresolved() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentRobotCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-
-    CommentInput createdCommentInput = CommentsUtil.newComment(COMMIT_MSG, "comment reply");
-    createdCommentInput.inReplyTo = parentRobotCommentUuid;
-    createdCommentInput.unresolved = null;
-    CommentsUtil.addComments(gApi, changeId, createdCommentInput);
-
-    CommentInfo resultNewComment =
-        Iterables.getOnlyElement(
-            getPublishedCommentsAsList(changeId).stream()
-                .filter(c -> c.message.equals("comment reply"))
-                .collect(toImmutableSet()));
-
-    assertThat(resultNewComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
-
-    // Default unresolved is false.
-    assertThat(resultNewComment.unresolved).isFalse();
-  }
-
-  @Test
   public void canCreateHumanCommentWithHumanCommentAsParent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
 
@@ -2164,26 +2112,8 @@
   }
 
   @Test
-  public void canCreateHumanCommentWithRobotCommentAsParent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentRobotCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-
-    CommentInput createdCommentInput = CommentsUtil.newComment(COMMIT_MSG, "comment reply");
-    createdCommentInput.inReplyTo = parentRobotCommentUuid;
-    CommentsUtil.addComments(gApi, changeId, createdCommentInput);
-
-    CommentInfo resultNewComment =
-        Iterables.getOnlyElement(
-            getPublishedCommentsAsList(changeId).stream()
-                .filter(c -> c.message.equals("comment reply"))
-                .collect(toImmutableSet()));
-    assertThat(resultNewComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
-  }
-
-  @Test
   public void cannotCreateCommentWithInvalidInReplyTo() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, "comment 1 reply");
     comment.inReplyTo = "invalid";
diff --git a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
index e172153..97eda4a 100644
--- a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
@@ -74,6 +74,30 @@
   public void configOverride_defaultFeatureDisabled() {
     assertThat(experimentFeatures.isFeatureEnabled("enabledFeature")).isTrue();
     assertThat(experimentFeatures.isFeatureEnabled("UiFeature__patchset_comments")).isFalse();
-    assertThat(experimentFeatures.getEnabledExperimentFeatures()).containsExactly("enabledFeature");
+
+    ImmutableSet<String> expectedEnabledFeatures =
+        new ImmutableSet.Builder<String>()
+            .addAll(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES)
+            .add("enabledFeature")
+            .build();
+
+    assertThat(experimentFeatures.getEnabledExperimentFeatures())
+        .isEqualTo(expectedEnabledFeatures);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "experiments.disabled",
+      values = {"UiFeature__get_ai_prompt"})
+  public void configOverride_getAiPromptDisabled() {
+    assertThat(experimentFeatures.isFeatureEnabled("UiFeature__get_ai_prompt")).isFalse();
+
+    ImmutableSet<String> expectedEnabledFeatures =
+        ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES.stream()
+            .filter(feature -> !feature.equals("UiFeature__get_ai_prompt"))
+            .collect(ImmutableSet.toImmutableSet());
+
+    assertThat(experimentFeatures.getEnabledExperimentFeatures())
+        .isEqualTo(expectedEnabledFeatures);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
index 807fd5b..fd5845e 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
@@ -273,7 +273,7 @@
   }
 
   @Test
-  @GerritConfig(name = "change.maxComments", value = "8")
+  @GerritConfig(name = "change.maxComments", value = "4")
   public void countComments_limitNumberOfComments() throws Exception {
     when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of());
     // Start out with 1 change message.
@@ -287,14 +287,7 @@
     amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
     assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(1);
 
-    for (int i = 0; i < 2; ++i) {
-      // Adds 1 robot comment and 1 change message.
-      testCommentHelper.addRobotComment(
-          changeId,
-          TestCommentHelper.createRobotCommentInput(result.getChange().currentFilePaths().get(0)));
-    }
-    // We now have 1 comment, 2 robot comments, 5 change messages.
-
+    // We now have 1 comment and 3 change messages.
     DraftInput newDraft = testCommentHelper.newDraft(filePath, Side.REVISION, 1, COMMENT_TEXT);
     Exception thrown =
         assertThrows(
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index d88db52..792e3b1 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -50,7 +50,6 @@
 import com.google.gerrit.entities.NotifyConfig.Header;
 import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Permission;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
@@ -58,10 +57,8 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -1056,7 +1053,7 @@
 
   @Test
   public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
-    setWorkInProgressByDefault(project, InheritableBoolean.TRUE);
+    projectOperations.project(project).forUpdate().workInProgressByDefault().update();
     StagedPreChange spc = stagePreChange("refs/for/master");
     Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
     assertThat(sender).didNotSend();
@@ -1330,6 +1327,7 @@
   @Test
   public void deleteReviewerByEmailFromWipChange() throws Exception {
     StagedChange sc = stageWipChangeWithExtraReviewer();
+    requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).reviewer(StagedUsers.REVIEWER_BY_EMAIL).remove();
     assertThat(sender).didNotSend();
   }
@@ -2559,11 +2557,4 @@
     requestScopeOperations.setApiUser(sc.owner.id());
     gApi.changes().id(sc.changeId).setReadyForReview();
   }
-
-  private void setWorkInProgressByDefault(Project.NameKey p, InheritableBoolean v)
-      throws Exception {
-    ConfigInput input = new ConfigInput();
-    input.workInProgressByDefault = v;
-    gApi.projects().name(p.get()).config(input);
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index fb7a29a..35e6605 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -28,7 +28,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Streams;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -37,7 +36,6 @@
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
-import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -501,7 +499,7 @@
   }
 
   @Test
-  @GerritConfig(name = "change.maxComments", value = "9")
+  @GerritConfig(name = "change.maxComments", value = "7")
   public void limitNumberOfComments() throws Exception {
     // This change has 2 change messages and 2 comments.
     String changeId = createChangeWithReview();
@@ -514,12 +512,9 @@
     commentInput.line = 1;
     commentInput.message = "foo";
     commentInput.path = FILE_NAME;
-    RobotCommentInput robotCommentInput =
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(FILE_NAME);
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(commentInput));
-    reviewInput.robotComments = ImmutableMap.of(FILE_NAME, ImmutableList.of(robotCommentInput));
-    // Add 1 change message and another 2 comments. Total count is now 7, which is still OK.
+    // Add 1 change message and another comment. Total count is now 6, which is still OK.
     gApi.changes().id(changeId).current().review(reviewInput);
 
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
@@ -532,13 +527,13 @@
             "3) file comment");
     mailMessage.textContent(txt + textFooterForChange(changeInfo._number, ts));
 
-    ImmutableSet<CommentInfo> commentsBefore = getCommentsAndRobotComments(changeId);
-    // Should have 4 comments (and 3 change messages).
-    assertThat(commentsBefore).hasSize(4);
+    ImmutableSet<CommentInfo> commentsBefore = getComments(changeId);
+    // Should have 3 comments (and 2 change messages).
+    assertThat(commentsBefore).hasSize(3);
 
-    // The email adds 3 new comments (of which 1 is the change message).
+    // The email adds 2 new comments (of which 1 is the change message).
     mailProcessor.process(mailMessage.build());
-    ImmutableSet<CommentInfo> commentsAfter = getCommentsAndRobotComments(changeId);
+    ImmutableSet<CommentInfo> commentsAfter = getComments(changeId);
     assertThat(commentsAfter).isEqualTo(commentsBefore);
 
     assertNotifyTo(user);
@@ -602,11 +597,8 @@
         .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
   }
 
-  private ImmutableSet<CommentInfo> getCommentsAndRobotComments(String changeId)
-      throws RestApiException {
-    return Streams.concat(
-            gApi.changes().id(changeId).commentsRequest().get().values().stream(),
-            gApi.changes().id(changeId).robotComments().values().stream())
+  private ImmutableSet<CommentInfo> getComments(String changeId) throws RestApiException {
+    return gApi.changes().id(changeId).commentsRequest().get().values().stream()
         .flatMap(Collection::stream)
         .collect(toImmutableSet());
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
index 86b075c..0e13732 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
@@ -178,9 +178,9 @@
   public void defaultRefFilter_changeVisibilityIsAgnosticOfProvidedGroups() throws Exception {
     ExternalUser user = createUserInGroup("1", "it-department");
 
-    Change.Id changeOnMaster = changeOperations.newChange().project(project).create();
+    Change.Id changeOnMaster = changeOperations.newChange().project(project).createV1();
     Change.Id changeOnRefsMetaConfig =
-        changeOperations.newChange().project(project).branch("refs/meta/config").create();
+        changeOperations.newChange().project(project).branch("refs/meta/config").createV1();
     // Check that only the change on the default branch is visible
     assertThat(getVisibleRefNames(user))
         .containsExactly(
@@ -226,7 +226,7 @@
   public void changeVisibility_changeOnInvisibleBranchNotVisible() throws Exception {
     // Create a change that is not visible to members of 'externalGroup'
     Change.Id invisibleChange =
-        changeOperations.newChange().project(project).branch("refs/meta/config").create();
+        changeOperations.newChange().project(project).branch("refs/meta/config").createV1();
     ExternalUser user = createUserInGroup("1", "it-department");
     AuthException thrown =
         assertThrows(
@@ -241,7 +241,7 @@
 
   @Test
   public void changeVisibility_changeOnBranchVisibleToAnonymousIsVisible() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     ExternalUser user = createUserInGroup("1", "it-department");
     permissionBackend
         .user(user)
@@ -251,7 +251,7 @@
 
   @Test
   public void changeVisibility_changeOnBranchVisibleToRegisteredUsersIsVisible() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     ExternalUser user = createUserInGroup("1", "it-department");
     blockAnonymousRead();
     permissionBackend
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java
index 55c6464..4ad1caa 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java
@@ -85,9 +85,9 @@
   public void defaultRefFilter_changeVisibilityIsAgnosticOfProvidedGroups() throws Exception {
     GroupBackedUser user =
         new GroupBackedUser(ImmutableSet.of(ANONYMOUS_USERS, REGISTERED_USERS, externalGroup));
-    Change.Id changeOnMaster = changeOperations.newChange().project(project).create();
+    Change.Id changeOnMaster = changeOperations.newChange().project(project).createV1();
     Change.Id changeOnRefsMetaConfig =
-        changeOperations.newChange().project(project).branch("refs/meta/config").create();
+        changeOperations.newChange().project(project).branch("refs/meta/config").createV1();
     // Check that only the change on the default branch is visible
     assertThat(getVisibleRefNames(user))
         .containsExactly(
@@ -134,7 +134,7 @@
   public void changeVisibility_changeOnInvisibleBranchNotVisible() throws Exception {
     // Create a change that is not visible to members of 'externalGroup'
     Change.Id invisibleChange =
-        changeOperations.newChange().project(project).branch("refs/meta/config").create();
+        changeOperations.newChange().project(project).branch("refs/meta/config").createV1();
     GroupBackedUser user =
         new GroupBackedUser(ImmutableSet.of(ANONYMOUS_USERS, REGISTERED_USERS, externalGroup));
     AuthException thrown =
@@ -150,7 +150,7 @@
 
   @Test
   public void changeVisibility_changeOnBranchVisibleToAnonymousIsVisible() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     GroupBackedUser user =
         new GroupBackedUser(ImmutableSet.of(ANONYMOUS_USERS, REGISTERED_USERS, externalGroup));
     permissionBackend
@@ -161,7 +161,7 @@
 
   @Test
   public void changeVisibility_changeOnBranchVisibleToRegisteredUsersIsVisible() throws Exception {
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
     GroupBackedUser user =
         new GroupBackedUser(ImmutableSet.of(ANONYMOUS_USERS, REGISTERED_USERS, externalGroup));
     blockAnonymousRead();
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 8f0624c..2dfcb6b 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -576,15 +576,17 @@
 
   @Test
   public void byFileEdits_deletedContent_matching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 2\n", ""))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create("file:\"'^.*\\.txt',withDiffContaining='line 2'\"");
@@ -596,15 +598,17 @@
 
   @Test
   public void byFileEdits_deletedContent_nonMatching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 1\n", ""))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create("file:\"'^.*\\.txt',withDiffContaining='line 2'\"");
@@ -616,15 +620,17 @@
 
   @Test
   public void byFileEdits_addedContent_matching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT + "line 4\n")
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create("file:\"'^.*\\.txt',withDiffContaining='line 4'\"");
@@ -636,15 +642,17 @@
 
   @Test
   public void byFileEdits_addedContent_nonMatching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT + "line 4\n")
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create("file:\"'^.*\\.txt',withDiffContaining='line 5'\"");
@@ -656,15 +664,17 @@
 
   @Test
   public void byFileEdits_addedFile_matching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file("new_file.txt")
             .content("content of the new file")
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -677,15 +687,17 @@
 
   @Test
   public void byFileEdits_addedFile_nonMatching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file("new_file.txt")
             .content("content of the new file")
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -698,15 +710,17 @@
 
   @Test
   public void byFileEdits_modifiedContent_matching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create("file:\"'^.*\\.txt',withDiffContaining='three'\"");
@@ -718,15 +732,17 @@
 
   @Test
   public void byFileEdits_modifiedContent_nonMatching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create("file:\"'^.*\\.txt',withDiffContaining='ten'\"");
@@ -738,15 +754,17 @@
 
   @Test
   public void byFileEdits_modifiedContentPattern_matching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -759,15 +777,17 @@
 
   @Test
   public void byFileEdits_exactMatchingWithFilePath_matching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -780,15 +800,17 @@
 
   @Test
   public void byFileEdits_exactMatchingWithFilePath_nonMatching() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -801,15 +823,17 @@
 
   @Test
   public void byFileEdits_notMatchingWithFilePath() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     // commit edit only matches with files ending with ".java". Since our modified file name ends
     // with ".txt", the applicability expression will not match.
@@ -823,15 +847,17 @@
 
   @Test
   public void byFileEdits_escapeSingleQuotes() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line 'three' is modified\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -844,16 +870,18 @@
 
   @Test
   public void byFileEdits_doubleEscapeSingleQuote() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             // This will be written to the file as: line \'three\' is modified.
             .content(CONTENT.replace("line 3\n", "line \\'three\\' is modified\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     // Users can still provide back-slashes in regexes by escaping them.
     SubmitRequirementExpression exp =
@@ -867,15 +895,17 @@
 
   @Test
   public void byFileEdits_escapeDoubleQuotes() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line \"three\" is modified\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
@@ -888,15 +918,17 @@
 
   @Test
   public void byFileEdits_invalidSyntax() throws Exception {
-    Change.Id parent = changeOperations.newChange().file(FILE_NAME).content(CONTENT).create();
+    Change.Id parent =
+        changeOperations.newChange().project(project).file(FILE_NAME).content(CONTENT).createV1();
     Change.Id childId =
         changeOperations
             .newChange()
+            .project(project)
             .file(FILE_NAME)
             .content(CONTENT.replace("line 3\n", "line three\n"))
             .childOf()
             .change(parent)
-            .create();
+            .createV1();
 
     SubmitRequirementExpression exp =
         SubmitRequirementExpression.create(
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
index 9c7f5e9..ed30e94 100644
--- a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -263,7 +263,7 @@
   @Test
   public void hasChangedFilesPredicate() throws Exception {
     Change.Id changeId =
-        changeOperations.newChange().project(project).file("file").content("content").create();
+        changeOperations.newChange().project(project).file("file").content("content").createV1();
     changeOperations.change(changeId).newPatchset().file("file").content("new content").create();
 
     // can copy approval from patch-set 1 -> 2
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
index e21cb26..21ce66d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -140,7 +140,7 @@
   @Test
   public void requestRejectedIfInvalidDeadlineIsProvided_invalidValue() throws Exception {
     adminSshSession.exec("gerrit create-project --deadline invalid " + name("new"));
-    adminSshSession.assertFailure("Invalid deadline. Invalid value: invalid");
+    adminSshSession.assertFailure("Invalid deadline. Invalid time unit value: invalid");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
index 44fc4bb..9e563fc 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
@@ -26,6 +26,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
 import com.google.common.truth.Correspondence;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -33,10 +35,14 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -44,12 +50,16 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import com.google.gerrit.truth.OptionalSubject;
 import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.Map;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -64,7 +74,7 @@
 
   @Test
   public void changeCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
-    Change.Id numericChangeId = changeOperations.newChange().create();
+    Change.Id numericChangeId = changeOperations.newChange().createV1();
 
     ChangeInfo change = getChangeFromServer(numericChangeId);
     assertThat(change._number).isEqualTo(numericChangeId.get());
@@ -76,7 +86,7 @@
     Account.Id user = accountOperations.newAccount().create();
 
     requestScopeOperations.setApiUser(user);
-    Change.Id numericChangeId = changeOperations.newChange().create();
+    Change.Id numericChangeId = changeOperations.newChange().createV1();
 
     ChangeInfo change = getChangeFromServer(numericChangeId);
     assertThat(change._number).isEqualTo(numericChangeId.get());
@@ -84,8 +94,8 @@
 
   @Test
   public void twoChangesWithoutAnyParametersDoNotClash() {
-    Change.Id changeId1 = changeOperations.newChange().create();
-    Change.Id changeId2 = changeOperations.newChange().create();
+    Change.Id changeId1 = changeOperations.newChange().createV1();
+    Change.Id changeId2 = changeOperations.newChange().createV1();
 
     TestChange change1 = changeOperations.change(changeId1).get();
     TestChange change2 = changeOperations.change(changeId2).get();
@@ -95,8 +105,8 @@
 
   @Test
   public void twoSubsequentlyCreatedChangesDoNotDependOnEachOther() throws Exception {
-    Change.Id changeId1 = changeOperations.newChange().create();
-    Change.Id changeId2 = changeOperations.newChange().create();
+    Change.Id changeId1 = changeOperations.newChange().createV1();
+    Change.Id changeId2 = changeOperations.newChange().createV1();
 
     ChangeInfo change1 = getChangeFromServer(changeId1);
     ChangeInfo change2 = getChangeFromServer(changeId2);
@@ -114,7 +124,7 @@
 
   @Test
   public void createdChangeHasAtLeastOnePatchset() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThatMap(change.revisions).size().isAtLeast(1);
@@ -123,7 +133,7 @@
   @Test
   public void createdChangeIsInSpecifiedProject() throws Exception {
     Project.NameKey project = projectOperations.newProject().create();
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.project).isEqualTo(project.get());
@@ -132,7 +142,7 @@
   @Test
   public void changeCanBeCreatedInEmptyRepository() throws Exception {
     Project.NameKey project = projectOperations.newProject().noEmptyCommit().create();
-    Change.Id changeId = changeOperations.newChange().project(project).create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.project).isEqualTo(project.get());
@@ -142,17 +152,30 @@
   public void createdChangeHasSpecifiedTargetBranch() throws Exception {
     Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
     Change.Id changeId =
-        changeOperations.newChange().project(project).branch("test-branch").create();
+        changeOperations.newChange().project(project).branch("test-branch").createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.branch).isEqualTo("test-branch");
   }
 
   @Test
+  public void createdChangeHasSpecifiedCreationTimestamp() throws Exception {
+    Instant creationTimestamp = Instant.now();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).createdOn(creationTimestamp).createV1();
+
+    ChangeInfo change = getChangeFromServer(changeId);
+
+    // With NoteDb timestamps are rounded to seconds.
+    assertThat(change.created)
+        .isEqualTo(Timestamp.from(creationTimestamp.truncatedTo(ChronoUnit.SECONDS)));
+  }
+
+  @Test
   public void createdChangeHasDefaultGroupsByDefault() throws Exception {
     Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
     Change.Id changeId =
-        changeOperations.newChange().project(project).branch("test-branch").create();
+        changeOperations.newChange().project(project).branch("test-branch").createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(getGroups(project, changeId)).containsExactly(change.currentRevision);
@@ -168,7 +191,7 @@
             .project(project)
             .childOf()
             .tipOfBranch("refs/heads/test-branch")
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(getGroups(project, changeId)).containsExactly(change.currentRevision);
@@ -178,14 +201,14 @@
   public void createdChangeHasSameGroupsAsOpenParentChange() throws Exception {
     Project.NameKey project = projectOperations.newProject().create();
 
-    Change.Id parentChangeId = changeOperations.newChange().project(project).create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
 
     ChangeInfo parentChange = getChangeFromServer(parentChangeId);
     ImmutableList<String> parentGroups = getGroups(project, parentChangeId);
     assertThat(parentGroups).containsExactly(parentChange.currentRevision);
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).childOf().change(parentChangeId).create();
+        changeOperations.newChange().project(project).childOf().change(parentChangeId).createV1();
 
     assertThat(getGroups(project, changeId)).isEqualTo(parentGroups);
   }
@@ -194,12 +217,12 @@
   public void createdChangeHasDefaultGroupsIfClosedChangeIsSpecifiedAsParent() throws Exception {
     Project.NameKey project = projectOperations.newProject().create();
 
-    Change.Id parentChangeId = changeOperations.newChange().project(project).create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
     gApi.changes().id(parentChangeId.get()).current().review(ReviewInput.approve());
     gApi.changes().id(parentChangeId.get()).current().submit();
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).childOf().change(parentChangeId).create();
+        changeOperations.newChange().project(project).childOf().change(parentChangeId).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(getGroups(project, changeId)).containsExactly(change.currentRevision);
@@ -209,7 +232,7 @@
   public void createdChangeHasSameGroupsAsPatchSetOfOpenParentChange() throws Exception {
     Project.NameKey project = projectOperations.newProject().create();
 
-    Change.Id parentChangeId = changeOperations.newChange().project(project).create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
     TestPatchset parentPatchset = changeOperations.change(parentChangeId).currentPatchset().get();
     changeOperations.change(parentChangeId).newPatchset().create();
 
@@ -222,7 +245,7 @@
             .project(project)
             .childOf()
             .patchset(parentPatchset.patchsetId())
-            .create();
+            .createV1();
 
     assertThat(getGroups(project, changeId)).isEqualTo(parentGroups);
   }
@@ -232,7 +255,7 @@
       throws Exception {
     Project.NameKey project = projectOperations.newProject().create();
 
-    Change.Id parentChangeId = changeOperations.newChange().project(project).create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
     TestPatchset parentPatchset = changeOperations.change(parentChangeId).currentPatchset().get();
     changeOperations.change(parentChangeId).newPatchset().create();
     gApi.changes().id(parentChangeId.get()).current().review(ReviewInput.approve());
@@ -244,7 +267,7 @@
             .project(project)
             .childOf()
             .patchset(parentPatchset.patchsetId())
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(getGroups(project, changeId)).containsExactly(change.currentRevision);
@@ -255,12 +278,12 @@
     Project.NameKey project = projectOperations.newProject().create();
 
     // Currently, the easiest way to create a commit is by creating another change.
-    Change.Id anotherChangeId = changeOperations.newChange().project(project).create();
+    Change.Id anotherChangeId = changeOperations.newChange().project(project).createV1();
     ObjectId parentCommitId =
         changeOperations.change(anotherChangeId).currentPatchset().get().commitId();
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).childOf().commit(parentCommitId).create();
+        changeOperations.newChange().project(project).childOf().commit(parentCommitId).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(getGroups(project, changeId)).containsExactly(change.currentRevision);
@@ -271,7 +294,7 @@
     Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
     ObjectId parentCommitId = projectOperations.project(project).getHead("test-branch").getId();
     Change.Id changeId =
-        changeOperations.newChange().project(project).branch("test-branch").create();
+        changeOperations.newChange().project(project).branch("test-branch").createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -292,7 +315,7 @@
             .project(project)
             .childOf()
             .tipOfBranch("refs/heads/test-branch")
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -309,7 +332,12 @@
     Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
 
     Change.Id changeId =
-        changeOperations.newChange().project(project).childOf().tipOfBranch("test-branch").create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .childOf()
+            .tipOfBranch("test-branch")
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -327,15 +355,20 @@
         assertThrows(
             IllegalStateException.class,
             () ->
-                changeOperations.newChange().childOf().tipOfBranch("not-existing-branch").create());
+                changeOperations
+                    .newChange()
+                    .childOf()
+                    .tipOfBranch("not-existing-branch")
+                    .createV1());
     assertThat(exception).hasMessageThat().ignoringCase().contains("parent");
   }
 
   @Test
   public void createdChangeUsesSpecifiedChangeAsParent() throws Exception {
-    Change.Id parentChangeId = changeOperations.newChange().create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
 
-    Change.Id changeId = changeOperations.newChange().childOf().change(parentChangeId).create();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).childOf().change(parentChangeId).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -353,17 +386,22 @@
     IllegalStateException exception =
         assertThrows(
             IllegalStateException.class,
-            () -> changeOperations.newChange().childOf().change(Change.id(987654321)).create());
+            () -> changeOperations.newChange().childOf().change(Change.id(987654321)).createV1());
     assertThat(exception).hasMessageThat().ignoringCase().contains("parent");
   }
 
   @Test
   public void createdChangeUsesSpecifiedPatchsetAsParent() throws Exception {
-    Change.Id parentChangeId = changeOperations.newChange().create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
     TestPatchset parentPatchset = changeOperations.change(parentChangeId).currentPatchset().get();
 
     Change.Id changeId =
-        changeOperations.newChange().childOf().patchset(parentPatchset.patchsetId()).create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .childOf()
+            .patchset(parentPatchset.patchsetId())
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -384,13 +422,13 @@
                     .newChange()
                     .childOf()
                     .patchset(PatchSet.id(Change.id(987654321), 1))
-                    .create());
+                    .createV1());
     assertThat(exception).hasMessageThat().ignoringCase().contains("parent");
   }
 
   @Test
   public void specifiedParentPatchsetMustExist() {
-    Change.Id parentChangeId = changeOperations.newChange().create();
+    Change.Id parentChangeId = changeOperations.newChange().createV1();
 
     IllegalStateException exception =
         assertThrows(
@@ -400,18 +438,19 @@
                     .newChange()
                     .childOf()
                     .patchset(PatchSet.id(parentChangeId, 1000))
-                    .create());
+                    .createV1());
     assertThat(exception).hasMessageThat().ignoringCase().contains("parent");
   }
 
   @Test
   public void createdChangeUsesSpecifiedCommitAsParent() throws Exception {
     // Currently, the easiest way to create a commit is by creating another change.
-    Change.Id anotherChangeId = changeOperations.newChange().create();
+    Change.Id anotherChangeId = changeOperations.newChange().project(project).createV1();
     ObjectId parentCommitId =
         changeOperations.change(anotherChangeId).currentPatchset().get().commitId();
 
-    Change.Id changeId = changeOperations.newChange().childOf().commit(parentCommitId).create();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).childOf().commit(parentCommitId).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -432,23 +471,24 @@
                     .newChange()
                     .childOf()
                     .commit(ObjectId.fromString("0123456789012345678901234567890123456789"))
-                    .create());
+                    .createV1());
     assertThat(exception).hasMessageThat().ignoringCase().contains("parent");
   }
 
   @Test
   public void createdChangeUsesSpecifiedChangesInGivenOrderAsParents() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -466,18 +506,24 @@
   @Test
   public void createdChangeUsesMergedParentsAsBaseCommit() throws Exception {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Line 1").create();
+        changeOperations.newChange().project(project).file("file1").content("Line 1").createV1();
     Change.Id parent2ChangeId =
-        changeOperations.newChange().file("file2").content("Some other content").create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .file("file2")
+            .content("Some other content")
+            .createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
     BinaryResult file1Content = getFileContent(changeId, patchsetId, "file1");
@@ -489,9 +535,9 @@
   @Test
   public void mergeConflictsOfParentsAreReported() {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Content 1").create();
+        changeOperations.newChange().project(project).file("file1").content("Content 1").createV1();
     Change.Id parent2ChangeId =
-        changeOperations.newChange().file("file1").content("Content 2").create();
+        changeOperations.newChange().project(project).file("file1").content("Content 2").createV1();
 
     IllegalStateException exception =
         assertThrows(
@@ -499,11 +545,12 @@
             () ->
                 changeOperations
                     .newChange()
+                    .project(project)
                     .mergeOf()
                     .change(parent1ChangeId)
                     .and()
                     .change(parent2ChangeId)
-                    .create());
+                    .createV1());
 
     assertThat(exception).hasMessageThat().ignoringCase().contains("conflict");
   }
@@ -511,18 +558,19 @@
   @Test
   public void mergeConflictsCanBeAvoidedByUsingTheFirstParentAsBase() throws Exception {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Content 1").create();
+        changeOperations.newChange().project(project).file("file1").content("Content 1").createV1();
     Change.Id parent2ChangeId =
-        changeOperations.newChange().file("file1").content("Content 2").create();
+        changeOperations.newChange().project(project).file("file1").content("Content 2").createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOfButBaseOnFirst()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
     BinaryResult file1Content = getFileContent(changeId, patchsetId, "file1");
@@ -531,17 +579,18 @@
 
   @Test
   public void createdChangeHasAllParentsEvenWhenBasedOnFirst() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOfButBaseOnFirst()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -559,11 +608,11 @@
   @Test
   public void automaticMergeOfMoreThanTwoParentsIsNotPossible() {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Content 1").create();
+        changeOperations.newChange().file("file1").content("Content 1").createV1();
     Change.Id parent2ChangeId =
-        changeOperations.newChange().file("file2").content("Content 2").create();
+        changeOperations.newChange().file("file2").content("Content 2").createV1();
     Change.Id parent3ChangeId =
-        changeOperations.newChange().file("file3").content("Content 3").create();
+        changeOperations.newChange().file("file3").content("Content 3").createV1();
 
     IllegalStateException exception =
         assertThrows(
@@ -577,7 +626,7 @@
                     .change(parent2ChangeId)
                     .and()
                     .change(parent3ChangeId)
-                    .create());
+                    .createV1());
 
     assertThat(exception).hasMessageThat().ignoringCase().contains("conflict");
   }
@@ -585,22 +634,23 @@
   @Test
   public void createdChangeCanHaveMoreThanTwoParentsWhenBasedOnFirst() throws Exception {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Content 1").create();
+        changeOperations.newChange().project(project).file("file1").content("Content 1").createV1();
     Change.Id parent2ChangeId =
-        changeOperations.newChange().file("file2").content("Content 2").create();
+        changeOperations.newChange().project(project).file("file2").content("Content 2").createV1();
     Change.Id parent3ChangeId =
-        changeOperations.newChange().file("file3").content("Content 3").create();
+        changeOperations.newChange().project(project).file("file3").content("Content 3").createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOfButBaseOnFirst()
             .change(parent1ChangeId)
             .followedBy()
             .change(parent2ChangeId)
             .and()
             .change(parent3ChangeId)
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -622,20 +672,22 @@
     Change.Id parentChangeId =
         changeOperations
             .newChange()
+            .project(project)
             .file("file1")
             .content("Content 1")
             .file("file2")
             .content("Content 2")
-            .create();
+            .createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .childOf()
             .change(parentChangeId)
             .file("file1")
             .content("Different content")
-            .create();
+            .createV1();
 
     PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
     BinaryResult file1Content = getFileContent(changeId, patchsetId, "file1");
@@ -647,20 +699,21 @@
   @Test
   public void changeFromMergedParentsMayHaveAdditionalFileModifications() throws Exception {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Content 1").create();
+        changeOperations.newChange().project(project).file("file1").content("Content 1").createV1();
     Change.Id parent2ChangeId =
-        changeOperations.newChange().file("file2").content("Content 2").create();
+        changeOperations.newChange().project(project).file("file2").content("Content 2").createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
             .file("file1")
             .content("Different content")
-            .create();
+            .createV1();
 
     PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
     BinaryResult file1Content = getFileContent(changeId, patchsetId, "file1");
@@ -673,19 +726,20 @@
   public void changeBasedOnFirstOfMultipleParentsMayHaveAdditionalFileModifications()
       throws Exception {
     Change.Id parent1ChangeId =
-        changeOperations.newChange().file("file1").content("Content 1").create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+        changeOperations.newChange().project(project).file("file1").content("Content 1").createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
 
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOfButBaseOnFirst()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
             .file("file1")
             .content("Different content")
-            .create();
+            .createV1();
 
     PatchSet.Id patchsetId = changeOperations.change(changeId).currentPatchset().get().patchsetId();
     BinaryResult file1Content = getFileContent(changeId, patchsetId, "file1");
@@ -695,7 +749,7 @@
   @Test
   public void createdChangeHasSpecifiedOwner() throws Exception {
     Account.Id changeOwner = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
+    Change.Id changeId = changeOperations.newChange().owner(changeOwner).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
@@ -726,7 +780,7 @@
             .owner(changeOwner)
             .branch("test-branch")
             .project(project)
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
@@ -734,7 +788,7 @@
 
   @Test
   public void createdChangeHasOwnerAsAuthor() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     TestAccount changeOwner = accountOperations.account(Account.id(change.owner._accountId)).get();
@@ -753,7 +807,7 @@
             .fullname(changeOwnerName)
             .preferredEmail(changeOwnerEmail)
             .create();
-    Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
+    Change.Id changeId = changeOperations.newChange().owner(changeOwner).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
@@ -768,7 +822,7 @@
     String authorEmail = "author@example.com";
     Account.Id author =
         accountOperations.newAccount().fullname(authorName).preferredEmail(authorEmail).create();
-    Change.Id changeId = changeOperations.newChange().author(author).create();
+    Change.Id changeId = changeOperations.newChange().author(author).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.owner._accountId).isNotEqualTo(author.get());
@@ -780,7 +834,7 @@
   @Test
   public void createdChangeHasSpecifiedAuthorIdent() throws Exception {
     PersonIdent authorIdent = new PersonIdent("Author", "author@example.com");
-    Change.Id changeId = changeOperations.newChange().authorIdent(authorIdent).create();
+    Change.Id changeId = changeOperations.newChange().authorIdent(authorIdent).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     RevisionInfo revision = change.revisions.get(change.currentRevision);
@@ -796,7 +850,7 @@
     IllegalStateException exception =
         assertThrows(
             IllegalStateException.class,
-            () -> changeOperations.newChange().author(author).authorIdent(authorIdent).create());
+            () -> changeOperations.newChange().author(author).authorIdent(authorIdent).createV1());
     assertThat(exception)
         .hasMessageThat()
         .isEqualTo("author and authorIdent cannot be set together");
@@ -804,7 +858,7 @@
 
   @Test
   public void createdChangeHasOwnerAsCommitter() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     TestAccount changeOwner = accountOperations.account(Account.id(change.owner._accountId)).get();
@@ -823,7 +877,7 @@
             .fullname(changeOwnerName)
             .preferredEmail(changeOwnerEmail)
             .create();
-    Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
+    Change.Id changeId = changeOperations.newChange().owner(changeOwner).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
@@ -842,7 +896,7 @@
             .fullname(committerName)
             .preferredEmail(committerEmail)
             .create();
-    Change.Id changeId = changeOperations.newChange().committer(committer).create();
+    Change.Id changeId = changeOperations.newChange().committer(committer).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.owner._accountId).isNotEqualTo(committer.get());
@@ -854,7 +908,7 @@
   @Test
   public void createdChangeHasSpecifiedCommitterIdent() throws Exception {
     PersonIdent committerIdent = new PersonIdent("Committer", "committer@example.com");
-    Change.Id changeId = changeOperations.newChange().committerIdent(committerIdent).create();
+    Change.Id changeId = changeOperations.newChange().committerIdent(committerIdent).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     RevisionInfo revision = change.revisions.get(change.currentRevision);
@@ -875,7 +929,7 @@
                     .newChange()
                     .committer(committer)
                     .committerIdent(committerIdent)
-                    .create());
+                    .createV1());
     assertThat(exception)
         .hasMessageThat()
         .isEqualTo("committer and committerIdent cannot be set together");
@@ -883,7 +937,7 @@
 
   @Test
   public void createdChangeHasSpecifiedTopic() throws Exception {
-    Change.Id changeId = changeOperations.newChange().topic("test-topic").create();
+    Change.Id changeId = changeOperations.newChange().topic("test-topic").createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.topic).isEqualTo("test-topic");
@@ -892,7 +946,10 @@
   @Test
   public void createdChangeHasSpecifiedApprovals() throws Exception {
     Change.Id changeId =
-        changeOperations.newChange().approvals(ImmutableMap.of("Code-Review", (short) 1)).create();
+        changeOperations
+            .newChange()
+            .approvals(ImmutableMap.of("Code-Review", (short) 1))
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     assertThat(change.labels).hasSize(1);
@@ -906,7 +963,7 @@
         changeOperations
             .newChange()
             .commitMessage("Summary line\n\nDetailed description.")
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -916,7 +973,8 @@
   @Test
   public void changeCannotBeCreatedWithoutCommitMessage() {
     assertThrows(
-        IllegalStateException.class, () -> changeOperations.newChange().commitMessage("").create());
+        IllegalStateException.class,
+        () -> changeOperations.newChange().commitMessage("").createV1());
   }
 
   @Test
@@ -925,7 +983,7 @@
         changeOperations
             .newChange()
             .commitMessage("Summary line\n\nDetailed description.")
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -938,7 +996,7 @@
         changeOperations
             .newChange()
             .commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
@@ -957,7 +1015,7 @@
             .content("Line 1")
             .file("path/to/file2.txt")
             .content("Line one")
-            .create();
+            .createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
@@ -970,8 +1028,42 @@
   }
 
   @Test
+  public void createV2() {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestChange change = changeOperations.change(changeIdentifier).get();
+    assertThat(changeIdentifier.id())
+        .startsWith(change.project().get() + "~" + change.numericChangeId());
+  }
+
+  @Test
+  public void createAndGet() {
+    Instant creationTimestamp = Instant.now();
+    TestChange change =
+        changeOperations
+            .newChange()
+            .project(project)
+            .branch("master")
+            .owner(admin.id())
+            .createdOn(creationTimestamp)
+            .commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
+            .createAndGet();
+
+    assertThat(change.numericChangeId()).isNotNull();
+    assertThat(change.changeId()).isEqualTo("I0123456789012345678901234567890123456789");
+    assertThat(change.project()).isEqualTo(project);
+    assertThat(change.dest()).isEqualTo(BranchNameKey.create(project, "master"));
+    assertThat(change.project()).isEqualTo(project);
+    assertThat(change.owner()).isEqualTo(admin.id());
+    assertThat(change.subject()).isEqualTo("Summary line");
+
+    // With NoteDb timestamps are rounded to seconds.
+    assertThat(change.createdOn()).isEqualTo(creationTimestamp.truncatedTo(ChronoUnit.SECONDS));
+    assertThat(change.lastUpdatedOn()).isEqualTo(change.createdOn());
+  }
+
+  @Test
   public void existingChangeCanBeCheckedForExistence() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     boolean exists = changeOperations.change(changeId).exists();
 
@@ -995,7 +1087,7 @@
 
   @Test
   public void numericChangeIdOfExistingChangeCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     TestChange change = changeOperations.change(changeId).get();
     assertThat(change.numericChangeId()).isEqualTo(changeId);
@@ -1007,15 +1099,71 @@
         changeOperations
             .newChange()
             .commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
-            .create();
+            .createV1();
 
     TestChange change = changeOperations.change(changeId).get();
     assertThat(change.changeId()).isEqualTo("I0123456789012345678901234567890123456789");
   }
 
   @Test
+  public void projectOfExistingChangeCanBeRetrieved() {
+    Project.NameKey project = projectOperations.newProject().create();
+    Change.Id changeId = changeOperations.newChange().project(project).createV1();
+
+    TestChange change = changeOperations.change(changeId).get();
+    assertThat(change.project()).isEqualTo(project);
+  }
+
+  @Test
+  public void destOfExistingChangeCanBeRetrieved() {
+    Project.NameKey project = projectOperations.newProject().branches("foo").create();
+    Change.Id changeId = changeOperations.newChange().project(project).branch("foo").createV1();
+
+    TestChange change = changeOperations.change(changeId).get();
+    assertThat(change.dest()).isEqualTo(BranchNameKey.create(project, "foo"));
+  }
+
+  @Test
+  public void subjectOfExistingChangeCanBeRetrieved() {
+    Change.Id changeId = changeOperations.newChange().commitMessage("Foo\n\nBar Baz").createV1();
+
+    TestChange change = changeOperations.change(changeId).get();
+    assertThat(change.subject()).isEqualTo("Foo");
+  }
+
+  @Test
+  public void ownerOfExistingChangeCanBeRetrieved() {
+    Change.Id changeId = changeOperations.newChange().owner(admin.id()).createV1();
+
+    TestChange change = changeOperations.change(changeId).get();
+    assertThat(change.owner()).isEqualTo(admin.id());
+  }
+
+  @Test
+  public void createdOnOfExistingChangeCanBeRetrieved() {
+    Instant creationTimestamp = Instant.now();
+    Change.Id changeId = changeOperations.newChange().createdOn(creationTimestamp).createV1();
+
+    TestChange change = changeOperations.change(changeId).get();
+
+    // With NoteDb timestamps are rounded to seconds.
+    assertThat(change.createdOn()).isEqualTo(creationTimestamp.truncatedTo(ChronoUnit.SECONDS));
+  }
+
+  @Test
+  public void lastUpdatedOfExistingChangeCanBeRetrieved() {
+    Instant creationTimestamp = Instant.now();
+    Change.Id changeId = changeOperations.newChange().createdOn(creationTimestamp).createV1();
+
+    TestChange change = changeOperations.change(changeId).get();
+
+    // With NoteDb timestamps are rounded to seconds.
+    assertThat(change.lastUpdatedOn()).isEqualTo(creationTimestamp.truncatedTo(ChronoUnit.SECONDS));
+  }
+
+  @Test
   public void currentPatchsetOfExistingChangeCanBeRetrieved() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     TestPatchset patchset = changeOperations.change(changeId).currentPatchset().get();
 
@@ -1028,7 +1176,7 @@
 
   @Test
   public void earlierPatchsetOfExistingChangeCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     PatchSet.Id earlierPatchsetId =
         changeOperations.change(changeId).currentPatchset().get().patchsetId();
     PatchSet.Id currentPatchsetId = changeOperations.change(changeId).newPatchset().create();
@@ -1042,7 +1190,7 @@
 
   @Test
   public void newPatchsetCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     ChangeInfo unmodifiedChange = getChangeFromServer(changeId);
     int originalPatchsetCount = unmodifiedChange.revisions.size();
 
@@ -1056,7 +1204,7 @@
 
   @Test
   public void newPatchsetIsCopyOfPreviousPatchsetByDefault() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     PatchSet.Id patchsetId = changeOperations.change(changeId).newPatchset().create();
 
@@ -1068,7 +1216,7 @@
   @Test
   public void newPatchsetCanHaveDifferentUploader() throws Exception {
     Account.Id changeOwner = accountOperations.newAccount().create();
-    Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
+    Change.Id changeId = changeOperations.newChange().owner(changeOwner).createV1();
 
     ChangeInfo change = getChangeFromServer(changeId);
     RevisionInfo currentPatchsetRevision = change.revisions.get(change.currentRevision);
@@ -1088,7 +1236,7 @@
     String authorEmail = "author@example.com";
     Account.Id author =
         accountOperations.newAccount().fullname(authorName).preferredEmail(authorEmail).create();
-    Change.Id changeId = changeOperations.newChange().author(author).create();
+    Change.Id changeId = changeOperations.newChange().author(author).createV1();
     ChangeInfo change = getChangeFromServer(changeId);
     RevisionInfo revision = change.revisions.get(change.currentRevision);
     assertThat(revision.commit.author.name).isEqualTo(authorName);
@@ -1103,7 +1251,7 @@
 
   @Test
   public void createdPatchsetHasSpecifiedAuthor() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String authorName = "Author";
     String authorEmail = "author@example.com";
@@ -1120,7 +1268,7 @@
 
   @Test
   public void createdPatchsetHasSpecifiedAuthorIdent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     PersonIdent authorIdent = new PersonIdent("Author", "author@example.com");
     changeOperations.change(changeId).newPatchset().authorIdent(authorIdent).create();
@@ -1133,7 +1281,7 @@
 
   @Test
   public void patchsetCannotBeCreatedWithAuthorAndAuthorIdent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     Account.Id author = accountOperations.newAccount().create();
     PersonIdent authorIdent = new PersonIdent("Author", "author@example.com");
@@ -1163,7 +1311,7 @@
             .fullname(committerName)
             .preferredEmail(committerEmail)
             .create();
-    Change.Id changeId = changeOperations.newChange().committer(committer).create();
+    Change.Id changeId = changeOperations.newChange().committer(committer).createV1();
     ChangeInfo change = getChangeFromServer(changeId);
     RevisionInfo revision = change.revisions.get(change.currentRevision);
     assertThat(revision.commit.committer.name).isEqualTo(committerName);
@@ -1178,7 +1326,7 @@
 
   @Test
   public void createdPatchsetHasSpecifiedCommitter() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String committerName = "Committer";
     String committerEmail = "committer@example.com";
@@ -1199,7 +1347,7 @@
 
   @Test
   public void createdPatchsetHasSpecifiedCommitterIdent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     PersonIdent committerIdent = new PersonIdent("Committer", "committer@example.com");
     changeOperations.change(changeId).newPatchset().committerIdent(committerIdent).create();
@@ -1212,7 +1360,7 @@
 
   @Test
   public void patchsetCannotBeCreatedWithCommitterAndCommitterIdent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     Account.Id committer = accountOperations.newAccount().create();
     PersonIdent committerIdent = new PersonIdent("Committer", "committer@example.com");
@@ -1234,7 +1382,7 @@
 
   @Test
   public void newPatchsetCanHaveUpdatedCommitMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().commitMessage("Old message").create();
+    Change.Id changeId = changeOperations.newChange().commitMessage("Old message").createV1();
 
     changeOperations.change(changeId).newPatchset().commitMessage("New message").create();
 
@@ -1245,7 +1393,8 @@
 
   @Test
   public void updatedCommitMessageOfNewPatchsetAutomaticallyKeepsChangeId() throws Exception {
-    Change.Id numericChangeId = changeOperations.newChange().commitMessage("Old message").create();
+    Change.Id numericChangeId =
+        changeOperations.newChange().commitMessage("Old message").createV1();
     String changeId = changeOperations.change(numericChangeId).get().changeId();
 
     changeOperations.change(numericChangeId).newPatchset().commitMessage("New message").create();
@@ -1261,7 +1410,7 @@
         changeOperations
             .newChange()
             .commitMessage("Old message\n\nChange-Id: I1111111111111111111111111111111111111111")
-            .create();
+            .createV1();
 
     // Specifying another change-id is not an officially supported behavior of Gerrit but we might
     // need this for some test scenarios and hence we support it in the test API.
@@ -1286,7 +1435,7 @@
 
   @Test
   public void newPatchsetCanHaveReplacedFileContent() throws Exception {
-    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
+    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").createV1();
 
     PatchSet.Id patchsetId =
         changeOperations
@@ -1305,7 +1454,7 @@
 
   @Test
   public void newPatchsetCanHaveAdditionalFile() throws Exception {
-    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
+    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").createV1();
 
     PatchSet.Id patchsetId =
         changeOperations
@@ -1331,7 +1480,7 @@
             .content("Line 1")
             .file("file2")
             .content("Line one")
-            .create();
+            .createV1();
 
     changeOperations.change(changeId).newPatchset().file("file2").delete().create();
 
@@ -1349,7 +1498,7 @@
             .content("Line 1")
             .file("file2")
             .content("Line one")
-            .create();
+            .createV1();
 
     PatchSet.Id patchsetId =
         changeOperations
@@ -1377,7 +1526,7 @@
             .content("Some content")
             .file("file2")
             .content("Line 1\nLine 2\nLine 3\n")
-            .create();
+            .createV1();
     PatchSet.Id patchset1Id =
         changeOperations.change(changeId).currentPatchset().get().patchsetId();
 
@@ -1416,7 +1565,7 @@
             .content("Some content")
             .file("file2")
             .content("Line 1")
-            .create();
+            .createV1();
     PatchSet.Id patchset1Id =
         changeOperations.change(changeId).currentPatchset().get().patchsetId();
 
@@ -1461,7 +1610,7 @@
             .content("Some content")
             .file("file2")
             .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
+            .createV1();
     PatchSet.Id patchset1Id =
         changeOperations.change(changeId).currentPatchset().get().patchsetId();
 
@@ -1497,14 +1646,24 @@
 
   @Test
   public void newPatchsetCanHaveADifferentParent() throws Exception {
-    Change.Id originalParentChange = changeOperations.newChange().create();
-    Change.Id changeId =
-        changeOperations.newChange().childOf().change(originalParentChange).create();
-    Change.Id newParentChange = changeOperations.newChange().create();
+    Change.Id originalParentChange = changeOperations.newChange().project(project).createV1();
+    Change.Id changeIdentifier =
+        changeOperations
+            .newChange()
+            .project(project)
+            .childOf()
+            .change(originalParentChange)
+            .createV1();
+    Change.Id newParentChange = changeOperations.newChange().project(project).createV1();
 
-    changeOperations.change(changeId).newPatchset().parent().change(newParentChange).create();
+    changeOperations
+        .change(changeIdentifier)
+        .newPatchset()
+        .parent()
+        .change(newParentChange)
+        .create();
 
-    ChangeInfo change = getChangeFromServer(changeId);
+    ChangeInfo change = getChangeFromServer(changeIdentifier);
     CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
     ObjectId newParentCommitId =
         changeOperations.change(newParentChange).currentPatchset().get().commitId();
@@ -1517,18 +1676,19 @@
 
   @Test
   public void newPatchsetCanHaveDifferentParents() throws Exception {
-    Change.Id originalParent1Change = changeOperations.newChange().create();
-    Change.Id originalParent2Change = changeOperations.newChange().create();
+    Change.Id originalParent1Change = changeOperations.newChange().project(project).createV1();
+    Change.Id originalParent2Change = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(originalParent1Change)
             .and()
             .change(originalParent2Change)
-            .create();
-    Change.Id newParent1Change = changeOperations.newChange().create();
-    Change.Id newParent2Change = changeOperations.newChange().create();
+            .createV1();
+    Change.Id newParent1Change = changeOperations.newChange().project(project).createV1();
+    Change.Id newParent2Change = changeOperations.newChange().project(project).createV1();
 
     changeOperations
         .change(changeId)
@@ -1553,11 +1713,16 @@
 
   @Test
   public void newPatchsetCanHaveADifferentNumberOfParents() throws Exception {
-    Change.Id originalParentChange = changeOperations.newChange().create();
+    Change.Id originalParentChange = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
-        changeOperations.newChange().childOf().change(originalParentChange).create();
-    Change.Id newParent1Change = changeOperations.newChange().create();
-    Change.Id newParent2Change = changeOperations.newChange().create();
+        changeOperations
+            .newChange()
+            .project(project)
+            .childOf()
+            .change(originalParentChange)
+            .createV1();
+    Change.Id newParent1Change = changeOperations.newChange().project(project).createV1();
+    Change.Id newParent2Change = changeOperations.newChange().project(project).createV1();
 
     changeOperations
         .change(changeId)
@@ -1583,9 +1748,9 @@
   @Test
   public void newPatchsetKeepsFileContentsWithDifferentParent() throws Exception {
     Change.Id changeId =
-        changeOperations.newChange().file("file1").content("Actual change content").create();
+        changeOperations.newChange().file("file1").content("Actual change content").createV1();
     Change.Id newParentChange =
-        changeOperations.newChange().file("file1").content("Parent content").create();
+        changeOperations.newChange().file("file1").content("Parent content").createV1();
 
     changeOperations.change(changeId).newPatchset().parent().change(newParentChange).create();
 
@@ -1595,8 +1760,218 @@
   }
 
   @Test
+  public void newVoteCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    changeOperations.change(changeIdentifier).newVote().create();
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+  }
+
+  @Test
+  public void createdVoteHasSpecifiedUser_nonChangeOwner() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    TestVote testVote =
+        changeOperations.change(changeIdentifier).newVote().user(user.id()).create();
+    assertThat(testVote.userId()).isEqualTo(user.id());
+
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).accountId())
+        .isEqualTo(user.id());
+  }
+
+  @Test
+  public void createdVoteHasSpecifiedUser_changeOwner() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().owner(admin.id()).create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    TestVote testVote =
+        changeOperations.change(changeIdentifier).newVote().user(admin.id()).create();
+    assertThat(testVote.userId()).isEqualTo(admin.id());
+
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).accountId())
+        .isEqualTo(admin.id());
+  }
+
+  @Test
+  public void createdVoteHasSpecifiedLabelAndValue() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    // Create Foo-Review label.
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Approved", " 0", "No Vote", "-1", "Rejected");
+    gApi.projects().name(project.get()).label("Foo-Review").create(input);
+
+    TestVote testVote =
+        changeOperations.change(changeIdentifier).newVote().label("Foo-Review").value(1).create();
+    assertThat(testVote.label()).isEqualTo("Foo-Review");
+    assertThat(testVote.value()).isEqualTo(1);
+
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).label())
+        .isEqualTo("Foo-Review");
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).value())
+        .isEqualTo(1);
+  }
+
+  @Test
+  public void createdVoteHasValidNonZeroValueIfNotSpecified() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    // Create Foo-Review label.
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Approved", " 0", "No Vote", "-1", "Rejected");
+    gApi.projects().name(project.get()).label("Foo-Review").create(input);
+
+    changeOperations.change(changeIdentifier).newVote().label("Foo-Review").create();
+
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).label())
+        .isEqualTo("Foo-Review");
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).value())
+        .isIn(ImmutableList.of((short) -1, (short) 1));
+  }
+
+  @Test
+  public void createCodeReviewApproval() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    TestVote testVote =
+        changeOperations.change(changeIdentifier).newVote().codeReviewApproval().create();
+    assertThat(testVote.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(testVote.value()).isEqualTo(2);
+
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).label())
+        .isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).value())
+        .isEqualTo(2);
+  }
+
+  @Test
+  public void createCodeReviewVeto() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    TestPatchset currentPatchSet =
+        changeOperations.change(changeIdentifier).currentPatchset().get();
+    assertThat(getApprovals(changeIdentifier)).isEmpty();
+
+    TestVote testVote =
+        changeOperations.change(changeIdentifier).newVote().codeReviewVeto().create();
+    assertThat(testVote.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(testVote.value()).isEqualTo(-2);
+
+    ListMultimap<PatchSet.Id, PatchSetApproval> approvals = getApprovals(changeIdentifier);
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals).containsKey(currentPatchSet.patchsetId());
+    assertThat(approvals.get(currentPatchSet.patchsetId())).hasSize(1);
+
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).label())
+        .isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(Iterables.getOnlyElement(approvals.get(currentPatchSet.patchsetId())).value())
+        .isEqualTo(-2);
+  }
+
+  @Test
+  public void cannotCreateVoteForNonExistingUser() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    IllegalStateException exception =
+        assertThrows(
+            IllegalStateException.class,
+            () ->
+                changeOperations
+                    .change(changeIdentifier)
+                    .newVote()
+                    .user(Account.id(0))
+                    .value(1)
+                    .create());
+    assertThat(exception).hasMessageThat().contains("account 0 not found");
+  }
+
+  @Test
+  public void cannotCreateVoteForNonExistingLabel() throws Exception {
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().create();
+    IllegalStateException exception =
+        assertThrows(
+            IllegalStateException.class,
+            () ->
+                changeOperations
+                    .change(changeIdentifier)
+                    .newVote()
+                    .label("Non-Existing-Review")
+                    .value(1)
+                    .create());
+    assertThat(exception).hasMessageThat().contains("label Non-Existing-Review not found");
+  }
+
+  @Test
+  public void cannotCreateVoteForNonExistingValue() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+    ChangeIdentifier changeIdentifier = changeOperations.newChange().project(project).create();
+
+    // Create Foo-Review label.
+    LabelDefinitionInput input = new LabelDefinitionInput();
+    input.values = ImmutableMap.of("+1", "Approved", " 0", "No Vote", "-1", "Rejected");
+    gApi.projects().name(project.get()).label("Foo-Review").create(input);
+
+    IllegalStateException exception =
+        assertThrows(
+            IllegalStateException.class,
+            () ->
+                changeOperations
+                    .change(changeIdentifier)
+                    .newVote()
+                    .label("Foo-Review")
+                    .value(2)
+                    .create());
+    assertThat(exception).hasMessageThat().contains("value 2 not found for label Foo-Review");
+  }
+
+  @Test
   public void publishedCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String commentUuid = changeOperations.change(changeId).currentPatchset().newComment().create();
 
     TestHumanComment comment = changeOperations.change(changeId).comment(commentUuid).get();
@@ -1606,7 +1981,7 @@
 
   @Test
   public void retrievingDraftCommentAsPublishedCommentFails() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newDraftComment().create();
 
@@ -1616,7 +1991,7 @@
 
   @Test
   public void parentUuidOfPublishedCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
     String childCommentUuid =
@@ -1634,7 +2009,7 @@
 
   @Test
   public void tagOfPublishedCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String childCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().tag("tag").create();
 
@@ -1645,7 +2020,7 @@
 
   @Test
   public void unresolvedOfUnresolvedPublishedCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String childCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().unresolved().create();
 
@@ -1656,7 +2031,7 @@
 
   @Test
   public void unresolvedOfResolvedPublishedCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String childCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().resolved().create();
 
@@ -1667,7 +2042,7 @@
 
   @Test
   public void draftCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String commentUuid = changeOperations.change(changeId).currentPatchset().newComment().create();
 
     TestHumanComment comment = changeOperations.change(changeId).comment(commentUuid).get();
@@ -1677,7 +2052,7 @@
 
   @Test
   public void retrievingPublishedCommentAsDraftCommentFails() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String commentUuid = changeOperations.change(changeId).currentPatchset().newComment().create();
 
     assertThrows(
@@ -1686,7 +2061,7 @@
 
   @Test
   public void parentUuidOfDraftCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
     String childCommentUuid =
@@ -1703,34 +2078,12 @@
     OptionalSubject.assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
   }
 
-  @Test
-  public void robotCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
-    String commentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-
-    TestRobotComment comment = changeOperations.change(changeId).robotComment(commentUuid).get();
-
-    assertThat(comment.uuid()).isEqualTo(commentUuid);
-  }
-
-  @Test
-  public void parentUuidOfRobotCommentCanBeRetrieved() {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-    String childCommentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .parentUuid(parentCommentUuid)
-            .create();
-
-    TestRobotComment comment =
-        changeOperations.change(changeId).robotComment(childCommentUuid).get();
-
-    OptionalSubject.assertThat(comment.parentUuid()).value().isEqualTo(parentCommentUuid);
+  private ListMultimap<PatchSet.Id, PatchSetApproval> getApprovals(
+      ChangeIdentifier changeIdentifier) throws RestApiException {
+    ChangeInfo changeInfo = gApi.changes().id(changeIdentifier).get();
+    Project.NameKey projectName = Project.nameKey(changeInfo.project);
+    Change.Id changeId = Change.id(changeInfo._number);
+    return changeDataFactory.create(projectName, changeId).approvals();
   }
 
   private ChangeInfo getChangeFromServer(Change.Id changeId) throws RestApiException {
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/change/PatchsetOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/change/PatchsetOperationsImplTest.java
index 0406683..8ec20d0 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/change/PatchsetOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/change/PatchsetOperationsImplTest.java
@@ -17,8 +17,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.extensions.common.testing.CommentInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.CommentInfoSubject.assertThatList;
-import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThat;
-import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
 
 import com.google.common.truth.Correspondence;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -30,7 +28,6 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import com.google.inject.Inject;
@@ -51,7 +48,7 @@
 
   @Test
   public void commentCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid = changeOperations.change(changeId).currentPatchset().newComment().create();
     List<CommentInfo> comments = getCommentsFromServer(changeId);
@@ -60,7 +57,7 @@
 
   @Test
   public void commentCanBeCreatedOnOlderPatchset() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     PatchSet.Id previousPatchsetId =
         changeOperations.change(changeId).currentPatchset().get().patchsetId();
     changeOperations.change(changeId).newPatchset().create();
@@ -74,7 +71,7 @@
 
   @Test
   public void commentIsCreatedWithSpecifiedMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -90,7 +87,7 @@
 
   @Test
   public void commentCanBeCreatedWithEmptyMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().noMessage().create();
@@ -101,7 +98,7 @@
 
   @Test
   public void patchsetLevelCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().onPatchsetLevel().create();
@@ -112,7 +109,7 @@
 
   @Test
   public void fileCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
+    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").createV1();
 
     String commentUuid =
         changeOperations
@@ -135,7 +132,7 @@
             .newChange()
             .file("file1")
             .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -158,7 +155,7 @@
             .newChange()
             .file("file1")
             .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -183,7 +180,7 @@
 
   @Test
   public void commentCanBeCreatedOnPatchsetCommit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -201,7 +198,7 @@
 
   @Test
   public void commentCanBeCreatedOnParentCommit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().onParentCommit().create();
@@ -213,16 +210,17 @@
 
   @Test
   public void commentCanBeCreatedOnSecondParentCommit() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -239,8 +237,9 @@
 
   @Test
   public void commentCanBeCreatedOnNonExistingSecondParentCommit() throws Exception {
-    Change.Id parentChangeId = changeOperations.newChange().create();
-    Change.Id changeId = changeOperations.newChange().childOf().change(parentChangeId).create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).childOf().change(parentChangeId).createV1();
 
     String commentUuid =
         changeOperations
@@ -260,16 +259,17 @@
 
   @Test
   public void commentCanBeCreatedOnAutoMergeCommit() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -286,7 +286,7 @@
 
   @Test
   public void commentCanBeCreatedAsResolved() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().resolved().create();
@@ -297,7 +297,7 @@
 
   @Test
   public void commentCanBeCreatedAsUnresolved() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().unresolved().create();
@@ -308,7 +308,7 @@
 
   @Test
   public void replyToCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
 
@@ -326,7 +326,7 @@
 
   @Test
   public void tagCanBeAttachedToAComment() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -342,19 +342,19 @@
 
   @Test
   public void commentIsCreatedWithSpecifiedAuthor() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     Account.Id accountId = accountOperations.newAccount().create();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().author(accountId).create();
 
     CommentInfo comment = getCommentFromServer(changeId, commentUuid);
-    assertThat(comment).author().id().isEqualTo(accountId.get());
+    assertThat(comment).author().hasIdThat().isEqualTo(accountId.get());
   }
 
   @Test
   public void commentIsCreatedWithSpecifiedCreationTime() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     // Don't use nanos. NoteDb supports only second precision.
     Instant creationTime =
@@ -374,7 +374,7 @@
 
   @Test
   public void zoneOfCreationDateCanBeOmitted() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     // As we don't care about the exact time zone internally used as a default, do a relative test
     // so that we don't need to assert on exact instants in time. For a relative test, we need two
@@ -407,7 +407,7 @@
 
   @Test
   public void draftCommentCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newDraftComment().create();
@@ -418,7 +418,7 @@
 
   @Test
   public void draftCommentCanBeCreatedOnOlderPatchset() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     PatchSet.Id previousPatchsetId =
         changeOperations.change(changeId).currentPatchset().get().patchsetId();
     changeOperations.change(changeId).newPatchset().create();
@@ -432,7 +432,7 @@
 
   @Test
   public void draftCommentIsCreatedWithSpecifiedMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -448,7 +448,7 @@
 
   @Test
   public void draftCommentCanBeCreatedWithEmptyMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newDraftComment().noMessage().create();
@@ -459,7 +459,7 @@
 
   @Test
   public void draftPatchsetLevelCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -475,7 +475,7 @@
 
   @Test
   public void draftFileCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
+    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").createV1();
 
     String commentUuid =
         changeOperations
@@ -498,7 +498,7 @@
             .newChange()
             .file("file1")
             .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -521,7 +521,7 @@
             .newChange()
             .file("file1")
             .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -546,7 +546,7 @@
 
   @Test
   public void draftCommentCanBeCreatedOnPatchsetCommit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -564,7 +564,7 @@
 
   @Test
   public void draftCommentCanBeCreatedOnParentCommit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -581,16 +581,17 @@
 
   @Test
   public void draftCommentCanBeCreatedOnSecondParentCommit() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -607,8 +608,9 @@
 
   @Test
   public void draftCommentCanBeCreatedOnNonExistingSecondParentCommit() throws Exception {
-    Change.Id parentChangeId = changeOperations.newChange().create();
-    Change.Id changeId = changeOperations.newChange().childOf().change(parentChangeId).create();
+    Change.Id parentChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id changeId =
+        changeOperations.newChange().project(project).childOf().change(parentChangeId).createV1();
 
     String commentUuid =
         changeOperations
@@ -628,16 +630,17 @@
 
   @Test
   public void draftCommentCanBeCreatedOnAutoMergeCommit() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id parent1ChangeId = changeOperations.newChange().project(project).createV1();
+    Change.Id parent2ChangeId = changeOperations.newChange().project(project).createV1();
     Change.Id changeId =
         changeOperations
             .newChange()
+            .project(project)
             .mergeOf()
             .change(parent1ChangeId)
             .and()
             .change(parent2ChangeId)
-            .create();
+            .createV1();
 
     String commentUuid =
         changeOperations
@@ -654,7 +657,7 @@
 
   @Test
   public void draftCommentCanBeCreatedAsResolved() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newDraftComment().resolved().create();
@@ -665,7 +668,7 @@
 
   @Test
   public void draftCommentCanBeCreatedAsUnresolved() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations.change(changeId).currentPatchset().newDraftComment().unresolved().create();
@@ -676,7 +679,7 @@
 
   @Test
   public void draftReplyToDraftCommentCanBeCreated() {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newDraftComment().create();
 
@@ -693,7 +696,7 @@
 
   @Test
   public void draftReplyToPublishedCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     String parentCommentUuid =
         changeOperations.change(changeId).currentPatchset().newComment().create();
 
@@ -711,7 +714,7 @@
 
   @Test
   public void tagCanBeAttachedToADraftComment() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     String commentUuid =
         changeOperations
@@ -727,7 +730,7 @@
 
   @Test
   public void draftCommentIsCreatedWithSpecifiedAuthor() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
     Account.Id accountId = accountOperations.newAccount().create();
 
     String commentUuid =
@@ -749,7 +752,7 @@
 
   @Test
   public void draftCommentIsCreatedWithSpecifiedCreationTime() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     // Don't use nanos. NoteDb supports only second precision.
     Instant creationTime =
@@ -769,7 +772,7 @@
 
   @Test
   public void zoneOfCreationDateOfDraftCommentCanBeOmitted() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     // As we don't care about the exact time zone internally used as a default, do a relative test
     // so that we don't need to assert on exact instants in time. For a relative test, we need two
@@ -802,7 +805,7 @@
 
   @Test
   public void noDraftCommentsAreCreatedOnCreationOfPublishedComment() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     changeOperations.change(changeId).currentPatchset().newComment().create();
 
@@ -812,7 +815,7 @@
 
   @Test
   public void noPublishedCommentsAreCreatedOnCreationOfDraftComment() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
+    Change.Id changeId = changeOperations.newChange().createV1();
 
     changeOperations.change(changeId).currentPatchset().newDraftComment().create();
 
@@ -820,371 +823,10 @@
     assertThatList(comments).isEmpty();
   }
 
-  @Test
-  public void robotCommentCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-    List<RobotCommentInfo> robotComments = getRobotCommentsFromServerFromCurrentPatchset(changeId);
-    assertThatList(robotComments).comparingElementsUsing(hasUuid()).containsExactly(commentUuid);
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedOnOlderPatchset() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    PatchSet.Id previousPatchsetId =
-        changeOperations.change(changeId).currentPatchset().get().patchsetId();
-    changeOperations.change(changeId).newPatchset().create();
-
-    String commentUuid =
-        changeOperations.change(changeId).patchset(previousPatchsetId).newRobotComment().create();
-
-    CommentInfo comment = getRobotCommentFromServer(previousPatchsetId, commentUuid);
-    assertThat(comment).uuid().isEqualTo(commentUuid);
-  }
-
-  @Test
-  public void robotCommentIsCreatedWithSpecifiedMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .message("Test comment message")
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).message().isEqualTo("Test comment message");
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedWithEmptyMessage() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().noMessage().create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).message().isNull();
-  }
-
-  @Test
-  public void patchsetLevelRobotCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onPatchsetLevel()
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).path().isEqualTo(Patch.PATCHSET_LEVEL);
-  }
-
-  @Test
-  public void fileRobotCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onFileLevelOf("file1")
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).path().isEqualTo("file1");
-    assertThat(comment).line().isNull();
-    assertThat(comment).range().isNull();
-  }
-
-  @Test
-  public void lineRobotCommentCanBeCreated() throws Exception {
-    Change.Id changeId =
-        changeOperations
-            .newChange()
-            .file("file1")
-            .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onLine(3)
-            .ofFile("file1")
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).line().isEqualTo(3);
-    assertThat(comment).range().isNull();
-  }
-
-  @Test
-  public void rangeRobotCommentCanBeCreated() throws Exception {
-    Change.Id changeId =
-        changeOperations
-            .newChange()
-            .file("file1")
-            .content("Line 1\nLine 2\nLine 3\nLine 4\n")
-            .create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .fromLine(2)
-            .charOffset(4)
-            .toLine(3)
-            .charOffset(5)
-            .ofFile("file1")
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).range().startLine().isEqualTo(2);
-    assertThat(comment).range().startCharacter().isEqualTo(4);
-    assertThat(comment).range().endLine().isEqualTo(3);
-    assertThat(comment).range().endCharacter().isEqualTo(5);
-    // Line is automatically filled from specified range. It's the end line.
-    assertThat(comment).line().isEqualTo(3);
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedOnPatchsetCommit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onPatchsetCommit()
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    // Null is often used instead of Side.REVISION as Side.REVISION is the default.
-    assertThat(comment).side().isAnyOf(Side.REVISION, null);
-    assertThat(comment).parent().isNull();
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedOnParentCommit() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onParentCommit()
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).side().isEqualTo(Side.PARENT);
-    assertThat(comment).parent().isEqualTo(1);
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedOnSecondParentCommit() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
-    Change.Id changeId =
-        changeOperations
-            .newChange()
-            .mergeOf()
-            .change(parent1ChangeId)
-            .and()
-            .change(parent2ChangeId)
-            .create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onSecondParentCommit()
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).side().isEqualTo(Side.PARENT);
-    assertThat(comment).parent().isEqualTo(2);
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedOnNonExistingSecondParentCommit() throws Exception {
-    Change.Id parentChangeId = changeOperations.newChange().create();
-    Change.Id changeId = changeOperations.newChange().childOf().change(parentChangeId).create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onSecondParentCommit()
-            .create();
-
-    // We want to be able to create such invalid robot comments for testing purposes (e.g. testing
-    // error handling or resilience of an endpoint) and hence we need to allow such invalid robot
-    // comments in the test API.
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).side().isEqualTo(Side.PARENT);
-    assertThat(comment).parent().isEqualTo(2);
-  }
-
-  @Test
-  public void robotCommentCanBeCreatedOnAutoMergeCommit() throws Exception {
-    Change.Id parent1ChangeId = changeOperations.newChange().create();
-    Change.Id parent2ChangeId = changeOperations.newChange().create();
-    Change.Id changeId =
-        changeOperations
-            .newChange()
-            .mergeOf()
-            .change(parent1ChangeId)
-            .and()
-            .change(parent2ChangeId)
-            .create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .onAutoMergeCommit()
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).side().isEqualTo(Side.PARENT);
-    assertThat(comment).parent().isNull();
-  }
-
-  @Test
-  public void replyToRobotCommentCanBeCreated() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    String parentCommentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .parentUuid(parentCommentUuid)
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).inReplyTo().isEqualTo(parentCommentUuid);
-  }
-
-  @Test
-  public void tagCanBeAttachedToARobotComment() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .tag("my special tag")
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).tag().isEqualTo("my special tag");
-  }
-
-  @Test
-  public void robotCommentIsCreatedWithSpecifiedAuthor() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-    Account.Id accountId = accountOperations.newAccount().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .author(accountId)
-            .create();
-
-    CommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).author().id().isEqualTo(accountId.get());
-  }
-
-  @Test
-  public void robotCommentIsCreatedWithRobotId() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .robotId("robot-id")
-            .create();
-
-    RobotCommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).robotId().isEqualTo("robot-id");
-  }
-
-  @Test
-  public void robotCommentIsCreatedWithRobotRunId() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .robotId("robot-run-id")
-            .create();
-
-    RobotCommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).robotId().isEqualTo("robot-run-id");
-  }
-
-  @Test
-  public void robotCommentIsCreatedWithUrl() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations.change(changeId).currentPatchset().newRobotComment().url("url").create();
-
-    RobotCommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).url().isEqualTo("url");
-  }
-
-  @Test
-  public void robotCommentIsCreatedWithProperty() throws Exception {
-    Change.Id changeId = changeOperations.newChange().create();
-
-    String commentUuid =
-        changeOperations
-            .change(changeId)
-            .currentPatchset()
-            .newRobotComment()
-            .addProperty("key", "value")
-            .create();
-
-    RobotCommentInfo comment = getRobotCommentFromServerInCurrentPatchset(changeId, commentUuid);
-    assertThat(comment).properties().containsExactly("key", "value");
-  }
-
   private List<CommentInfo> getCommentsFromServer(Change.Id changeId) throws RestApiException {
     return gApi.changes().id(changeId.get()).commentsRequest().getAsList();
   }
 
-  private List<RobotCommentInfo> getRobotCommentsFromServerFromCurrentPatchset(Change.Id changeId)
-      throws RestApiException {
-    return gApi.changes().id(changeId.get()).current().robotCommentsAsList();
-  }
-
   private List<CommentInfo> getDraftCommentsFromServer(Change.Id changeId) throws RestApiException {
     return gApi.changes().id(changeId.get()).draftsAsList();
   }
@@ -1200,37 +842,6 @@
                     String.format("Comment %s not found on change %d", uuid, changeId.get())));
   }
 
-  private RobotCommentInfo getRobotCommentFromServerInCurrentPatchset(
-      Change.Id changeId, String uuid) throws RestApiException {
-    return gApi.changes().id(changeId.get()).current().robotCommentsAsList().stream()
-        .filter(comment -> comment.id.equals(uuid))
-        .findAny()
-        .orElseThrow(
-            () ->
-                new IllegalStateException(
-                    String.format(
-                        "Robot Comment %s not found on change %d on the latest patchset",
-                        uuid, changeId.get())));
-  }
-
-  private RobotCommentInfo getRobotCommentFromServer(PatchSet.Id patchsetId, String uuid)
-      throws RestApiException {
-    return gApi
-        .changes()
-        .id(patchsetId.changeId().toString())
-        .revision(patchsetId.getId().toString())
-        .robotCommentsAsList()
-        .stream()
-        .filter(comment -> comment.id.equals(uuid))
-        .findAny()
-        .orElseThrow(
-            () ->
-                new IllegalStateException(
-                    String.format(
-                        "Robot Comment %s not found on change %d on patchset %d",
-                        uuid, patchsetId.changeId().get(), patchsetId.get())));
-  }
-
   private CommentInfo getDraftCommentFromServer(Change.Id changeId, String uuid)
       throws RestApiException {
     return gApi.changes().id(changeId.get()).draftsAsList().stream()
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 8e2dfdd..97a36d2 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import com.google.inject.Inject;
@@ -700,6 +701,54 @@
   }
 
   @Test
+  public void setBooleanProjectConfigToTrue() {
+    projectOperations
+        .allProjectsForUpdate()
+        .useContributorAgreements(InheritableBoolean.TRUE)
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .stringValue("receive", /* subsection= */ null, "requireContributorAgreement")
+        .ignoringCase()
+        .isEqualTo(InheritableBoolean.TRUE.name());
+  }
+
+  @Test
+  public void setBooleanProjectConfigToFalse() {
+    projectOperations
+        .allProjectsForUpdate()
+        .useContributorAgreements(InheritableBoolean.FALSE)
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .stringValue("receive", /* subsection= */ null, "requireContributorAgreement")
+        .ignoringCase()
+        .isEqualTo(InheritableBoolean.FALSE.name());
+  }
+
+  @Test
+  public void setBooleanProjectConfigToInherit() {
+    projectOperations
+        .allProjectsForUpdate()
+        .useContributorAgreements(InheritableBoolean.INHERIT)
+        .update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .stringValue("receive", /* subsection= */ null, "requireContributorAgreement")
+        .isNull();
+  }
+
+  @Test
+  public void booleanProjectConfig() {
+    projectOperations.allProjectsForUpdate().useContributorAgreements().update();
+
+    assertThat(projectOperations.project(allProjects).getConfig())
+        .stringValue("receive", /* subsection= */ null, "requireContributorAgreement")
+        .ignoringCase()
+        .isEqualTo(InheritableBoolean.TRUE.name());
+  }
+
+  @Test
   public void updatingCapabilitiesNotAllowedForNonAllProjects() throws Exception {
     Project.NameKey key = projectOperations.newProject().create();
     assertThrows(
diff --git a/javatests/com/google/gerrit/common/data/BUILD b/javatests/com/google/gerrit/common/data/BUILD
index 154fd89..8f5f8fe 100644
--- a/javatests/com/google/gerrit/common/data/BUILD
+++ b/javatests/com/google/gerrit/common/data/BUILD
@@ -7,7 +7,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/entities/BUILD b/javatests/com/google/gerrit/entities/BUILD
index d93aa60..e3fa839 100644
--- a/javatests/com/google/gerrit/entities/BUILD
+++ b/javatests/com/google/gerrit/entities/BUILD
@@ -5,7 +5,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/entities",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib:jgit",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/entities/BranchTest.java b/javatests/com/google/gerrit/entities/BranchTest.java
index 0483ebc..d75418a 100644
--- a/javatests/com/google/gerrit/entities/BranchTest.java
+++ b/javatests/com/google/gerrit/entities/BranchTest.java
@@ -21,19 +21,19 @@
 public class BranchTest {
   @Test
   public void canonicalizeNameDuringConstruction() {
-    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "bar").branch())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo"), "bar").branch())
         .isEqualTo("refs/heads/bar");
-    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "refs/heads/bar").branch())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo"), "refs/heads/bar").branch())
         .isEqualTo("refs/heads/bar");
   }
 
   @Test
   public void idToString() {
-    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "bar").toString())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo"), "bar").toString())
         .isEqualTo("foo,refs/heads/bar");
-    assertThat(BranchNameKey.create(new Project.NameKey("foo bar"), "bar baz").toString())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo bar"), "bar baz").toString())
         .isEqualTo("foo+bar,refs/heads/bar+baz");
-    assertThat(BranchNameKey.create(new Project.NameKey("foo^bar"), "bar^baz").toString())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo^bar"), "bar^baz").toString())
         .isEqualTo("foo%5Ebar,refs/heads/bar%5Ebaz");
   }
 }
diff --git a/javatests/com/google/gerrit/entities/ChangeTest.java b/javatests/com/google/gerrit/entities/ChangeTest.java
index c75ad5a..99730a9 100644
--- a/javatests/com/google/gerrit/entities/ChangeTest.java
+++ b/javatests/com/google/gerrit/entities/ChangeTest.java
@@ -84,16 +84,6 @@
   }
 
   @Test
-  public void parseRobotCommentRefNames() {
-    assertRef(1, "refs/changes/01/1/robot-comments");
-    assertRef(1234, "refs/changes/34/1234/robot-comments");
-
-    assertNotRef("refs/changes/01/1/robot-comment");
-    assertNotRef("refs/changes/01/1/ROBOT-COMMENTS");
-    assertNotRef("refs/changes/01/1/1/robot-comments");
-  }
-
-  @Test
   public void parseStarredChangesRefNames() {
     assertAllUsersRef(1, "refs/starred-changes/01/1/1001");
     assertAllUsersRef(1234, "refs/starred-changes/34/1234/1001");
diff --git a/javatests/com/google/gerrit/entities/ProjectTest.java b/javatests/com/google/gerrit/entities/ProjectTest.java
index 94a6106..b3b3e0a 100644
--- a/javatests/com/google/gerrit/entities/ProjectTest.java
+++ b/javatests/com/google/gerrit/entities/ProjectTest.java
@@ -21,13 +21,12 @@
 public class ProjectTest {
   @Test
   public void parseId() {
-    assertThat(Project.NameKey.parse("foo")).isEqualTo(new Project.NameKey("foo"));
-    assertThat(Project.NameKey.parse("foo%20bar")).isEqualTo(new Project.NameKey("foo bar"));
-    assertThat(Project.NameKey.parse("foo+bar")).isEqualTo(new Project.NameKey("foo bar"));
-    assertThat(Project.NameKey.parse("foo%2fbar")).isEqualTo(new Project.NameKey("foo/bar"));
-    assertThat(Project.NameKey.parse("foo%2Fbar")).isEqualTo(new Project.NameKey("foo/bar"));
-    assertThat(Project.NameKey.parse("foo/a_bar%2B%2B"))
-        .isEqualTo(new Project.NameKey("foo/a_bar++"));
+    assertThat(Project.NameKey.parse("foo")).isEqualTo(Project.nameKey("foo"));
+    assertThat(Project.NameKey.parse("foo%20bar")).isEqualTo(Project.nameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo+bar")).isEqualTo(Project.nameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo%2fbar")).isEqualTo(Project.nameKey("foo/bar"));
+    assertThat(Project.NameKey.parse("foo%2Fbar")).isEqualTo(Project.nameKey("foo/bar"));
+    assertThat(Project.NameKey.parse("foo/a_bar%2B%2B")).isEqualTo(Project.nameKey("foo/a_bar++"));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/entities/RefNamesTest.java b/javatests/com/google/gerrit/entities/RefNamesTest.java
index 1990292..7bcbea4 100644
--- a/javatests/com/google/gerrit/entities/RefNamesTest.java
+++ b/javatests/com/google/gerrit/entities/RefNamesTest.java
@@ -47,10 +47,6 @@
     assertThat(changeMetaRef).isEqualTo("refs/changes/73/67473/meta");
     assertThat(RefNames.isNoteDbMetaRef(changeMetaRef)).isTrue();
 
-    String robotCommentsRef = RefNames.robotCommentsRef(changeId);
-    assertThat(robotCommentsRef).isEqualTo("refs/changes/73/67473/robot-comments");
-    assertThat(RefNames.isNoteDbMetaRef(robotCommentsRef)).isTrue();
-
     String changeRefPrefix = RefNames.changeRefPrefix(changeId);
     assertThat(changeRefPrefix).isEqualTo("refs/changes/73/67473/");
   }
diff --git a/javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java
index 12045b1..8bf20b5 100644
--- a/javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/AccountIdProtoConverterTest.java
@@ -16,12 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.proto.testing.SerializedClassSubject;
 import org.junit.Test;
 
 public class AccountIdProtoConverterTest {
@@ -47,10 +44,12 @@
     assertThat(convertedAccountId).isEqualTo(accountId);
   }
 
-  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
+  /**
+   * A record can have no extra fields but the ones set by the constructor, making it safe to
+   * convert.
+   */
   @Test
-  public void methodsExistAsExpected() {
-    assertThatSerializedClass(Account.Id.class)
-        .hasAutoValueMethods(ImmutableMap.of("id", int.class));
+  public void javaClassIsRecord() {
+    assertThat(Account.Id.class.isRecord()).isTrue();
   }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java
index c14e9261..9dfcdbe 100644
--- a/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/AccountInputProtoConverterTest.java
@@ -23,19 +23,24 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 import org.junit.Test;
 
 public class AccountInputProtoConverterTest {
   private final AccountInputProtoConverter accountInputProtoConverter =
       AccountInputProtoConverter.INSTANCE;
+  private final TokenInputProtoConverter tokenInputProtoConverter =
+      TokenInputProtoConverter.INSTANCE;
 
   private AccountInput createAccountInputInstance() {
+
     AccountInput accountInput = new AccountInput();
     accountInput.username = "test-username";
     accountInput.name = "test-name";
@@ -44,9 +49,22 @@
     accountInput.sshKey = "test-ssh-key";
     accountInput.httpPassword = "test-http-password";
     accountInput.groups = List.of("group1", "group2");
+    accountInput.tokens = getTokens();
     return accountInput;
   }
 
+  private List<AuthTokenInput> getTokens() {
+    AuthTokenInput token1 = new AuthTokenInput();
+    token1.id = "id1";
+    token1.token = "secret";
+
+    AuthTokenInput token2 = new AuthTokenInput();
+    token2.id = "another_token";
+    token2.token = "123456";
+
+    return List.of(token1, token2);
+  }
+
   private void assertAccountInputEquals(AccountInput expected, AccountInput actual) {
     assertThat(
             Objects.equals(expected.username, actual.username)
@@ -55,6 +73,7 @@
                 && Objects.equals(expected.email, actual.email)
                 && Objects.equals(expected.sshKey, actual.sshKey)
                 && Objects.equals(expected.httpPassword, actual.httpPassword)
+                && Objects.equals(expected.tokens, actual.tokens)
                 && Objects.equals(expected.groups, actual.groups))
         .isTrue();
   }
@@ -73,6 +92,10 @@
             .setSshKey("test-ssh-key")
             .setHttpPassword("test-http-password")
             .addAllGroups(ImmutableList.of("group1", "group2"))
+            .addAllTokens(
+                getTokens().stream()
+                    .map(tokenInputProtoConverter::toProto)
+                    .collect(Collectors.toList()))
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
@@ -100,6 +123,7 @@
                 .put("sshKey", String.class)
                 .put("httpPassword", String.class)
                 .put("groups", new TypeLiteral<List<String>>() {}.getType())
+                .put("tokens", new TypeLiteral<List<AuthTokenInput>>() {}.getType())
                 .build());
   }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/BUILD b/javatests/com/google/gerrit/entities/converter/BUILD
index 804397b..c1490fe 100644
--- a/javatests/com/google/gerrit/entities/converter/BUILD
+++ b/javatests/com/google/gerrit/entities/converter/BUILD
@@ -6,9 +6,14 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:converters",
+        "//java/com/google/gerrit/entities/converter:proto_converter",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/proto/testing",
         "//java/com/google/gerrit/server",
+        # We should include flow/converters here, so that they are covered
+        # by the SafeProtoConverterTest.
+        "//java/com/google/gerrit/server/flow/converters",
         "//lib:guava",
         "//lib:guava-testlib",
         "//lib:jgit",
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
index ffa99d9..e64a9e5 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeInputProtoConverterTest.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.auth.AuthTokenInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInput;
@@ -69,6 +70,10 @@
     accountInput.sshKey = "test-ssh-key";
     accountInput.httpPassword = "test-http-password";
     accountInput.groups = ImmutableList.of("test-group");
+    AuthTokenInput tokenInput = new AuthTokenInput();
+    tokenInput.id = "test-id";
+    tokenInput.token = "secret";
+    accountInput.tokens = ImmutableList.of(tokenInput);
     return accountInput;
   }
 
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
index 2efc11b..4f42f31 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
@@ -52,9 +52,13 @@
                 Optional.of(
                     PatchSet.Conflicts.create(
                         Optional.of(
+                            ObjectId.fromString("76ce7b5cb6f176fb64d9e4406f83fe5d658e1136")),
+                        Optional.of(
                             ObjectId.fromString("fc5fcbe1cf0b3e4c203ff4cb77c3912208174695")),
                         Optional.of(
                             ObjectId.fromString("f72b99f6898debf8d1ceeffb92faa66bb5cb7a9f")),
+                        Optional.of("resolve"),
+                        Optional.empty(),
                         true)))
             .build();
 
@@ -77,6 +81,9 @@
             .setBranch("refs/heads/master")
             .setConflicts(
                 Entities.Conflicts.newBuilder()
+                    .setBase(
+                        Entities.ObjectId.newBuilder()
+                            .setName("76ce7b5cb6f176fb64d9e4406f83fe5d658e1136"))
                     .setOurs(
                         Entities.ObjectId.newBuilder()
                             .setName("fc5fcbe1cf0b3e4c203ff4cb77c3912208174695"))
@@ -84,6 +91,7 @@
                         Entities.ObjectId.newBuilder()
                             .setName("f72b99f6898debf8d1ceeffb92faa66bb5cb7a9f"))
                     .setContainsConflicts(true)
+                    .setMergeStrategy("resolve")
                     .build())
             .build();
     assertThat(proto).isEqualTo(expectedProto);
@@ -134,9 +142,13 @@
                 Optional.of(
                     PatchSet.Conflicts.create(
                         Optional.of(
+                            ObjectId.fromString("76ce7b5cb6f176fb64d9e4406f83fe5d658e1136")),
+                        Optional.of(
                             ObjectId.fromString("fc5fcbe1cf0b3e4c203ff4cb77c3912208174695")),
                         Optional.of(
                             ObjectId.fromString("f72b99f6898debf8d1ceeffb92faa66bb5cb7a9f")),
+                        Optional.of("resolve"),
+                        Optional.empty(),
                         true)))
             .build();
 
diff --git a/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
index 2fa89a5..b8b2115 100644
--- a/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.GeneralProjectName;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
@@ -47,12 +48,14 @@
         projectNameKeyProtoConverter.fromProto(projectNameKeyProtoConverter.toProto(nameKey));
 
     assertThat(convertedNameKey).isEqualTo(nameKey);
+    assertThat(convertedNameKey.getClass()).isEqualTo(GeneralProjectName.class);
   }
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void fieldsExistAsExpected() {
-    assertThatSerializedClass(Project.NameKey.class)
+    assertThat(GeneralProjectName.class.isRecord()).isTrue();
+    assertThatSerializedClass(GeneralProjectName.class)
         .hasFields(ImmutableMap.of("name", String.class));
   }
 }
diff --git a/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java
index 0c8232d..ce7664a 100644
--- a/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/SafeProtoConverterTest.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package com.google.gerrit.entities.converter;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
@@ -147,7 +161,7 @@
         return null;
       }
       Object obj = construct(clz);
-      if (isSimple(clz)) {
+      if (isSimple(clz) || clz.isRecord()) {
         return obj;
       }
       for (Field field : clz.getDeclaredFields()) {
@@ -264,7 +278,8 @@
       Message.Builder res = defaultInstance.toBuilder();
       for (FieldDescriptor f : defaultInstance.getDescriptorForType().getFields()) {
         try {
-          if (f.getType().equals(FieldDescriptor.Type.MESSAGE)) {
+          if (f.getType().equals(FieldDescriptor.Type.MESSAGE)
+              && !f.getMessageType().toProto().getOptions().getMapEntry()) {
             if (f.isRepeated()) {
               res.addRepeatedField(
                   f,
diff --git a/javatests/com/google/gerrit/extensions/BUILD b/javatests/com/google/gerrit/extensions/BUILD
index 540ed95..dddf9e8 100644
--- a/javatests/com/google/gerrit/extensions/BUILD
+++ b/javatests/com/google/gerrit/extensions/BUILD
@@ -8,7 +8,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib/commons:text",
         "//lib/guice",
diff --git a/javatests/com/google/gerrit/git/BUILD b/javatests/com/google/gerrit/git/BUILD
index c2c9cce..6d42308 100644
--- a/javatests/com/google/gerrit/git/BUILD
+++ b/javatests/com/google/gerrit/git/BUILD
@@ -27,7 +27,7 @@
     ),
     deps = [
         "//java/com/google/gerrit/git",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index a69d60f..2b93045 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -8,7 +8,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/httpd",
         "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//javatests/com/google/gerrit/util/http/testutil",
         "//lib:gson",
         "//lib:guava",
diff --git a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
index 9be5548..a38973175 100644
--- a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -34,10 +34,12 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.AuthToken;
+import com.google.gerrit.server.account.AuthTokenAccessor;
+import com.google.gerrit.server.account.AuthTokenVerifier;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
-import com.google.gerrit.server.account.externalids.PasswordVerifier;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
@@ -47,6 +49,7 @@
 import java.time.Instant;
 import java.util.Base64;
 import java.util.Collection;
+import java.util.List;
 import java.util.Optional;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
@@ -87,13 +90,15 @@
 
   @Mock private WebSessionManager webSessionManager;
 
+  @Mock private AuthTokenAccessor tokenAccessor;
+
   private WebSession webSession;
   private FakeHttpServletRequest req;
   private HttpServletResponse res;
   private AuthResult authSuccessful;
   private ExternalIdFactory extIdFactory;
   private ExternalIdKeyFactory extIdKeyFactory;
-  private PasswordVerifier pwdVerifier;
+  private AuthTokenVerifier tokenVerifier;
   private AuthRequest.Factory authRequestFactory;
 
   @Before
@@ -104,7 +109,7 @@
     extIdKeyFactory = new ExternalIdKeyFactory(new ExternalIdKeyFactory.ConfigImpl(authConfig));
     extIdFactory = new ExternalIdFactoryNoteDbImpl(extIdKeyFactory, authConfig);
     authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
-    pwdVerifier = new PasswordVerifier(extIdKeyFactory, authConfig);
+    tokenVerifier = new AuthTokenVerifier(tokenAccessor);
 
     authSuccessful =
         new AuthResult(AUTH_ACCOUNT_ID, extIdKeyFactory.create("username", AUTH_USER), false);
@@ -116,6 +121,9 @@
     doReturn(webSessionValue)
         .when(webSessionManager)
         .createVal(any(), any(), eq(false), any(), any(), any());
+    doReturn(List.of(AuthToken.createWithPlainToken("token", AUTH_PASSWORD)))
+        .when(tokenAccessor)
+        .getValidTokens(AUTH_ACCOUNT_ID);
   }
 
   @Test
@@ -131,7 +139,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -153,7 +161,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -185,7 +193,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -208,7 +216,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
     assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
@@ -228,7 +236,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
     assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
@@ -250,7 +258,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -277,7 +285,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -327,7 +335,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -352,7 +360,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -367,7 +375,7 @@
   }
 
   private void initAccount(Collection<ExternalId> extIds) throws Exception {
-    Account account = Account.builder(Account.id(1000000), Instant.now()).build();
+    Account account = Account.builder(AUTH_ACCOUNT_ID, Instant.now()).build();
     AccountState accountState = AccountState.forAccount(account, extIds);
     doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
     doReturn(Optional.of(accountState)).when(accountCache).get(AUTH_ACCOUNT_ID);
@@ -388,7 +396,7 @@
             accountManager,
             authConfig,
             authRequestFactory,
-            pwdVerifier);
+            tokenVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
   }
@@ -410,6 +418,7 @@
     doReturn(webSession).when(webSessionItem).get();
   }
 
+  @Deprecated
   private ExternalId createUsernamePasswordExternalId() {
     return extIdFactory.createWithPassword(
         extIdKeyFactory.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
diff --git a/javatests/com/google/gerrit/httpd/ProjectOAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectOAuthFilterTest.java
new file mode 100644
index 0000000..3ce35de
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/ProjectOAuthFilterTest.java
@@ -0,0 +1,320 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Optional;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectOAuthFilterTest {
+  private static final Base64.Encoder B64_ENC = Base64.getEncoder();
+  private static final Account.Id AUTH_ACCOUNT_ID = Account.id(1000);
+  private static final String AUTH_USER = "johndoe";
+  private static final String AUTH_USER_B64 =
+      B64_ENC.encodeToString(AUTH_USER.getBytes(StandardCharsets.UTF_8));
+  private static final String OAUTH_TOKEN =
+      B64_ENC.encodeToString("{\"sub\":\"johndoe\"}".getBytes(UTF_8));
+  private static final String AUTH_USER_PWD_B64 =
+      B64_ENC.encodeToString(
+          String.format("%s:%s", AUTH_USER, OAUTH_TOKEN).getBytes(StandardCharsets.UTF_8));
+  private static final OAuthUserInfo OAUTH_USER_INFO =
+      new OAuthUserInfo(
+          String.format("oauth-test:%s", AUTH_USER),
+          AUTH_USER,
+          "johndoe@example.com",
+          "John Doe",
+          null);
+
+  @Mock private DynamicItem<WebSession> webSessionItem;
+
+  @Mock private AccountCache accountCache;
+
+  @Mock private AccountManager accountManager;
+
+  @Mock private AuthConfig authConfig;
+
+  @Mock private FilterChain chain;
+
+  @Captor private ArgumentCaptor<HttpServletResponse> filterResponseCaptor;
+
+  @Mock private IdentifiedUser.RequestFactory userRequestFactory;
+
+  @Mock private WebSessionManager webSessionManager;
+
+  @Mock private DynamicMap<OAuthLoginProvider> pluginsProvider;
+
+  private WebSession webSession;
+  private FakeHttpServletRequest req;
+  private HttpServletResponse res;
+  private AuthResult authSuccessful;
+  private ExternalIdKeyFactory extIdKeyFactory;
+  private AuthRequest.Factory authRequestFactory;
+  private Config gerritConfig = new Config();
+
+  @Before
+  public void setUp() throws Exception {
+    req = new FakeHttpServletRequest("gerrit.example.com", 80, "", "");
+    res = new FakeHttpServletResponse();
+
+    extIdKeyFactory = new ExternalIdKeyFactory(new ExternalIdKeyFactory.ConfigImpl(authConfig));
+    authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
+
+    authSuccessful =
+        new AuthResult(AUTH_ACCOUNT_ID, extIdKeyFactory.create("username", AUTH_USER), false);
+    doReturn(authSuccessful).when(accountManager).authenticate(any());
+
+    doReturn(
+            new OAuthLoginProvider() {
+              @Override
+              public OAuthUserInfo login(String username, String secret) throws IOException {
+                if (username.equals(AUTH_USER) && secret.equals(OAUTH_TOKEN)) {
+                  return OAUTH_USER_INFO;
+                }
+                throw new IOException("Authentication error");
+              }
+            })
+        .when(pluginsProvider)
+        .get(any(), any());
+    gerritConfig.setString("auth", null, "gitOAuthProvider", "oauth:test");
+  }
+
+  @Test
+  public void shouldAllowAnonymousRequest() throws Exception {
+    initAccount();
+    initWebSession();
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+
+    verify(chain).doFilter(eq(req), filterResponseCaptor.capture());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldRequestAuthenticationForBasicAuthRequest() throws Exception {
+    initAccount();
+    initWebSession();
+    req.addHeader("Authorization", "Basic " + AUTH_USER_B64);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+
+    verify(chain, never()).doFilter(any(), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
+    assertThat(res.getHeader("WWW-Authenticate")).contains("Basic realm=");
+  }
+
+  @Test
+  public void shouldRequestAuthenticationForValidBasicAuthRequest() throws Exception {
+    initAccount();
+    initWebSession();
+    req.addHeader("Authorization", "Basic " + AUTH_USER_PWD_B64);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldRequestAuthenticationForBearerTokenAuthRequest() throws Exception {
+    initAccount();
+    initWebSession();
+    req.addHeader("Authorization", "Bearer " + OAUTH_TOKEN);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldFailAuthenticationIfGerritUserDoesNotExist() throws Exception {
+    doReturn(Optional.empty()).when(accountCache).getByUsername(AUTH_USER);
+    initWebSession();
+    requestBasicAuth(req, OAUTH_TOKEN);
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
+  }
+
+  @Test
+  public void shouldAuthenticateSucessfullyAgainstRealm() throws Exception {
+    initAccount();
+    initWebSession();
+    requestBasicAuth(req, OAUTH_TOKEN);
+    res.setStatus(HttpServletResponse.SC_OK);
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+
+    verify(accountManager).authenticate(any());
+
+    verify(chain).doFilter(eq(req), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+  }
+
+  @Test
+  public void shouldFailedAuthenticationAgainstRealm() throws Exception {
+    initAccount();
+    initWebSession();
+    requestBasicAuth(req, OAUTH_TOKEN);
+
+    doThrow(new AccountException("Authentication error")).when(accountManager).authenticate(any());
+
+    ProjectOAuthFilter oAuthFilter =
+        new ProjectOAuthFilter(
+            webSessionItem,
+            pluginsProvider,
+            accountCache,
+            accountManager,
+            gerritConfig,
+            authRequestFactory);
+
+    oAuthFilter.init(null);
+    oAuthFilter.doFilter(req, res, chain);
+
+    verify(accountManager).authenticate(any());
+
+    verify(chain, never()).doFilter(any(), any());
+    assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
+  }
+
+  private void initAccount() throws Exception {
+    initAccount(ImmutableSet.of());
+  }
+
+  private void initAccount(Collection<ExternalId> extIds) throws Exception {
+    Account account = Account.builder(AUTH_ACCOUNT_ID, Instant.now()).build();
+    AccountState accountState = AccountState.forAccount(account, extIds);
+    doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
+  }
+
+  private void initWebSession() {
+    webSession =
+        new CacheBasedWebSession(
+            req, res, webSessionManager, authConfig, null, userRequestFactory, accountCache) {};
+    doReturn(webSession).when(webSessionItem).get();
+  }
+
+  private void requestBasicAuth(FakeHttpServletRequest fakeReq, String secret) {
+    fakeReq.addHeader(
+        "Authorization",
+        "Basic "
+            + B64_ENC.encodeToString((AUTH_USER + ":" + secret).getBytes(StandardCharsets.UTF_8)));
+  }
+}
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index 2f6d565..976a883 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -117,7 +117,7 @@
     String requestedPath = "/c/project/+/123/4..6";
     assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(4);
 
-    assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
+    assertThat(dynamicTemplateData(gerritApi, requestedPath, "", false))
         .containsAtLeast(
             "defaultChangeDetailHex", "9996394",
             "changeRequestsPath", "changes/project~123");
@@ -145,12 +145,41 @@
     String requestedPath = "/c/project/+/123";
     assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(0);
 
-    assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
+    assertThat(dynamicTemplateData(gerritApi, requestedPath, "", false))
         .containsAtLeast(
             "defaultChangeDetailHex", "1996394",
             "changeRequestsPath", "changes/project~123");
   }
 
+  @Test
+  public void usePreloadRestWithAsyncSRs() throws Exception {
+    Accounts accountsApi = mock(Accounts.class);
+    when(accountsApi.self()).thenThrow(new AuthException("user needs to be authenticated"));
+
+    Server serverApi = mock(Server.class);
+    when(serverApi.getVersion()).thenReturn("123");
+    when(serverApi.topMenus()).thenReturn(ImmutableList.of());
+    ServerInfo serverInfo = new ServerInfo();
+    serverInfo.defaultTheme = "my-default-theme";
+    when(serverApi.getInfo()).thenReturn(serverInfo);
+
+    Config configApi = mock(Config.class);
+    when(configApi.server()).thenReturn(serverApi);
+
+    GerritApi gerritApi = mock(GerritApi.class);
+    when(gerritApi.accounts()).thenReturn(accountsApi);
+    when(gerritApi.config()).thenReturn(configApi);
+
+    String requestedPath = "/c/project/+/123";
+    assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(0);
+
+    assertThat(dynamicTemplateData(gerritApi, requestedPath, "", true))
+        .containsAtLeast(
+            "defaultChangeDetailHex", "896394",
+            "submitRequirementsHex", "1900000",
+            "changeRequestsPath", "changes/project~123");
+  }
+
   private static SanitizedContent ordain(String s) {
     return UnsafeSanitizedContentOrdainer.ordainAsSafe(
         s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index d582b4b..6865fe2 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -60,7 +60,8 @@
     String testCdnPath = "bar-cdn";
     String testFaviconURL = "zaz-url";
 
-    assertThat(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES).isEmpty();
+    assertThat(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES)
+        .containsExactly(ExperimentFeaturesConstants.UI_FEATURE_GET_AI_PROMPT);
 
     org.eclipse.jgit.lib.Config serverConfig = new org.eclipse.jgit.lib.Config();
     serverConfig.setStringList(
diff --git a/javatests/com/google/gerrit/json/BUILD b/javatests/com/google/gerrit/json/BUILD
index a242b0e..edafe16 100644
--- a/javatests/com/google/gerrit/json/BUILD
+++ b/javatests/com/google/gerrit/json/BUILD
@@ -5,7 +5,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/json",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:gson",
         "//lib:guava",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/mail/BUILD b/javatests/com/google/gerrit/mail/BUILD
index 3d63844..e4af431 100644
--- a/javatests/com/google/gerrit/mail/BUILD
+++ b/javatests/com/google/gerrit/mail/BUILD
@@ -10,12 +10,13 @@
     deps = [
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/mail",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:gson",
         "//lib:guava-retrying",
         "//lib:jgit",
         "//lib:jgit-junit",
         "//lib/guice",
+        "//lib/log:log4j",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
diff --git a/javatests/com/google/gerrit/metrics/BUILD b/javatests/com/google/gerrit/metrics/BUILD
index 531280e..fc36b50 100644
--- a/javatests/com/google/gerrit/metrics/BUILD
+++ b/javatests/com/google/gerrit/metrics/BUILD
@@ -7,7 +7,6 @@
     tags = ["metrics"],
     deps = [
         "//java/com/google/gerrit/metrics",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/metrics/dropwizard/BUILD b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
index 42cf111..2dbe7f0 100644
--- a/javatests/com/google/gerrit/metrics/dropwizard/BUILD
+++ b/javatests/com/google/gerrit/metrics/dropwizard/BUILD
@@ -8,7 +8,7 @@
     deps = [
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/metrics/dropwizard",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib/mockito",
         "//lib/truth",
         "@dropwizard-core//jar",
diff --git a/javatests/com/google/gerrit/metrics/proc/BUILD b/javatests/com/google/gerrit/metrics/proc/BUILD
index 7d8af90..94db828 100644
--- a/javatests/com/google/gerrit/metrics/proc/BUILD
+++ b/javatests/com/google/gerrit/metrics/proc/BUILD
@@ -9,9 +9,10 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/metrics/dropwizard",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib/dropwizard:dropwizard-core",
         "//lib/guice",
+        "//lib/mockito",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/pgm/http/jetty/ProjectQoSFilterTest.java b/javatests/com/google/gerrit/pgm/http/jetty/ProjectQoSFilterTest.java
index dcbcc76..23c9724 100644
--- a/javatests/com/google/gerrit/pgm/http/jetty/ProjectQoSFilterTest.java
+++ b/javatests/com/google/gerrit/pgm/http/jetty/ProjectQoSFilterTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.http.jetty;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static org.mockito.Mockito.when;
 
 import com.google.gerrit.server.CurrentUser;
@@ -65,6 +66,7 @@
           try {
             listener.onComplete(asyncEvent);
           } catch (Exception e) {
+            assertWithMessage("should never happen").fail();
           }
         }
       }.run();
@@ -89,6 +91,7 @@
           try {
             listener.onTimeout(asyncEvent);
           } catch (Exception e) {
+            assertWithMessage("should never happen").fail();
           }
         }
       }.run();
@@ -114,6 +117,7 @@
           try {
             listener.onError(asyncEvent);
           } catch (Exception e) {
+            assertWithMessage("should never happen").fail();
           }
         }
       }.run();
diff --git a/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java b/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java
index b3f59cc..4d60eef 100644
--- a/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java
+++ b/javatests/com/google/gerrit/pgm/util/LogFileManagerTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.server.config.LogConfig;
 import com.google.gerrit.server.config.SitePaths;
 import java.nio.file.Path;
 import java.time.Instant;
@@ -45,7 +46,8 @@
             "sshd_log",
             "httpd_log");
 
-    LogFileManager manager = new LogFileManager(new SitePaths(Path.of("/gerrit")), new Config());
+    LogFileManager manager =
+        new LogFileManager(new SitePaths(Path.of("/gerrit")), new LogConfig(new Config()));
     Instant expected = Instant.parse("2024-01-01T00:00:00.00Z");
     for (String filename : filenamesWithDate) {
       assertThat(manager.getDateFromFilename(Path.of(filename)).get()).isEqualTo(expected);
diff --git a/javatests/com/google/gerrit/prettify/BUILD b/javatests/com/google/gerrit/prettify/BUILD
index 0eb7cee..cde28d6 100644
--- a/javatests/com/google/gerrit/prettify/BUILD
+++ b/javatests/com/google/gerrit/prettify/BUILD
@@ -6,7 +6,7 @@
     deps = [
         "//java/com/google/gerrit/prettify:server",
         "//java/com/google/gerrit/prettify/common/testing",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index fd8ddd2..7cc40fb 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -39,6 +39,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/entities/converter:converters",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
diff --git a/javatests/com/google/gerrit/server/account/AuthTokenCacheTest.java b/javatests/com/google/gerrit/server/account/AuthTokenCacheTest.java
new file mode 100644
index 0000000..df7791a
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AuthTokenCacheTest.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+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 AuthTokenCacheTest {
+  private static final Account.Id ACCOUNT_ID = Account.id(1);
+  private static final String PWD = "secret";
+
+  private AuthTokenCache.Loader cacheLoader;
+
+  @Mock private DirectAuthTokenAccessor tokenAccessor;
+
+  @Before
+  public void setUp() throws Exception {
+    doReturn(ImmutableList.of(AuthToken.createWithPlainToken("token", PWD)))
+        .when(tokenAccessor)
+        .getTokens(ACCOUNT_ID);
+    cacheLoader = new AuthTokenCache.Loader(tokenAccessor);
+  }
+
+  @Test
+  public void loadTokenFromAccount() throws Exception {
+    ImmutableList<AuthToken> tokens = cacheLoader.load(ACCOUNT_ID);
+    assertThat(HashedPassword.decode(tokens.get(0).hashedToken()).checkPassword(PWD)).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/AuthTokenVerifierTest.java b/javatests/com/google/gerrit/server/account/AuthTokenVerifierTest.java
new file mode 100644
index 0000000..eb8d6e7
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AuthTokenVerifierTest.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Mockito.doReturn;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Optional;
+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 AuthTokenVerifierTest {
+  private static final Account.Id ACCOUNT_ID = Account.id(1);
+  private AuthTokenVerifier tokenVerifier;
+  private LoadingCache<Account.Id, List<AuthToken>> cache;
+  @Mock AuthTokenCache.Loader loader;
+
+  @Before
+  public void setUp() throws Exception {
+    ImmutableList<AuthToken> tokens =
+        ImmutableList.of(
+            AuthToken.createWithPlainToken("id1", "hashedToken"),
+            AuthToken.createWithPlainToken("id2", "another_Token"),
+            AuthToken.createWithPlainToken(
+                "id3", "tokenWithLifetime", Optional.of(Instant.now().plus(1, ChronoUnit.DAYS))),
+            AuthToken.createWithPlainToken(
+                "id4",
+                "tokenWithExpiredLifetime",
+                Optional.of(Instant.now().minus(1, ChronoUnit.DAYS))));
+    doReturn(tokens).when(loader).load(ACCOUNT_ID);
+    cache = CacheBuilder.newBuilder().build(loader);
+    AuthTokenCache authTokenCache = new AuthTokenCache(cache);
+    AuthTokenAccessor tokenAccessor = new CachingAuthTokenAccessor(authTokenCache, null);
+    tokenVerifier = new AuthTokenVerifier(tokenAccessor);
+  }
+
+  @Test
+  public void checkTokenWithoutLimitedLifetime() {
+    assertThat(tokenVerifier.checkToken(ACCOUNT_ID, "hashedToken")).isTrue();
+    assertThat(tokenVerifier.checkToken(ACCOUNT_ID, "another_Token")).isTrue();
+    assertThat(tokenVerifier.checkToken(ACCOUNT_ID, "invalid")).isFalse();
+    assertThat(tokenVerifier.checkToken(ACCOUNT_ID, "another_token")).isFalse();
+  }
+
+  @Test
+  public void assertThatTokensSucceedAuthenticationWithinLifetime() {
+    assertThat(tokenVerifier.checkToken(ACCOUNT_ID, "tokenWithLifetime")).isTrue();
+  }
+
+  @Test
+  public void assertThatExpiredTokensFailAuthentication() {
+    assertThat(tokenVerifier.checkToken(ACCOUNT_ID, "tokenWithExpiredLifetime")).isFalse();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/DirectAuthTokenAccessorTest.java b/javatests/com/google/gerrit/server/account/DirectAuthTokenAccessorTest.java
new file mode 100644
index 0000000..ded75e0
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/DirectAuthTokenAccessorTest.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import java.time.Instant;
+import java.util.List;
+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 DirectAuthTokenAccessorTest {
+  private static final Account.Id ACCOUNT_ID = Account.id(1);
+  private Account account;
+  @Mock private VersionedAuthTokens versionedAuthTokens;
+  @Mock private VersionedAuthTokens.Factory authTokenFactory;
+  @Mock private AccountCache accountCache;
+  private HttpPasswordFallbackAuthTokenAccessor tokenAccessor;
+  private ImmutableList<AuthToken> tokens;
+
+  @Before
+  public void setUp() throws Exception {
+    tokenAccessor =
+        new HttpPasswordFallbackAuthTokenAccessor(
+            accountCache,
+            new DirectAuthTokenAccessor(null, authTokenFactory, null, null),
+            null,
+            null,
+            null,
+            null,
+            null,
+            null);
+    tokens =
+        ImmutableList.of(
+            AuthToken.createWithPlainToken("id1", "hashedToken"),
+            AuthToken.createWithPlainToken("id2", "another_Token"));
+    doReturn(versionedAuthTokens).when(authTokenFactory).create(ACCOUNT_ID);
+    doReturn(versionedAuthTokens).when(versionedAuthTokens).load();
+    doReturn(tokens).when(versionedAuthTokens).getTokens();
+    account =
+        Account.builder(ACCOUNT_ID, Instant.EPOCH)
+            .setFullName("foo bar")
+            .setDisplayName("foo")
+            .setActive(true)
+            .setMetaId("dead..beef")
+            .setUniqueTag("dead..beef..tag")
+            .setStatus("OOO")
+            .setPreferredEmail("foo@bar.tld")
+            .build();
+    ;
+    doReturn(
+            AccountState.forAccount(
+                account,
+                ImmutableSet.of(
+                    ExternalId.create(
+                        ExternalId.Key.create(ExternalId.SCHEME_USERNAME, "foo", false),
+                        ACCOUNT_ID,
+                        null,
+                        "secret",
+                        null))))
+        .when(accountCache)
+        .getEvenIfMissing(ACCOUNT_ID);
+  }
+
+  @Test
+  public void getTokensReturnsAuthTokens() throws Exception {
+    assertThat(tokenAccessor.getTokens(ACCOUNT_ID)).containsExactlyElementsIn(tokens);
+  }
+
+  @Test
+  public void getTokensReturnsHttpPasswordIfNoAuthTokenExists() throws Exception {
+    doReturn(ImmutableList.of()).when(versionedAuthTokens).getTokens();
+    List<AuthToken> result = tokenAccessor.getTokens(ACCOUNT_ID);
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0))
+        .isEqualTo(AuthToken.create(DirectAuthTokenAccessor.LEGACY_ID, "secret"));
+  }
+
+  @Test
+  public void getTokensReturnsEmptyListIfNeitherTokensOrPasswordExists() throws Exception {
+    doReturn(ImmutableList.of()).when(versionedAuthTokens).getTokens();
+    doReturn(AccountState.forAccount(account)).when(accountCache).getEvenIfMissing(ACCOUNT_ID);
+    List<AuthToken> result = tokenAccessor.getTokens(ACCOUNT_ID);
+    assertThat(result).hasSize(0);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index 5f062be..76cd5e0 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -27,15 +27,20 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.AccountGroup.UUID;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
+import java.util.Collection;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
@@ -138,4 +143,80 @@
     assertFalse(checker.contains(handled));
     assertFalse(checker.contains(notHandled));
   }
+
+  private static GroupReference newGroup(String name) {
+    return GroupReference.create(AccountGroup.uuid(name.toLowerCase().replace(' ', '_')), name);
+  }
+
+  private static List<String> suggest(UniversalGroupBackend backend, String name) {
+    Collection<GroupReference> suggestions = backend.suggest(name, null);
+    return suggestions.stream().map(GroupReference::getName).collect(Collectors.toList());
+  }
+
+  @Test
+  public void suggestInterleaved() {
+    GroupBackend backend1 = mock(GroupBackend.class);
+    when(backend1.suggest(any(), any()))
+        .thenReturn(
+            ImmutableList.of(
+                newGroup("Administrators"), // 14
+                newGroup("Users") // 5
+                ));
+
+    GroupBackend backend2 = mock(GroupBackend.class);
+    when(backend2.suggest(any(), any()))
+        .thenReturn(
+            ImmutableList.of(
+                newGroup("Test Users"), // 10
+                newGroup("Project Owners") // 14
+                ));
+
+    backends = new DynamicSet<>();
+    backends.add("gerrit", backend1);
+    backends.add("test", backend2);
+    backend =
+        new UniversalGroupBackend(
+            new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE),
+            new DisabledMetricMaker());
+
+    // Test interleaving
+    List<String> actual = suggest(backend, "user");
+    assertEquals(
+        ImmutableList.of("Users", "Test Users", "Administrators", "Project Owners"), actual);
+
+    // Test exact match first
+    actual = suggest(backend, "Administrators");
+    assertEquals(
+        ImmutableList.of("Administrators", "Users", "Test Users", "Project Owners"), actual);
+
+    // Test exact match first (case-insensitive)
+    actual = suggest(backend, "aDMINistrators");
+    assertEquals(
+        ImmutableList.of("Administrators", "Users", "Test Users", "Project Owners"), actual);
+  }
+
+  @Test
+  public void suggestWithDifferentSizeLists() {
+    GroupBackend backend1 = mock(GroupBackend.class);
+    when(backend1.suggest(any(), any()))
+        .thenReturn(ImmutableList.of(newGroup("a"), newGroup("bbb"), newGroup("eeeee")));
+
+    GroupBackend backend2 = mock(GroupBackend.class);
+    when(backend2.suggest(any(), any()))
+        .thenReturn(ImmutableList.of(newGroup("cc"), newGroup("dddd")));
+
+    backends = new DynamicSet<>();
+    backends.add("gerrit", backend1);
+    backends.add("test", backend2);
+    backend =
+        new UniversalGroupBackend(
+            new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE),
+            new DisabledMetricMaker());
+
+    List<String> actual = suggest(backend, "a");
+    assertEquals(ImmutableList.of("a", "cc", "bbb", "dddd", "eeeee"), actual);
+
+    actual = suggest(backend, "dddd");
+    assertEquals(ImmutableList.of("dddd", "a", "cc", "bbb", "eeeee"), actual);
+  }
 }
diff --git a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
index d36f62b..ae8dea2 100644
--- a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
@@ -144,7 +144,7 @@
   @Test
   public void externalIdMethods() {
     assertThatSerializedClass(ExternalId.class)
-        .hasAutoValueMethods(
+        .hasFields(
             ImmutableMap.<String, Type>builder()
                 .put("key", ExternalId.Key.class)
                 .put("accountId", Account.Id.class)
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index cd6a6b4..74a365d 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -1,5 +1,5 @@
-load("//tools/bzl:junit.bzl", "junit_tests")
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+load("//tools/bzl:junit.bzl", "junit_tests")
 
 junit_tests(
     name = "tests",
@@ -7,7 +7,7 @@
     deps = [
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:junit",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 508fb3b..cf8bdda 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -64,7 +64,7 @@
       @Nullable Duration expireAfterWrite,
       @Nullable Duration refreshAfterWrite) {
     return new SqlStore<>(
-        "jdbc:h2:mem:Test_" + id,
+        "jdbc:h2:mem:Test_" + id + ";DB_CLOSE_DELAY=-1",
         KEY_TYPE,
         StringCacheSerializer.INSTANCE,
         StringCacheSerializer.INSTANCE,
@@ -74,7 +74,8 @@
         expireAfterWrite,
         refreshAfterWrite,
         true,
-        true);
+        true,
+        new AtomicBoolean(false));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BUILD b/javatests/com/google/gerrit/server/cache/serialize/BUILD
index fa6a717..28018df 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -8,7 +8,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/cache/testing",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib:jgit",
         "//lib:junit",
diff --git a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
index f8c04a9..acfb54f 100644
--- a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
@@ -22,6 +23,7 @@
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -179,10 +181,26 @@
     assertThat(parse("2years")).isEqualTo(ms(365 * 2, DAYS));
   }
 
-  private static long ms(int cnt, TimeUnit unit) {
+  @Test
+  public void timeUnitDefault() {
+    assertThat(ConfigUtil.getTimeUnit("   ", 1, MILLISECONDS)).isEqualTo(ms(1, MILLISECONDS));
+    assertThat(ms(ConfigUtil.getTimeUnit("", 2, SECONDS), SECONDS)).isEqualTo(ms(2, SECONDS));
+    assertThat(ms(ConfigUtil.getTimeUnit(null, 3, MINUTES), MINUTES)).isEqualTo(ms(3, MINUTES));
+  }
+
+  @Test
+  public void timeUnitInvalid() {
+    assertThrows(IllegalArgumentException.class, () -> parse("foo"));
+    assertThrows(IllegalArgumentException.class, () -> parse("1.5"));
+    assertThrows(IllegalArgumentException.class, () -> parse("-5"));
+    assertThrows(IllegalArgumentException.class, () -> parse("5 bar"));
+  }
+
+  private static long ms(long cnt, TimeUnit unit) {
     return MILLISECONDS.convert(cnt, unit);
   }
 
+  @CanIgnoreReturnValue
   private static long parse(String string) {
     return ConfigUtil.getTimeUnit(string, 1, MILLISECONDS);
   }
diff --git a/javatests/com/google/gerrit/server/config/DefaultBooleanProjectConfigTest.java b/javatests/com/google/gerrit/server/config/DefaultBooleanProjectConfigTest.java
new file mode 100644
index 0000000..1dfbe2f
--- /dev/null
+++ b/javatests/com/google/gerrit/server/config/DefaultBooleanProjectConfigTest.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+import org.junit.Test;
+
+/** Unit test for {@link DefaultBooleanProjectConfig}. */
+public class DefaultBooleanProjectConfigTest {
+  @Test
+  public void tryParse() {
+    // assert valid values for key with subsection
+    assertTryParseOk(
+        "section.subsection.name=false",
+        "section",
+        "subsection",
+        "name",
+        DefaultBooleanProjectConfig.Value.FALSE);
+    assertTryParseOk(
+        "section.subsection.name=FALSE",
+        "section",
+        "subsection",
+        "name",
+        DefaultBooleanProjectConfig.Value.FALSE);
+    assertTryParseOk(
+        "section.subsection.name=true",
+        "section",
+        "subsection",
+        "name",
+        DefaultBooleanProjectConfig.Value.TRUE);
+    assertTryParseOk(
+        "section.subsection.name=TRUE",
+        "section",
+        "subsection",
+        "name",
+        DefaultBooleanProjectConfig.Value.TRUE);
+    assertTryParseOk(
+        "section.subsection.name=forced",
+        "section",
+        "subsection",
+        "name",
+        DefaultBooleanProjectConfig.Value.FORCED);
+    assertTryParseOk(
+        "section.subsection.name=FORCED",
+        "section",
+        "subsection",
+        "name",
+        DefaultBooleanProjectConfig.Value.FORCED);
+
+    // assert valid values for key without subsection
+    assertTryParseOk(
+        "section.name=false", "section", null, "name", DefaultBooleanProjectConfig.Value.FALSE);
+    assertTryParseOk(
+        "section.name=FALSE", "section", null, "name", DefaultBooleanProjectConfig.Value.FALSE);
+    assertTryParseOk(
+        "section.name=true", "section", null, "name", DefaultBooleanProjectConfig.Value.TRUE);
+    assertTryParseOk(
+        "section.name=TRUE", "section", null, "name", DefaultBooleanProjectConfig.Value.TRUE);
+    assertTryParseOk(
+        "section.name=forced", "section", null, "name", DefaultBooleanProjectConfig.Value.FORCED);
+    assertTryParseOk(
+        "section.name=FORCED", "section", null, "name", DefaultBooleanProjectConfig.Value.FORCED);
+
+    // assert invalid values
+    assertTryParseFail(null);
+
+    // key is missing
+    assertTryParseFail("true");
+    assertTryParseFail("TRUE");
+    assertTryParseFail("=true");
+    assertTryParseFail("=TRUE");
+
+    // value is missing
+    assertTryParseFail("receive.rejectImplicitMerges");
+    assertTryParseFail("receive.rejectImplicitMerges=");
+
+    // key and value not separated by '='
+    assertTryParseFail("receive.rejectImplicitMerges:true");
+
+    // section or name is missing
+    assertTryParseFail("foo=true");
+
+    // key has too many parts
+    assertTryParseFail("section.subsection.subsubsection.name=true");
+
+    // invalid value
+    assertTryParseFail("section.subsubsection.name=disabled");
+
+    // empty key parts
+    assertTryParseFail("section.=true");
+    assertTryParseFail(".key=true");
+    assertTryParseFail("section..key=true");
+    assertTryParseFail(".subsection.key=true");
+    assertTryParseFail("section.subsection.=true");
+    assertTryParseFail("section..=true");
+    assertTryParseFail(".subsection.=true");
+    assertTryParseFail("..key=true");
+    assertTryParseFail(".=true");
+    assertTryParseFail("..=true");
+  }
+
+  private void assertTryParseOk(
+      String s,
+      String expectedSection,
+      @Nullable String expectedSubSection,
+      String expectedName,
+      DefaultBooleanProjectConfig.Value expectedDefaultValue) {
+    Optional<DefaultBooleanProjectConfig> defaultBooleanProjectConfig =
+        DefaultBooleanProjectConfig.tryParse(s);
+    assertThat(defaultBooleanProjectConfig).isPresent();
+
+    assertThat(defaultBooleanProjectConfig.get().section()).isEqualTo(expectedSection);
+    assertThat(defaultBooleanProjectConfig.get().subSection())
+        .isEqualTo(Optional.ofNullable(expectedSubSection));
+    assertThat(defaultBooleanProjectConfig.get().name()).isEqualTo(expectedName);
+    assertThat(defaultBooleanProjectConfig.get().defaultValue()).isEqualTo(expectedDefaultValue);
+  }
+
+  private void assertTryParseFail(String s) {
+    assertThat(DefaultBooleanProjectConfig.tryParse(s)).isEmpty();
+  }
+}
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index 708e247..cf13fd6 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.restapi.config.ListCapabilities;
 import com.google.gerrit.server.restapi.config.ListCapabilities.CapabilityInfo;
@@ -100,6 +101,16 @@
     }
 
     @Override
+    public WithUser user(CurrentUser user, ImpersonationPermissionMode permissionMode) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public WithUser exactUser(CurrentUser user) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
     public WithUser absentUser(Account.Id id) {
       throw new UnsupportedOperationException();
     }
diff --git a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
index 8d93f66..b526411 100644
--- a/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.SubmitType;
 import java.nio.file.Path;
@@ -96,6 +97,79 @@
   }
 
   @Test
+  public void defaultConfigForSpecificFilter() {
+    configureDefaultConfig(
+        "someProject", "receive.rejectImplicitMerges", DefaultBooleanProjectConfig.Value.FORCED);
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("someProject"), BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isEqualTo(DefaultBooleanProjectConfig.Value.FORCED);
+    // for non-matching project the default is read from the BooleanProjectConfig.
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("someOtherProject"), BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isEqualTo(
+            DefaultBooleanProjectConfig.Value.fromBooleanProjectConfig(
+                BooleanProjectConfig.REJECT_IMPLICIT_MERGES));
+
+    // for another key the default is read from the BooleanProjectConfig.
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("someProject"),
+                BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET))
+        .isEqualTo(
+            DefaultBooleanProjectConfig.Value.fromBooleanProjectConfig(
+                BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET));
+  }
+
+  @Test
+  public void defaultConfigForStartWithFilter() {
+    configureDefaultConfig(
+        "somePath/somePath/*",
+        "receive.rejectImplicitMerges",
+        DefaultBooleanProjectConfig.Value.FORCED);
+    configureDefaultConfig(
+        "somePath/*", "receive.rejectImplicitMerges", DefaultBooleanProjectConfig.Value.FALSE);
+    configureDefaultConfig(
+        "*", "receive.rejectImplicitMerges", DefaultBooleanProjectConfig.Value.TRUE);
+
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("someProject"), BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isEqualTo(DefaultBooleanProjectConfig.Value.TRUE);
+
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("somePath/someProject"),
+                BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isEqualTo(DefaultBooleanProjectConfig.Value.FALSE);
+
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("somePath/somePath/someProject"),
+                BooleanProjectConfig.REJECT_IMPLICIT_MERGES))
+        .isEqualTo(DefaultBooleanProjectConfig.Value.FORCED);
+
+    // for another key the default is read from the BooleanProjectConfig.
+    assertThat(
+            repoCfg.getDefault(
+                Project.nameKey("someProject"),
+                BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET))
+        .isEqualTo(
+            DefaultBooleanProjectConfig.Value.fromBooleanProjectConfig(
+                BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET));
+  }
+
+  private void configureDefaultConfig(
+      String projectFilter, String key, DefaultBooleanProjectConfig.Value defaultValue) {
+    cfg.setString(
+        RepositoryConfig.SECTION_NAME,
+        projectFilter,
+        RepositoryConfig.DEFAULT_CONFIG_NAME,
+        String.format("%s=%s", key, defaultValue.name()));
+  }
+
+  @Test
   public void ownerGroupsWhenNotConfigured() {
     assertThat(repoCfg.getOwnerGroups(Project.nameKey("someProject"))).isEmpty();
   }
diff --git a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
index 341872d..7d1f13e 100644
--- a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -25,7 +25,9 @@
 import java.time.Month;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
+import java.util.HashSet;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -73,6 +75,62 @@
   }
 
   @Test
+  public void jitterAddsRandomDelay() {
+    Config rc = new Config();
+    rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+    rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "11:30");
+    rc.setString("a", null, ScheduleConfig.KEY_JITTER, "10m");
+
+    long jitter = ms(10, MINUTES);
+    long interval = ms(1, HOURS);
+
+    Set<Long> initialDelays = new HashSet<>();
+    for (int i = 0; i < 10; i++) {
+      Optional<Schedule> schedule = ScheduleConfig.builder(rc, "a").setNow(NOW).buildSchedule();
+      assertThat(schedule).isPresent();
+      long actualInitialDelay = schedule.get().initialDelay();
+      assertThat(actualInitialDelay).isGreaterThan(0);
+      assertThat(actualInitialDelay).isAtMost(jitter + interval);
+      assertThat(schedule.get().interval()).isEqualTo(interval);
+      initialDelays.add(actualInitialDelay);
+    }
+    assertThat(initialDelays.size()).isGreaterThan(1);
+  }
+
+  @Test
+  public void zeroJitterHasNoEffect() {
+    Config rc = new Config();
+    rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+    rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "11:00");
+    rc.setString("a", null, ScheduleConfig.KEY_JITTER, "0s");
+
+    Optional<Schedule> schedule = ScheduleConfig.builder(rc, "a").setNow(NOW).buildSchedule();
+    assertThat(schedule).isPresent();
+    assertThat(schedule.get().initialDelay()).isEqualTo(ms(1, HOURS));
+  }
+
+  @Test
+  public void missingJitterHasNoEffect() {
+    Config rc = new Config();
+    rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+    rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "11:00");
+
+    Optional<Schedule> schedule = ScheduleConfig.builder(rc, "a").setNow(NOW).buildSchedule();
+    assertThat(schedule).isPresent();
+    assertThat(schedule.get().initialDelay()).isEqualTo(ms(1, HOURS));
+  }
+
+  @Test
+  public void invalidConfigBadJitter() {
+    Config rc = new Config();
+    rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+    rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "11:00");
+    rc.setString("a", null, ScheduleConfig.KEY_JITTER, "invalid");
+
+    assertThat(ScheduleConfig.builder(rc, "a").setNow(NOW).buildSchedule()).isEmpty();
+  }
+
+  @Test
   public void defaultKeysWithSubsection() {
     Config rc = new Config();
     rc.setString("a", "b", ScheduleConfig.KEY_INTERVAL, "1h");
diff --git a/javatests/com/google/gerrit/server/git/ChangesByProjectCacheTest.java b/javatests/com/google/gerrit/server/git/ChangesByProjectCacheTest.java
new file mode 100644
index 0000000..9529db1
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/ChangesByProjectCacheTest.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableTable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.git.ChangesByProjectCacheImpl.CachedProjectChanges;
+import com.google.gerrit.server.git.ChangesByProjectCacheImpl.CachedProjectChangesSerializer;
+import com.google.gerrit.server.git.ChangesByProjectCacheImpl.PrivateChange;
+import com.google.gerrit.server.notedb.AbstractChangeNotesTest;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.InvalidServerIdException;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public final class ChangesByProjectCacheTest extends AbstractChangeNotesTest {
+
+  ChangeData.Factory cdFactory;
+
+  @Override
+  public void setUpTestEnvironment() throws Exception {
+    cdFactory = mock(ChangeData.Factory.class);
+    super.setUpTestEnvironment();
+  }
+
+  @Test
+  public void cachedProjectChangesSerializer() throws Exception {
+    ChangeData cd1 = createChange(false, true);
+    ChangeData cd2 = createChange(true, true);
+    ChangeData cd3 = createChange(true, false);
+    CachedProjectChanges cachedProjectChanges = new CachedProjectChanges(List.of(cd1, cd2, cd3));
+
+    CachedProjectChangesSerializer s = CachedProjectChangesSerializer.INSTANCE;
+    CachedProjectChanges deserialized = s.deserialize(s.serialize(cachedProjectChanges));
+    assertThat(deserialized.metaObjectIdByNonPrivateChangeByBranch)
+        .isEqualTo(cachedProjectChanges.metaObjectIdByNonPrivateChangeByBranch);
+    for (Map.Entry<Change.Id, PrivateChange> e : deserialized.privateChangeById.entrySet()) {
+      PrivateChange deserializedChange = e.getValue();
+      PrivateChange origChange = cachedProjectChanges.privateChangeById.get(e.getKey());
+      assertThat(deserializedChange.metaRevision()).isEqualTo(origChange.metaRevision());
+      assertThat(deserializedChange.reviewers()).isEqualTo(origChange.reviewers());
+      assertThat(deserializedChange.change().getChangeId())
+          .isEqualTo(origChange.change().getChangeId());
+      assertThat(deserializedChange.weigh()).isEqualTo(origChange.weigh());
+    }
+  }
+
+  @Test
+  public void updateChangeDatasShouldSkipForeignServerIdChanges() throws Exception {
+    // Create change with the Gerrit's server-id
+    ChangeData validChange = createChange(false, false);
+    when(cdFactory.create(project, validChange.getId())).thenReturn(validChange);
+    // Create change with a foreign server-id
+    ChangeData invalidChange = createChange(false, false);
+    when(cdFactory.create(project, invalidChange.getId()))
+        .thenThrow(new InvalidServerIdException(LOCAL_SERVER_ID, "foreign-sever-id"));
+
+    CachedProjectChanges cachedProjectChanges =
+        new CachedProjectChanges(List.of(validChange, invalidChange));
+
+    CachedProjectChanges.ChangeDataUpdateResult result =
+        cachedProjectChanges.updateChangeDatas(
+            project,
+            cdFactory,
+            Map.of(
+                validChange.getId(),
+                validChange.metaRevisionOrThrow(),
+                invalidChange.getId(),
+                invalidChange.metaRevisionOrThrow()),
+            "Test");
+
+    assertThat(result.cds()).doesNotContain(null);
+    assertThat(result.cds()).containsExactly(validChange);
+  }
+
+  private ChangeData createChange(boolean isPrivate, boolean hasReviewers) throws Exception {
+    Change change = newChange();
+    if (isPrivate) {
+      ChangeUpdate update = newUpdate(change, changeOwner);
+      update.setPrivate(true);
+      update.commit();
+    }
+    ChangeNotes notes = newNotes(change);
+    ChangeData cd =
+        ChangeData.createForTest(
+            project,
+            change.getId(),
+            Objects.requireNonNull(change.currentPatchSetId()).get(),
+            ObjectId.zeroId(),
+            null,
+            changeNotesFactory,
+            notes);
+    if (hasReviewers) {
+      cd.setReviewers(
+          ReviewerSet.fromTable(
+              ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
+                  .put(ReviewerStateInternal.CC, Account.id(1001), Instant.ofEpochMilli(1212L))
+                  .put(
+                      ReviewerStateInternal.REVIEWER, Account.id(1002), Instant.ofEpochMilli(1213L))
+                  .build()));
+    } else {
+      cd.setReviewers(ReviewerSet.empty());
+    }
+    return cd;
+  }
+}
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index a40afe8..8e74c44 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -59,7 +59,6 @@
             ExternalId.Key.create(ExternalId.SCHEME_MAILTO, "foo.bar@example.com", false),
             id,
             "foo.bar@example.com",
-            null,
             ObjectId.fromString("1b9a0cf038ea38a0ab08617c39aa8e28413a27ca"));
     ExternalId extId2 =
         ExternalId.create(
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index 4b3c658..3cb5bae 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -33,7 +33,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import java.time.Instant;
-import java.util.concurrent.CopyOnWriteArrayList;
 import org.eclipse.jgit.lib.Config;
 import org.junit.After;
 import org.junit.Before;
@@ -112,33 +111,6 @@
   }
 
   @Test
-  public void
-      traceTimersInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers()
-          throws Exception {
-    // Remove test performance logger so that there are no registered performance loggers.
-    removeAllPerformanceLoggers();
-
-    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-
-    try (PerformanceLogContext traceContext =
-        new PerformanceLogContext(config, performanceLoggers)) {
-      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-
-      TraceContext.newTimer("test1").close();
-      TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
-          .close();
-
-      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-    }
-
-    assertThat(testPerformanceLogger.logEntries()).isEmpty();
-
-    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-  }
-
-  @Test
   public void timerMetricsInsidePerformanceLogContextCreatePerformanceLog() {
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
     assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
@@ -280,57 +252,6 @@
   }
 
   @Test
-  public void
-      timerMetricssInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers()
-          throws Exception {
-    // Remove all performance loggers so that there are no registered performance loggers.
-    removeAllPerformanceLoggers();
-
-    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-
-    try (PerformanceLogContext traceContext =
-        new PerformanceLogContext(config, performanceLoggers)) {
-      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-
-      Timer0 timer0 =
-          metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
-      timer0.start().close();
-
-      Timer1<Integer> timer1 =
-          metricMaker.newTimer(
-              "test2/latency",
-              new Description("Latency metric for testing"),
-              Field.ofInteger("accoutn", Metadata.Builder::accountId).build());
-      timer1.start(1000000).close();
-
-      Timer2<Integer, Integer> timer2 =
-          metricMaker.newTimer(
-              "test3/latency",
-              new Description("Latency metric for testing"),
-              Field.ofInteger("account", Metadata.Builder::accountId).build(),
-              Field.ofInteger("change", Metadata.Builder::changeId).build());
-      timer2.start(1000000, 123).close();
-
-      Timer3<Integer, Integer, String> timer3 =
-          metricMaker.newTimer(
-              "test4/latency",
-              new Description("Latency metric for testing"),
-              Field.ofInteger("account", Metadata.Builder::accountId).build(),
-              Field.ofInteger("change", Metadata.Builder::changeId).build(),
-              Field.ofProjectName("project").build());
-      timer3.start(1000000, 123, "foo/bar").close();
-
-      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-    }
-
-    assertThat(testPerformanceLogger.logEntries()).isEmpty();
-
-    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-  }
-
-  @Test
   public void nestingPerformanceLogContextsIsPossible() {
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
     assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
@@ -374,12 +295,6 @@
     }
   }
 
-  private void removeAllPerformanceLoggers() throws Exception {
-    java.lang.reflect.Field itemsField = DynamicSet.class.getDeclaredField("items");
-    itemsField.setAccessible(true);
-    ((CopyOnWriteArrayList<?>) itemsField.get(performanceLoggers)).clear();
-  }
-
   @AutoValue
   abstract static class PerformanceLogEntry {
     static PerformanceLogEntry create(String operation, Metadata metadata) {
diff --git a/javatests/com/google/gerrit/server/logging/TraceContextTest.java b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
index 0478acc..5354125 100644
--- a/javatests/com/google/gerrit/server/logging/TraceContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
@@ -298,7 +298,7 @@
 
   @Test
   public void metadataForTraceTimerCannotBeNull() throws Exception {
-    assertThrows(NullPointerException.class, () -> TraceContext.newTimer("test", null));
+    assertThrows(NullPointerException.class, () -> TraceContext.newTimer("test", (Metadata) null));
   }
 
   private void assertTags(ImmutableMap<String, ImmutableSet<String>> expectedTagMap) {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 17a5796..b3f96d4 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -19,6 +19,7 @@
 
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.common.NoMergeBaseReason;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.time.TimeUtil;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -782,8 +783,10 @@
             + "Subject: This is a test change\n"
             + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
             + "Contains-Conflicts: true\n"
+            + "Base: 76ce7b5cb6f176fb64d9e4406f83fe5d658e1136\n"
             + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
-            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n");
+            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n"
+            + "Merge-strategy: resolve\n");
 
     // Conflicts information present, Contains-Conflicts: false
     assertParseSucceeds(
@@ -795,10 +798,56 @@
             + "Subject: This is a test change\n"
             + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
             + "Contains-Conflicts: false\n"
+            + "Base: 76ce7b5cb6f176fb64d9e4406f83fe5d658e1136\n"
             + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
-            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n");
+            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n"
+            + "Merge-strategy: resolve\n");
 
-    // Ours/Theirs is optional if "Contains-Conflicts: false" is set
+    // base not set, Conflicts information present, Contains-Conflicts: true
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 2\n"
+            + "Subject: This is a test change\n"
+            + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+            + "Contains-Conflicts: true\n"
+            + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
+            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n"
+            + "Merge-strategy: recursive\n"
+            + "No-base-reason: COMPUTED_BASE\n");
+
+    // base not set, Conflicts information present, Contains-Conflicts: false
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 2\n"
+            + "Subject: This is a test change\n"
+            + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+            + "Contains-Conflicts: false\n"
+            + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
+            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n"
+            + "Merge-strategy: ours\n"
+            + "No-base-reason: ONE_SIDED_MERGE_STRATEGY\n");
+
+    // Base/Ours/Theirs/Merge-strategy is optional if "Contains-Conflicts: false" is set.
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 2\n"
+            + "Subject: This is a test change\n"
+            + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+            + "Contains-Conflicts: false\n"
+            + "No-base-reason: NO_MERGE_PERFORMED\n");
+
+    // Base/Ours/Theirs/Merge-strategy is optional if "Contains-Conflicts: false" is set.
+    // No-base-reason is missing for revisions that have been created before Gerrit started to store
+    // the base in the conflicts information.
     assertParseSucceeds(
         "Update change\n"
             + "\n"
@@ -809,7 +858,34 @@
             + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
             + "Contains-Conflicts: false\n");
 
-    // Ours/Theirs is ignored if Contains-Conflicts is missing
+    // Base/Ours/Theirs/Merge-strategy/No-base-reason is optional if "Contains-Conflicts: true" is
+    // set.
+    // The data is missing for revisions that have been created before Gerrit started to store
+    // the base in the conflicts information.
+    ChangeNotesState changeNotesState =
+        assertParseSucceeds(
+            "Update change\n"
+                + "\n"
+                + "Branch: refs/heads/master\n"
+                + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+                + "Patch-set: 2\n"
+                + "Subject: This is a test change\n"
+                + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+                + "Contains-Conflicts: true\n"
+                + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
+                + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n");
+    assertThat(
+            changeNotesState.patchSets().stream()
+                .filter(e -> e.getKey().get() == 2)
+                .findFirst()
+                .get()
+                .getValue()
+                .conflicts()
+                .get()
+                .noBaseReason())
+        .hasValue(NoMergeBaseReason.HISTORIC_DATA_WITHOUT_BASE);
+
+    // Base/Ours/Theirs/Merge-strategy/No-base-reason is ignored if Contains-Conflicts is missing
     assertParseSucceeds(
         "Update change\n"
             + "\n"
@@ -818,6 +894,25 @@
             + "Patch-set: 2\n"
             + "Subject: This is a test change\n"
             + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+            + "Base: 76ce7b5cb6f176fb64d9e4406f83fe5d658e1136\n"
+            + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
+            + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n"
+            + "Merge-strategy: resolve\n"
+            + "No-base-reason: COMPUTED_BASE\n");
+
+    // Base/Merge-Strategy/No-base-reason is missing when conflicts information is present,
+    // Contains-Conflicts: true
+    // Base/Merge-Strategy/No-base-reason is missing for patch sets that have been created before
+    // Gerrit started to store the base for conflicts.
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 2\n"
+            + "Subject: This is a test change\n"
+            + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+            + "Contains-Conflicts: true\n"
             + "Ours: 2d1a400a2e56090699f8aeb522ec1f82bbd54d57\n"
             + "Theirs: aaeceb9f08df45748b1420ab2b0687906151ae59\n");
 
@@ -851,6 +946,18 @@
             + "Subject: This is a test change\n"
             + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
             + "Contains-Conflicts: true\n");
+
+    // Parsing fails if No-base-reason is invalid
+    assertParseFails(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 2\n"
+            + "Subject: This is a test change\n"
+            + "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+            + "Contains-Conflicts: false\n"
+            + "No-base-reason: INVALID\n");
   }
 
   private RevCommit writeCommit(String body) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 3c1abbd..71e60d2 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DraftCommentsReader;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.git.validators.TopicValidator;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -879,7 +880,11 @@
     for (String strangeTag : strangeTags) {
       Change c = newChange();
       CurrentUser otherUserAsOwner =
-          userFactory.runAs(/* remotePeer= */ null, changeOwner.getAccountId(), otherUser);
+          userFactory.runAs(
+              /* remotePeer= */ null,
+              changeOwner.getAccountId(),
+              otherUser,
+              ImpersonationPermissionMode.THIS_USER);
       ChangeUpdate update = newUpdate(c, otherUserAsOwner);
       update.putApproval(LabelId.CODE_REVIEW, (short) 2);
       update.setTag(strangeTag);
@@ -2524,7 +2529,8 @@
   @Test
   public void patchLineCommentNotesFormatRealAuthor() throws Exception {
     Change c = newChange();
-    CurrentUser ownerAsOtherUser = userFactory.runAs(null, otherUserId, changeOwner);
+    CurrentUser ownerAsOtherUser =
+        userFactory.runAs(null, otherUserId, changeOwner, ImpersonationPermissionMode.THIS_USER);
     ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
     String uuid = "uuid";
     String message = "comment";
@@ -3551,7 +3557,8 @@
   @Test
   public void realUser() throws Exception {
     Change c = newChange();
-    CurrentUser ownerAsOtherUser = userFactory.runAs(null, otherUserId, changeOwner);
+    CurrentUser ownerAsOtherUser =
+        userFactory.runAs(null, otherUserId, changeOwner, ImpersonationPermissionMode.THIS_USER);
     ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
     update.setChangeMessage("Message on behalf of other user");
     update.commit();
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index f85c8dd..90f858d 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.SubmissionId;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestChanges;
 import java.time.ZoneOffset;
@@ -357,7 +358,11 @@
   public void realUser() throws Exception {
     Change c = newChange();
     CurrentUser ownerAsOtherUser =
-        userFactory.runAs(/* remotePeer= */ null, otherUserId, changeOwner);
+        userFactory.runAs(
+            /* remotePeer= */ null,
+            otherUserId,
+            changeOwner,
+            ImpersonationPermissionMode.THIS_USER);
     ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
     update.setChangeMessage("Message on behalf of other user");
     update.commit();
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 16194d4..eb709aa 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.ImpersonationPermissionMode;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.validators.TopicValidator;
 import com.google.gerrit.server.notedb.ChangeNoteUtil.AttentionStatusInNoteDb;
@@ -408,7 +409,8 @@
         this.userFactory.runAs(
             /* remotePeer= */ null,
             changeOwner.getAccountId(),
-            requireNonNull(otherUser).getRealUser());
+            requireNonNull(otherUser),
+            ImpersonationPermissionMode.THIS_USER);
     ChangeUpdate impersonatedChangeMessageUpdate = newUpdate(c, impersonatedChangeOwner);
     impersonatedChangeMessageUpdate.setChangeMessage("Other comment on behalf of");
     impersonatedChangeMessageUpdate.commit();
diff --git a/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
index 366cbf7..b9f8308 100644
--- a/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
@@ -187,7 +187,7 @@
           ExternalId.Key.create(
               ExternalId.SCHEME_IMPORTED, importedAccount + "@" + IMPORTED_SERVER_ID, false);
       ExternalId linkedExternalId =
-          ExternalId.create(importedAccountIdKey, localAccountId, null, null, null);
+          ExternalId.create(importedAccountIdKey, localAccountId, null, null);
 
       when(externalIdCacheMock.byKey(eq(importedAccountIdKey)))
           .thenReturn(Optional.of(linkedExternalId));
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 45da767..6c5527c 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.util.Comparator.naturalOrder;
 import static java.util.stream.Collectors.toList;
 import static org.junit.Assert.fail;
 
@@ -68,6 +69,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -118,6 +120,8 @@
 
   @Inject protected AccountManager accountManager;
 
+  @Inject protected ExternalIdFactory externalIdFactory;
+
   @Inject protected GerritApi gApi;
 
   @Inject @GerritPersonIdent Provider<PersonIdent> serverIdent;
@@ -587,16 +591,32 @@
   @Test
   public void withSecondaryEmails() throws Exception {
     AccountInfo user1 = newAccount("myuser", "My User", "my.user@example.com", true);
-    String[] secondaryEmails = new String[] {"bar@example.com", "foo@example.com"};
-    addEmails(user1, secondaryEmails);
+
+    String[] mailToSecondaryEmails = new String[] {"bar@example.com", "foo@example.com"};
+    addEmails(user1, mailToSecondaryEmails);
+
+    String nonMailToSecondaryEmail = "baz@example.com";
+    Account.Id accountId = Account.id(user1._accountId);
+    accountsUpdate
+        .get()
+        .update(
+            "Add External ID",
+            accountId,
+            u ->
+                u.addExternalId(
+                    externalIdFactory.createWithEmail(
+                        "x", "1", accountId, nonMailToSecondaryEmail)));
+
+    List<String> secondaryEmails = new ArrayList<>();
+    secondaryEmails.addAll(Arrays.asList(mailToSecondaryEmails));
+    secondaryEmails.add(nonMailToSecondaryEmail);
+    secondaryEmails.sort(naturalOrder());
 
     List<AccountInfo> result = assertQuery(getDefaultSearch(user1), user1);
     assertThat(result.get(0).secondaryEmails).isNull();
 
     result = assertQuery(newQuery(getDefaultSearch(user1)).withSuggest(true), user1);
-    assertThat(result.get(0).secondaryEmails)
-        .containsExactlyElementsIn(Arrays.asList(secondaryEmails))
-        .inOrder();
+    assertThat(result.get(0).secondaryEmails).containsExactlyElementsIn(secondaryEmails).inOrder();
 
     result =
         assertQuery(
@@ -606,18 +626,14 @@
     result =
         assertQuery(
             newQuery(getDefaultSearch(user1)).withOption(ListAccountsOption.ALL_EMAILS), user1);
-    assertThat(result.get(0).secondaryEmails)
-        .containsExactlyElementsIn(Arrays.asList(secondaryEmails))
-        .inOrder();
+    assertThat(result.get(0).secondaryEmails).containsExactlyElementsIn(secondaryEmails).inOrder();
 
     result =
         assertQuery(
             newQuery(getDefaultSearch(user1))
                 .withOptions(ListAccountsOption.DETAILS, ListAccountsOption.ALL_EMAILS),
             user1);
-    assertThat(result.get(0).secondaryEmails)
-        .containsExactlyElementsIn(Arrays.asList(secondaryEmails))
-        .inOrder();
+    assertThat(result.get(0).secondaryEmails).containsExactlyElementsIn(secondaryEmails).inOrder();
   }
 
   @Test
@@ -882,7 +898,6 @@
     for (String email : emails) {
       accountManager.link(id, authRequestFactory.createForEmail(email));
     }
-    accountIndexer.index(id);
   }
 
   protected QueryRequest newQuery(Object query) throws RestApiException {
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 6ca4c6b..5220137 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -74,10 +74,12 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ChangeIdentifier;
 import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
 import com.google.gerrit.extensions.api.changes.CustomKeyedValuesInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
@@ -93,6 +95,7 @@
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -1564,15 +1567,33 @@
     assertQuery("label:Code-Review>=+1,count=2", reviewTwoPlus1Change);
 
     // "count" and "user" args cannot be used simultaneously.
-    assertThrows(
-        BadRequestException.class,
-        () -> assertQuery("label:Code-Review=+1,user=non_uploader,count=2"));
+    thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> assertQuery("label:Code-Review=+1,user=non_uploader,count=2"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot use the 'count' argument in conjunction with the 'user' argument");
 
     // "count" and "group" args cannot be used simultaneously.
-    assertThrows(
-        BadRequestException.class, () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
+    thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot use the 'count' argument in conjunction with the 'group' argument");
 
-    // "non_contributor arg for the label operator is not allowed in change queries
+    // "user" and "group" args cannot be used simultaneously.
+    thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> assertQuery("label:Code-Review=+1,user=non_uploader,group=gerrit"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("Cannot use the 'user' argument in conjunction with the 'group' argument");
+
+    // "non_contributor" arg for the label operator is not allowed in change queries
     thrown =
         assertThrows(
             BadRequestException.class,
@@ -1580,6 +1601,23 @@
     assertThat(thrown)
         .hasMessageThat()
         .isEqualTo("non_contributor arg is not allowed in change queries");
+
+    // "non_auther" arg for the label operator is not allowed in change queries
+    thrown =
+        assertThrows(
+            BadRequestException.class, () -> assertQuery("label:Code-Review=+2,user=non_author"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("non_author arg is not allowed in change queries");
+
+    // "non_committer" arg for the label operator is not allowed in change queries
+    thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> assertQuery("label:Code-Review=+2,user=non_committer"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("non_committer arg is not allowed in change queries");
   }
 
   @Test
@@ -2096,21 +2134,31 @@
     Change change3 = insert(project, newChangeForCommit(repo, commit3));
     RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
     Change change4 = insert(project, newChangeForCommit(repo, commit4));
+    RevCommit commit5 = repo.parseBody(repo.commit().message("Test\n\nFoo-Bar: baz").create());
+    Change change5 = insert(project, newChangeForCommit(repo, commit5));
+    RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\nFoo_Bar: baz").create());
+    insert(project, newChangeForCommit(repo, commit6));
 
     // create a changes with lines that look like footers, but which are not
-    RevCommit commit5 =
+    RevCommit commit7 =
         repo.parseBody(
             repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
-    Change change5 = insert(project, newChangeForCommit(repo, commit5));
-    RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
-    insert(project, newChangeForCommit(repo, commit6));
+    Change change7 = insert(project, newChangeForCommit(repo, commit7));
+    RevCommit commit8 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
+    insert(project, newChangeForCommit(repo, commit8));
 
     // matching by 'key=value' works
     assertQuery("footer:foo=bar", change3, change1);
     assertQuery("footer:foo=baz", change3, change2);
-    assertQuery("footer:Change-Id=" + change5.getKey(), change5);
+    assertQuery("footer:Change-Id=" + change7.getKey(), change7);
     assertQuery("footer:foo=bar=baz", change4);
 
+    // matching by 'key=value' works if footer name contains '-'
+    assertQuery("footer:Foo-Bar=baz", change5);
+
+    // matching by 'key=value' doesn't work if footer name contains '_' :(
+    assertQuery("footer:Foo_Bar=baz");
+
     // case doesn't matter
     assertQuery("footer:foo=BAR", change3, change1);
     assertQuery("footer:FOO=bar", change3, change1);
@@ -2119,9 +2167,15 @@
     // verbatim matching of footers works
     assertQuery("footer:\"foo: bar\"", change3, change1);
     assertQuery("footer:\"foo: baz\"", change3, change2);
-    assertQuery("footer:\"Change-Id: " + change5.getKey() + "\"", change5);
+    assertQuery("footer:\"Change-Id: " + change7.getKey() + "\"", change7);
     assertQuery("footer:\"foo: bar=baz\"", change4);
 
+    // verbatim matching works if footer name contains '-'
+    assertQuery("footer:\"Foo-Bar: baz\"", change5);
+
+    // verbatim matching doesn't work if footer name contains '_' :(
+    assertQuery("footer:\"Foo_Bar: baz\"");
+
     // expect no match because 'a=b: c' of commit6 is not a valid footer (footer key cannot contain
     // '=')
     assertQuery("footer:a=b=c");
@@ -2143,17 +2197,27 @@
     Change change1 = insert(project, newChangeForCommit(repo, commit1));
     RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nBaR: baz").create());
     Change change2 = insert(project, newChangeForCommit(repo, commit2));
+    RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nFoo-Bar: baz").create());
+    Change change3 = insert(project, newChangeForCommit(repo, commit3));
+    RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nFoo_Bar: baz").create());
+    insert(project, newChangeForCommit(repo, commit4));
 
-    // create a changes with lines that look like footers, but which are not
-    RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
-    insert(project, newChangeForCommit(repo, commit6));
+    // create a change with lines that look like footers, but which are not
+    RevCommit commit5 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
+    insert(project, newChangeForCommit(repo, commit5));
 
-    // matching by 'key=value' works
+    // matching by footer name works
     assertQuery("hasfooter:foo", change1);
 
     // case matters
     assertQuery("hasfooter:BaR", change2);
     assertQuery("hasfooter:Bar");
+
+    // matching footer name with '-' works
+    assertQuery("hasfooter:Foo-Bar", change3);
+
+    // matching footer name with '_' doesn't work :(
+    assertQuery("hasfooter:Foo_Bar");
   }
 
   @Test
@@ -3561,28 +3625,47 @@
     Change change2 = insert(project, newChange(repo));
     Change change3 = insert(project, newChange(repo));
 
+    Change change4 = insert(project, newChangeWithFiles(repo, "oldPath"));
+    ChangeIdentifier changeIdentifier4 =
+        ChangeIdentifier.byProjectAndNumericChangeId(project.get(), change4.getChangeId());
+
     // Change1 has one resolved comment (unresolvedcount = 0)
     // Change2 has one unresolved comment (unresolvedcount = 1)
     // Change3 has one resolved comment and one unresolved comment (unresolvedcount = 1)
+    // Change4 has one unresolved comment on a newly added file that will be renamed below
+    // (unresolvedcount = 1)
     addComment(change1, "comment 1", false);
     addComment(change2, "comment 2", true);
     addComment(change3, "comment 3", false);
     addComment(change3, "comment 4", true);
+    addComment(change4, "comment 5", true, "oldPath");
 
-    assertQuery("has:unresolved", change3, change2);
+    // Rename the file in Change4.
+    gApi.changes().id(changeIdentifier4).edit().create();
+    gApi.changes().id(changeIdentifier4).edit().renameFile("oldPath", "newPath");
+    gApi.changes().id(changeIdentifier4).edit().publish(new PublishChangeEditInput());
+
+    // Assert that the rename has been detected as such.
+    Map<String, FileInfo> files = gApi.changes().id(changeIdentifier4).current().files("1");
+    assertThat(files.keySet()).containsExactly(Patch.COMMIT_MSG, "newPath");
+    assertThat(files.get("newPath").oldPath).isEqualTo("oldPath");
+
+    assertQuery("has:unresolved", change4, change3, change2);
 
     assertQuery("unresolved:0", change1);
-    List<ChangeInfo> changeInfos = assertQuery("unresolved:>=0", change3, change2, change1);
-    assertThat(changeInfos.get(0).unresolvedCommentCount).isEqualTo(1); // Change3
-    assertThat(changeInfos.get(1).unresolvedCommentCount).isEqualTo(1); // Change2
-    assertThat(changeInfos.get(2).unresolvedCommentCount).isEqualTo(0); // Change1
-    assertQuery("unresolved:>0", change3, change2);
+    List<ChangeInfo> changeInfos =
+        assertQuery("unresolved:>=0", change4, change3, change2, change1);
+    assertThat(changeInfos.get(0).unresolvedCommentCount).isEqualTo(1); // Change4
+    assertThat(changeInfos.get(1).unresolvedCommentCount).isEqualTo(1); // Change3
+    assertThat(changeInfos.get(2).unresolvedCommentCount).isEqualTo(1); // Change2
+    assertThat(changeInfos.get(3).unresolvedCommentCount).isEqualTo(0); // Change1
+    assertQuery("unresolved:>0", change4, change3, change2);
 
     assertQuery("unresolved:<1", change1);
-    assertQuery("unresolved:<=1", change3, change2, change1);
-    assertQuery("unresolved:1", change3, change2);
+    assertQuery("unresolved:<=1", change4, change3, change2, change1);
+    assertQuery("unresolved:1", change4, change3, change2);
     assertQuery("unresolved:>1");
-    assertQuery("unresolved:>=1", change3, change2);
+    assertQuery("unresolved:>=1", change4, change3, change2);
   }
 
   @Test
@@ -4705,7 +4788,10 @@
   }
 
   protected void createProject(Project.NameKey project) throws Exception {
-    gApi.projects().create(project.get());
+    ProjectInput projectInput = new ProjectInput();
+    projectInput.name = project.get();
+    projectInput.createEmptyCommit = true;
+    gApi.projects().create(projectInput);
   }
 
   protected void createProject(Project.NameKey project, Project.NameKey parent) throws Exception {
@@ -4891,12 +4977,17 @@
   }
 
   private void addComment(Change change, String message, Boolean unresolved) throws Exception {
+    addComment(change, message, unresolved, Patch.COMMIT_MSG);
+  }
+
+  private void addComment(Change change, String message, Boolean unresolved, String filePath)
+      throws Exception {
     ReviewInput input = new ReviewInput();
     ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
     comment.line = 1;
     comment.message = message;
     comment.unresolved = unresolved;
-    input.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
+    input.comments = ImmutableMap.of(filePath, ImmutableList.of(comment));
     getChangeApi(change).current().review(input);
   }
 
diff --git a/javatests/com/google/gerrit/server/query/change/IsConsistentPredicateTest.java b/javatests/com/google/gerrit/server/query/change/IsConsistentPredicateTest.java
new file mode 100644
index 0000000..7c79f34
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/IsConsistentPredicateTest.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IsConsistentPredicateTest {
+  @Mock private ChangeNotes changeNotesMock;
+  @Mock private ChangeData changeData;
+
+  @Test
+  public void matchConsistent() {
+    IsConsistentPredicate isConsistentPredicate = new IsConsistentPredicate();
+    Change.Id changeId = Change.id(1);
+    int currentPS = 1;
+
+    ChangeData cd =
+        ChangeData.createForTest(
+            Project.nameKey("project"),
+            changeId,
+            currentPS,
+            ObjectId.zeroId(),
+            (sid, legacyChangeNum) -> changeId,
+            changeNotesMock);
+
+    assertTrue(isConsistentPredicate.match(cd));
+  }
+
+  @Test
+  public void matchInconsistent() {
+    IsConsistentPredicate isConsistentPredicate = new IsConsistentPredicate();
+    Change.Id changeId = Change.id(1);
+
+    when(changeData.notes()).thenThrow(new NoSuchChangeException(changeId));
+
+    assertFalse(isConsistentPredicate.match(changeData));
+  }
+}
diff --git a/javatests/com/google/gerrit/server/rules/prolog/BUILD b/javatests/com/google/gerrit/server/rules/prolog/BUILD
index ce02a06..bc96c8b 100644
--- a/javatests/com/google/gerrit/server/rules/prolog/BUILD
+++ b/javatests/com/google/gerrit/server/rules/prolog/BUILD
@@ -12,7 +12,7 @@
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/rules/prolog",
         "//java/com/google/gerrit/server/util/time",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib:jgit",
         "//lib/guice",
diff --git a/javatests/com/google/gerrit/server/update/context/BUILD b/javatests/com/google/gerrit/server/update/context/BUILD
index 65e544b..c91ecdb 100644
--- a/javatests/com/google/gerrit/server/update/context/BUILD
+++ b/javatests/com/google/gerrit/server/update/context/BUILD
@@ -6,8 +6,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/server",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
-        "//java/com/google/gerrit/testing:test-ref-update-context",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib:guava",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
diff --git a/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java b/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
index 55ec604..f2b481f 100644
--- a/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
+++ b/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
@@ -197,6 +197,7 @@
   }
 
   private static interface Callback {
+    @SuppressWarnings("unused")
     void callback();
   }
 }
diff --git a/javatests/com/google/gerrit/server/util/MarkdownImagesUtilTest.java b/javatests/com/google/gerrit/server/util/MarkdownImagesUtilTest.java
new file mode 100644
index 0000000..3fa074c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/util/MarkdownImagesUtilTest.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class MarkdownImagesUtilTest {
+
+  private static final String IMAGE_DATA =
+      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
+  private static final String MARKDOWN_IMAGE = "![alt text](" + IMAGE_DATA + ")";
+
+  @Test
+  public void stripImages_featureDisabled() {
+    MarkdownImagesUtil util = new MarkdownImagesUtil(new Config());
+    String comment = "Hello " + MARKDOWN_IMAGE + " world";
+    assertThat(util.stripImages(comment)).isEqualTo(comment);
+  }
+
+  @Test
+  public void replaceImages_featureDisabled() {
+    MarkdownImagesUtil util = new MarkdownImagesUtil(new Config());
+    String comment = "Hello " + MARKDOWN_IMAGE + " world";
+    assertThat(util.replaceImagesWithPlaceholder(comment)).isEqualTo(comment);
+  }
+
+  @Test
+  public void stripImages_featureEnabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    assertThat(util.stripImages("Hello " + MARKDOWN_IMAGE + " world")).isEqualTo("Hello   world");
+    assertThat(util.stripImages(MARKDOWN_IMAGE)).isEqualTo(" ");
+    assertThat(util.stripImages("No image here")).isEqualTo("No image here");
+    assertThat(util.stripImages(null)).isNull();
+    assertThat(util.stripImages("")).isEmpty();
+    assertThat(util.stripImages(" " + MARKDOWN_IMAGE + " ")).isEqualTo("   ");
+    assertThat(util.stripImages(MARKDOWN_IMAGE + " " + MARKDOWN_IMAGE).trim()).isEqualTo("");
+    assertThat(util.stripImages(MARKDOWN_IMAGE + " at the beginning"))
+        .isEqualTo("  at the beginning");
+    assertThat(util.stripImages("at the end " + MARKDOWN_IMAGE)).isEqualTo("at the end  ");
+  }
+
+  @Test
+  public void replaceImages_featureEnabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    assertThat(util.replaceImagesWithPlaceholder("Hello " + MARKDOWN_IMAGE + " world"))
+        .isEqualTo("Hello ![base64-image] world");
+    assertThat(util.replaceImagesWithPlaceholder(MARKDOWN_IMAGE)).isEqualTo("![base64-image]");
+    assertThat(util.replaceImagesWithPlaceholder("No image here")).isEqualTo("No image here");
+    assertThat(util.replaceImagesWithPlaceholder(null)).isNull();
+    assertThat(util.replaceImagesWithPlaceholder("")).isEmpty();
+  }
+
+  @Test
+  public void stripImages_withSpaces() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    String comment = "![alt]( " + IMAGE_DATA + " )";
+    assertThat(util.stripImages(comment)).doesNotContain("base64");
+  }
+
+  @Test
+  public void stripImages_withUppercaseMime() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    String upperData = IMAGE_DATA.replace("image/png", "IMAGE/PNG");
+    String comment = "![alt](" + upperData + ")";
+    assertThat(util.stripImages(comment)).doesNotContain("base64");
+  }
+
+  @Test
+  public void stripImages_withMixedCaseProtocol() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    String mixedData = IMAGE_DATA.replace("data:", "Data:");
+    String comment = "![alt](" + mixedData + ")";
+    assertThat(util.stripImages(comment)).doesNotContain("base64");
+  }
+
+  @Test
+  public void stripImages_withExtraParams() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    String dataWithParam = IMAGE_DATA.replace("base64,", "name=foo.png;base64,");
+    String comment = "![alt](" + dataWithParam + ")";
+    assertThat(util.stripImages(comment)).doesNotContain("base64");
+  }
+
+  @Test
+  public void stripImages_withNewlineInBase64() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowMarkdownBase64ImagesInComments", true);
+    MarkdownImagesUtil util = new MarkdownImagesUtil(cfg);
+
+    String dataWithNewline = IMAGE_DATA.substring(0, 30) + "\n" + IMAGE_DATA.substring(30);
+    String comment = "![alt](" + dataWithNewline + ")";
+    assertThat(util.stripImages(comment)).doesNotContain("base64");
+  }
+}
diff --git a/javatests/com/google/gerrit/sshd/BUILD b/javatests/com/google/gerrit/sshd/BUILD
index 44b9c62..1905818 100644
--- a/javatests/com/google/gerrit/sshd/BUILD
+++ b/javatests/com/google/gerrit/sshd/BUILD
@@ -8,7 +8,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/sshd",
-        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:gerrit-junit",
         "//lib/mina:sshd",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/util/http/testutil/BUILD b/javatests/com/google/gerrit/util/http/testutil/BUILD
index 3b4817b..4471b96 100644
--- a/javatests/com/google/gerrit/util/http/testutil/BUILD
+++ b/javatests/com/google/gerrit/util/http/testutil/BUILD
@@ -10,6 +10,7 @@
         "//lib:guava",
         "//lib:jgit",
         "//lib:servlet-api",
+        "//lib/errorprone:annotations",
         "//lib/httpcomponents:httpclient",
     ],
 )
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index 78b362d..c6b44be 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -25,6 +25,7 @@
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import java.io.BufferedReader;
 import java.io.UnsupportedEncodingException;
@@ -499,6 +500,7 @@
     throw new UnsupportedOperationException();
   }
 
+  @CanIgnoreReturnValue
   public FakeHttpServletRequest addHeader(String name, String value) {
     headers.put(name, value);
     return this;
diff --git a/lib/auto/BUILD b/lib/auto/BUILD
index 6e5418e..13a6665 100644
--- a/lib/auto/BUILD
+++ b/lib/auto/BUILD
@@ -27,6 +27,21 @@
 )
 
 java_plugin(
+    name = "auto-builder-plugin",
+    processor_class = "com.google.auto.value.processor.AutoBuilderProcessor",
+    deps = [
+        "@auto-common//jar",
+        "@auto-factory//jar",
+        "@auto-service-annotations//jar",
+        "@auto-value-annotations//jar",
+        "@auto-value//jar",
+        "@guava//jar",
+        "@javapoet//jar",
+        "@javax_inject//jar",
+    ],
+)
+
+java_plugin(
     name = "auto-value-plugin",
     processor_class = "com.google.auto.value.processor.AutoValueProcessor",
     deps = [
@@ -74,6 +89,7 @@
     data = ["//lib:LICENSE-Apache2.0"],
     exported_plugins = [
         ":auto-annotation-plugin",
+        ":auto-builder-plugin",
         ":auto-value-plugin",
         ":auto-oneof-plugin",
     ],
@@ -86,6 +102,7 @@
     data = ["//lib:LICENSE-Apache2.0"],
     exported_plugins = [
         ":auto-annotation-plugin",
+        ":auto-builder-plugin",
         ":auto-value-plugin",
         ":auto-oneof-plugin",
     ],
diff --git a/lib/emojis/BUILD b/lib/emojis/BUILD
index f87b101..2264434 100644
--- a/lib/emojis/BUILD
+++ b/lib/emojis/BUILD
@@ -14,6 +14,9 @@
     srcs = [
         ":emojis.js",
     ],
+    args = [
+        "--bundleConfigAsCjs=true",
+    ],
     config_file = "rollup.config.js",
     entry_point = "index.js",
     format = "iife",
@@ -21,8 +24,9 @@
     silent = True,
     sourcemap = "hidden",
     deps = [
+        "@tools_npm//@rollup/plugin-commonjs",
+        "@tools_npm//@rollup/plugin-node-resolve",
+        "@tools_npm//@rollup/plugin-terser",
         "@tools_npm//rollup",
-        "@tools_npm//rollup-plugin-commonjs",
-        "@tools_npm//rollup-plugin-node-resolve",
     ],
 )
diff --git a/lib/emojis/rollup.config.js b/lib/emojis/rollup.config.js
index 50b0be23..4e98b87 100644
--- a/lib/emojis/rollup.config.js
+++ b/lib/emojis/rollup.config.js
@@ -39,9 +39,9 @@
   return require(pluginPath);
 }
 
-const cjs = requirePlugin('rollup-plugin-commonjs');
-const nodeResolve = requirePlugin('rollup-plugin-node-resolve');
-const {terser} = requirePlugin('rollup-plugin-terser');
+const cjs = requirePlugin('@rollup/plugin-commonjs');
+const {nodeResolve} = requirePlugin('@rollup/plugin-node-resolve');
+const terser = requirePlugin('@rollup/plugin-terser');
 
 export default {
   onwarn: warning => {
@@ -58,13 +58,14 @@
     exports: 'auto',
     name: 'emojis',
     footer: '',
+    generatedCode: 'es2015',
     plugins: [
       terser({
+        ecma: 2021,
         output: {
           comments: false
         },
         compress: {
-          ecma: 2015,
           unsafe_arrows: true,
           passes: 2,
           unsafe: true,
@@ -77,9 +78,7 @@
   },
   plugins: [
     nodeResolve({
-      customResolveOptions: {
-        moduleDirectory: 'external/ui_npm/node_modules'
-      }
+      modulePaths: [path.join(process.cwd(), 'external/ui_npm/node_modules')]
     }),
     cjs(),
   ]
diff --git a/lib/highlightjs/BUILD b/lib/highlightjs/BUILD
index 8054816..9503f20 100644
--- a/lib/highlightjs/BUILD
+++ b/lib/highlightjs/BUILD
@@ -7,15 +7,25 @@
     licenses = ["notice"],
 )
 
+# Until https://github.com/highlightjs/highlightjs-GN/pull/4 is merged
+# we have to apply the fix manually.
+# Once it's merged, this can be removed and we can depend on
+# highlightjs-gn.
+exports_files(["gn.js"])
+
 rollup_bundle(
     name = "highlight.min",
     srcs = [
+        ":gn.js",
         "@ui_npm//highlight.js",
         "@ui_npm//highlightjs-closure-templates",
         "@ui_npm//highlightjs-epp",
         "@ui_npm//highlightjs-structured-text",
         "@ui_npm//highlightjs-vue",
     ],
+    args = [
+        "--bundleConfigAsCjs=true",
+    ],
     config_file = "rollup.config.js",
     entry_point = "index.js",
     format = "iife",
@@ -23,8 +33,9 @@
     silent = True,
     sourcemap = "hidden",
     deps = [
+        "@tools_npm//@rollup/plugin-commonjs",
+        "@tools_npm//@rollup/plugin-node-resolve",
+        "@tools_npm//@rollup/plugin-terser",
         "@tools_npm//rollup",
-        "@tools_npm//rollup-plugin-commonjs",
-        "@tools_npm//rollup-plugin-node-resolve",
     ],
 )
diff --git a/lib/highlightjs/gn.js b/lib/highlightjs/gn.js
new file mode 100644
index 0000000..406f57b
--- /dev/null
+++ b/lib/highlightjs/gn.js
@@ -0,0 +1,120 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Petr Hosek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/*
+ * highlight.js GN syntax highlighting definition
+ *
+ * @see https://github.com/highlightjs/highlight.js
+ *
+ * @package: highlightjs-GN
+ * @author:  Petr Hosek <petrhosek@gmail.com>
+ * @since:   2019-05-28
+ *
+ * Description: GN is a meta-build system that generates build files for Ninja.
+ * Category: common
+ */
+
+/** @type LanguageFn */
+export default function(hljs) {
+  var SUBST = {
+    className: 'subst', relevance: 2,
+    variants: [
+      {
+        begin: '\\$[A-Za-z0-9_]+'
+      },
+      {
+        begin: '\\${', end: '}',
+        contains: [{
+          className: 'variable',
+          begin: hljs.UNDERSCORE_IDENT_RE,
+          relevance: 0
+        }]
+      }
+    ],
+  };
+
+  var LINK = {
+    className: 'link', relevance: 5,
+    begin: ':\\w+',
+  }
+
+  var NUMBER = {
+    className: 'number', relevance: 0,
+    begin: hljs.NUMBER_RE
+  };
+
+  var STRING = {
+    className: 'string', relevance: 0,
+    begin: '"',
+    end: '"',
+    illegal: '\\n',
+    contains: [hljs.BACKSLASH_ESCAPE, SUBST, LINK],
+  };
+
+  var KEYWORDS = {
+    keyword:
+      'if else',
+    literal:
+      'true false ' +
+      'current_cpu current_os current_toolchain ' +
+      'default_toolchain host_cpu host_os ' +
+      'root_build_dir root_gen_dir root_out_dir ' +
+      'target_cpu target_gen_dir target_out_dir ' +
+      'target_os target_name invoker',
+    type:
+      'action action_foreach copy executable group ' +
+      'shared_library source_set static_library ' +
+      'loadable_module generated_file',
+    built_in:
+      'assert config declare_args defined exec_script ' +
+      'foreach get_label_info get_path_info ' +
+      'get_target_outputs getenv import print ' +
+      'process_file_template read_file rebase_path ' +
+      'set_default_toolchain set_defaults ' +
+      'set_sources_assignment_filter template tool ' +
+      'toolchain toolchain_args propagates_configs ' +
+      'write_file forward_variables_from target ' +
+      'get_name_info not_needed',
+    symbol:
+      'all_dependent_configs allow_circular_includes_from ' +
+      'args asmflags cflags cflags_c cflags_cc cflags_objc ' +
+      'cflags_objcc check_includes complete_static_lib ' +
+      'configs data data_deps defines depfile deps ' +
+      'include_dirs inputs ldflags lib_dirs libs ' +
+      'output_extension output_name outputs public ' +
+      'public_configs public_deps script sources testonly ' +
+      'visibility contents output_conversion rebase ' +
+      'data_keys walk_keys',
+  };
+
+  return {
+    aliases: ['gn', 'gni'],
+    keywords: KEYWORDS,
+    contains: [
+      NUMBER,
+      STRING,
+      hljs.HASH_COMMENT_MODE,
+    ]
+  };
+}
diff --git a/lib/highlightjs/index.js b/lib/highlightjs/index.js
index e958a04..7b9088a7 100644
--- a/lib/highlightjs/index.js
+++ b/lib/highlightjs/index.js
@@ -20,10 +20,12 @@
 import epp from 'highlightjs-epp';
 import iecst from 'highlightjs-structured-text';
 import vue from 'highlightjs-vue';
+import gn from './gn';
 
 hljs.registerLanguage('soy', soy);
 hljs.registerLanguage('epp', epp);
 hljs.registerLanguage('iecst', iecst);
 hljs.registerLanguage('vue', vue);
+hljs.registerLanguage('gn', gn);
 
 export default hljs;
diff --git a/lib/highlightjs/rollup.config.js b/lib/highlightjs/rollup.config.js
index c3a1340..85ab910 100644
--- a/lib/highlightjs/rollup.config.js
+++ b/lib/highlightjs/rollup.config.js
@@ -39,9 +39,9 @@
   return require(pluginPath);
 }
 
-const cjs = requirePlugin('rollup-plugin-commonjs');
-const nodeResolve = requirePlugin('rollup-plugin-node-resolve');
-const {terser} = requirePlugin('rollup-plugin-terser');
+const cjs = requirePlugin('@rollup/plugin-commonjs');
+const {nodeResolve} = requirePlugin('@rollup/plugin-node-resolve');
+const terser = requirePlugin('@rollup/plugin-terser');
 
 export default {
   onwarn: warning => {
@@ -58,28 +58,27 @@
     exports: 'auto',
     name: 'hljs',
     footer: '',
+    generatedCode: 'es2015',
     plugins: [
       terser({
+        ecma: 2021,
         output: {
           comments: false
         },
         compress: {
-          ecma: 2015,
           unsafe_arrows: true,
           passes: 2,
           unsafe: true,
           warnings: true,
           dead_code: true,
-          toplevel: "funcs"
+          toplevel: 'funcs'
         }
       })
     ]
   },
   plugins: [
     nodeResolve({
-      customResolveOptions: {
-        moduleDirectory: 'external/ui_npm/node_modules'
-      }
+      modulePaths: [path.join(process.cwd(), 'external/ui_npm/node_modules')]
     }),
     cjs(),
   ]
diff --git a/package.json b/package.json
index e79235e..2723b81 100644
--- a/package.json
+++ b/package.json
@@ -7,31 +7,48 @@
     "@bazel/rollup": "^5.8.1",
     "@bazel/terser": "^5.8.1",
     "@bazel/typescript": "^5.8.1",
-    "@typescript-eslint/parser": "^5.62.0"
+    "@typescript-eslint/parser": "^8.32.0"
   },
   "devDependencies": {
     "@koa/cors": "^5.0.0",
     "@types/page": "^1.11.9",
-    "@typescript-eslint/eslint-plugin": "^5.62.0",
-    "@web/dev-server": "^0.1.38",
-    "@web/dev-server-esbuild": "^0.3.6",
-    "eslint": "^8.57.1",
+    "@typescript-eslint/eslint-plugin": "^8.32.0",
+    "@web/dev-server": "^0.4.6",
+    "@web/dev-server-esbuild": "^1.0.4",
+    "eslint": "^9.26.0",
     "eslint-config-google": "^0.14.0",
-    "eslint-plugin-html": "^7.1.0",
+    "eslint-plugin-es-x": "8.0.0",
+    "eslint-plugin-html": "^8.1.3",
     "eslint-plugin-import": "^2.31.0",
-    "eslint-plugin-jsdoc": "^44.2.7",
+    "eslint-plugin-jsdoc": "^50.6.11",
     "eslint-plugin-lit": "^1.15.0",
-    "eslint-plugin-node": "^11.1.0",
-    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-n": "^17.17.0",
+    "eslint-plugin-prettier": "^5.4.0",
     "eslint-plugin-regex": "^1.10.0",
-    "gts": "^3.1.1",
-    "lit-analyzer": "^1.2.1",
+    "gts": "^6.0.2",
+    "lit-analyzer": "^2.0.3",
     "npm-run-all": "^4.1.5",
     "prettier": "^2.8.8",
-    "rollup": "^2.79.2",
-    "terser": "~5.8.0",
-    "ts-lit-plugin": "^1.2.1",
-    "typescript": "^4.9.5"
+    "rollup": "^4.49.0",
+    "terser": "~5.39.0",
+    "ts-lit-plugin": "^2.0.2",
+    "typescript": "5.8.2"
+  },
+  "dependencies // comments": {
+    "eslint": "eslint 9.26 upgrade in change 473401",
+    "@typescript-eslint/eslint-plugin": "eslint 9.26 upgrade in change 473401",
+    "@typescript-eslint/parser": "eslint 9.26 upgrade in change 473401",
+    "typescript": "must match go/ts-version at Google",
+    "vscode-css-languageservice": "for lit-analyzer, see change 487301",
+    "vscode-html-languageservice": "for lit-analyzer, see change 487301"
+  },
+  "resolutions": {
+    "eslint": "^9.26.0",
+    "@typescript-eslint/eslint-plugin": "^8.32.0",
+    "@typescript-eslint/parser": "^8.32.0",
+    "typescript": "5.8.2",
+    "vscode-css-languageservice": "^6.3.6",
+    "vscode-html-languageservice": "^5.5.0"
   },
   "scripts": {
     "setup": "yarn && yarn --cwd=polygerrit-ui && yarn --cwd=polygerrit-ui/app",
@@ -47,23 +64,21 @@
     "test:coverage": "yarn --cwd=polygerrit-ui test:coverage",
     "test:watch": "yarn --cwd=polygerrit-ui test:watch",
     "test:single": "yarn --cwd=polygerrit-ui test:single",
+    "test:single:nowatch": "yarn --cwd=polygerrit-ui test:single:nowatch",
     "test:single:coverage": "yarn --cwd=polygerrit-ui test:single:coverage",
     "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
+    "eslintfix:modified": "git diff --name-only --diff-filter=d | grep -E 'polygerrit-ui/app/.*\\.(js|ts)$' | sed 's|^polygerrit-ui/app/||' | xargs -r npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix",
     "litlint": "npm run safe_bazelisk run polygerrit-ui/app:lit_analysis",
-    "lint": "eslint -c polygerrit-ui/app/.eslintrc.js --ignore-path polygerrit-ui/app/.eslintignore polygerrit-ui/app",
+    "litlintforCI": "lit-analyzer --strict --rules.no-unknown-property off --rules.no-unknown-tag-name off --rules.no-incompatible-type-binding off --rules.no-incompatible-property-type off --rules.no-invalid-tag-name off --rules.no-property-visibility-mismatch off --rules.no-unknown-attribute off **/elements/**/*.ts",
+    "lint": "eslint -c polygerrit-ui/app/eslint-bazel.config.js polygerrit-ui/app",
     "gjf": "./tools/gjf.sh run"
   },
   "repository": {
     "type": "git",
     "url": "https://gerrit.googlesource.com/gerrit"
   },
-  "resolutions": {
-    "eslint": "^8.57.1",
-    "@typescript-eslint/eslint-plugin": "^5.62.0",
-    "@typescript-eslint/parser": "^5.62.0"
-  },
   "author": "",
   "license": "Apache-2.0"
 }
\ No newline at end of file
diff --git a/plugins/.eslintignore b/plugins/.eslintignore
deleted file mode 100644
index e69de29..0000000
--- a/plugins/.eslintignore
+++ /dev/null
diff --git a/plugins/.eslintrc.js b/plugins/.eslintrc.js
deleted file mode 100644
index 920ec2a..0000000
--- a/plugins/.eslintrc.js
+++ /dev/null
@@ -1,316 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * This is a base template for TypeScript plugins.
- *
- * When extending this template you have to:
- * - Set the __plugindir variable.
- */
-
-const path = require('path');
-
-module.exports = {
-  extends: ['eslint:recommended', 'google'],
-  parserOptions: {
-    ecmaVersion: 9,
-    sourceType: 'module',
-  },
-  env: {
-    browser: true,
-    es6: true,
-  },
-  rules: {
-    // https://eslint.org/docs/rules/no-confusing-arrow
-    'no-confusing-arrow': 'error',
-    // https://eslint.org/docs/rules/newline-per-chained-call
-    'newline-per-chained-call': ['error', {ignoreChainWithDepth: 2}],
-    // https://eslint.org/docs/rules/arrow-body-style
-    'arrow-body-style': ['error', 'as-needed',
-      {requireReturnForObjectLiteral: true}],
-    // https://eslint.org/docs/rules/arrow-parens
-    'arrow-parens': ['error', 'as-needed'],
-    // https://eslint.org/docs/rules/block-spacing
-    'block-spacing': ['error', 'always'],
-    // https://eslint.org/docs/rules/brace-style
-    'brace-style': ['error', '1tbs', {allowSingleLine: true}],
-    // https://eslint.org/docs/rules/camelcase
-    'camelcase': 'off',
-    // https://eslint.org/docs/rules/comma-dangle
-    'comma-dangle': ['error', {
-      arrays: 'always-multiline',
-      objects: 'always-multiline',
-      imports: 'always-multiline',
-      exports: 'always-multiline',
-      functions: 'never',
-    }],
-    // https://eslint.org/docs/rules/eol-last
-    'eol-last': 'off',
-    'guard-for-in': 'error',
-    // https://eslint.org/docs/rules/indent
-    'indent': ['error', 2, {
-      MemberExpression: 2,
-      FunctionDeclaration: {body: 1, parameters: 2},
-      FunctionExpression: {body: 1, parameters: 2},
-      CallExpression: {arguments: 2},
-      ArrayExpression: 1,
-      ObjectExpression: 1,
-      SwitchCase: 1,
-    }],
-    // https://eslint.org/docs/rules/keyword-spacing
-    'keyword-spacing': ['error', {after: true, before: true}],
-    // https://eslint.org/docs/rules/lines-between-class-members
-    'lines-between-class-members': ['error', 'always'],
-    // https://eslint.org/docs/rules/max-len
-    'max-len': [
-      'error',
-      80,
-      2,
-      {
-        ignoreComments: true,
-        ignorePattern: '^import .*;$',
-      },
-    ],
-    // https://eslint.org/docs/rules/new-cap
-    'new-cap': ['error', {
-      capIsNewExceptions: ['Polymer'],
-      capIsNewExceptionPattern: '^.*Mixin$',
-    }],
-    // https://eslint.org/docs/rules/no-console
-    'no-console': [
-      'error',
-      {allow: ['warn', 'error', 'info', 'assert', 'group', 'groupEnd']},
-    ],
-    // https://eslint.org/docs/rules/no-multiple-empty-lines
-    'no-multiple-empty-lines': ['error', {max: 1}],
-    // https://eslint.org/docs/rules/no-prototype-builtins
-    'no-prototype-builtins': 'off',
-    // https://eslint.org/docs/rules/no-redeclare
-    'no-redeclare': 'off',
-    // https://eslint.org/docs/rules/no-trailing-spaces
-    'no-trailing-spaces': 'error',
-    // https://eslint.org/docs/rules/no-irregular-whitespace
-    'no-irregular-whitespace': 'error',
-    // https://eslint.org/docs/rules/array-callback-return
-    'array-callback-return': ['error', {allowImplicit: true}],
-    // https://eslint.org/docs/rules/no-restricted-syntax
-    'no-restricted-syntax': [
-      'error',
-      {
-        selector: 'ExpressionStatement > CallExpression > ' +
-            'MemberExpression[object.name=\'test\'][property.name=\'only\']',
-        message: 'Remove test.only.',
-      },
-      {
-        selector: 'ExpressionStatement > CallExpression > ' +
-            'MemberExpression[object.name=\'suite\'][property.name=\'only\']',
-        message: 'Remove suite.only.',
-      },
-    ],
-    // no-undef disables global variable.
-    // "globals" declares allowed global variables.
-    // https://eslint.org/docs/rules/no-undef
-    'no-undef': ['error'],
-    // https://eslint.org/docs/rules/no-useless-escape
-    'no-useless-escape': 'off',
-    // https://eslint.org/docs/rules/no-var
-    'no-var': 'error',
-    // https://eslint.org/docs/rules/operator-linebreak
-    'operator-linebreak': 'off',
-    // https://eslint.org/docs/rules/object-shorthand
-    'object-shorthand': ['error', 'always'],
-    // https://eslint.org/docs/rules/padding-line-between-statements
-    'padding-line-between-statements': [
-      'error',
-      {
-        blankLine: 'always',
-        prev: 'class',
-        next: '*',
-      },
-      {
-        blankLine: 'always',
-        prev: '*',
-        next: 'class',
-      },
-    ],
-    // https://eslint.org/docs/rules/prefer-arrow-callback
-    'prefer-arrow-callback': 'error',
-    // https://eslint.org/docs/rules/prefer-const
-    'prefer-const': 'error',
-    // https://eslint.org/docs/rules/prefer-promise-reject-errors
-    'prefer-promise-reject-errors': 'error',
-    // https://eslint.org/docs/rules/prefer-spread
-    'prefer-spread': 'error',
-    // https://eslint.org/docs/rules/prefer-object-spread
-    'prefer-object-spread': 'error',
-    // https://eslint.org/docs/rules/quote-props
-    'quote-props': ['error', 'consistent-as-needed'],
-    // https://eslint.org/docs/rules/semi
-    'semi': ['error', 'always'],
-    // https://eslint.org/docs/rules/template-curly-spacing
-    'template-curly-spacing': 'error',
-
-    // https://eslint.org/docs/rules/require-jsdoc
-    'require-jsdoc': 0,
-    // https://eslint.org/docs/rules/valid-jsdoc
-    'valid-jsdoc': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-alignment
-    'jsdoc/check-alignment': 2,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-examples
-    'jsdoc/check-examples': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-indentation
-    'jsdoc/check-indentation': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-param-names
-    'jsdoc/check-param-names': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-syntax
-    'jsdoc/check-syntax': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-tag-names
-    'jsdoc/check-tag-names': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-types
-    'jsdoc/check-types': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-implements-on-classes
-    'jsdoc/implements-on-classes': 2,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-match-description
-    'jsdoc/match-description': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-types
-    'jsdoc/no-types': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-undefined-types
-    'jsdoc/no-undefined-types': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description
-    'jsdoc/require-description': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description-complete-sentence
-    'jsdoc/require-description-complete-sentence': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-example
-    'jsdoc/require-example': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-hyphen-before-param-description
-    'jsdoc/require-hyphen-before-param-description': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-jsdoc
-    'jsdoc/require-jsdoc': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param
-    'jsdoc/require-param': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-description
-    'jsdoc/require-param-description': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-name
-    'jsdoc/require-param-name': 2,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns
-    'jsdoc/require-returns': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-check
-    'jsdoc/require-returns-check': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description
-    'jsdoc/require-returns-description': 0,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types
-    'jsdoc/valid-types': 2,
-    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-file-overview
-    'jsdoc/require-file-overview': ['error', {
-      tags: {
-        license: {
-          mustExist: true,
-          preventDuplicates: true,
-        },
-      },
-    }],
-    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
-    'import/no-self-import': 2,
-    // The no-cycle rule is slow, because it doesn't cache dependencies.
-    // Disable it.
-    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-cycle.md
-    'import/no-cycle': 0,
-    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-useless-path-segments.md
-    'import/no-useless-path-segments': 2,
-    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unused-modules.md
-    'import/no-unused-modules': 2,
-    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
-    'import/no-default-export': 2,
-    // Prevents certain identifiers being used.
-    // Prefer flush() over flushAsynchronousOperations().
-    'id-blacklist': ['error', 'flushAsynchronousOperations'],
-  },
-
-  overrides: [
-    {
-      files: ['.eslintrc.js'],
-      env: {
-        browser: false,
-        es6: true,
-        node: true,
-      },
-    },
-    {
-      // .js-only rules
-      files: ['**/*.js'],
-      rules: {
-        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
-        'jsdoc/require-param-type': 2,
-        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
-        'jsdoc/require-returns-type': 2,
-        // The rule is required for .js files only, because typescript compiler
-        // always checks import.
-        'import/no-unresolved': 2,
-        'import/named': 2,
-      },
-    },
-    {
-      files: ['**/*.ts'],
-      extends: [require.resolve('gts/.eslintrc.json')],
-      rules: {
-        'no-restricted-imports': ['error', {
-          name: '@polymer/decorators/lib/decorators',
-          message: 'Use @polymer/decorators instead',
-        }],
-        '@typescript-eslint/no-explicit-any': 'error',
-        // See https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/9
-        '@typescript-eslint/ban-ts-comment': 'off',
-        // The following rules is required to match internal google rules
-        '@typescript-eslint/restrict-plus-operands': 'error',
-        '@typescript-eslint/no-unused-vars': [
-          'error',
-          {argsIgnorePattern: '^_'},
-        ],
-        // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
-        'node/no-unsupported-features/node-builtins': 'off',
-        // Disable no-invalid-this for ts files, because it incorrectly reports
-        // errors in some cases (see https://github.com/typescript-eslint/typescript-eslint/issues/491)
-        // At the same time, we are using typescript in a strict mode and
-        // it catches almost all errors related to invalid usage of this.
-        'no-invalid-this': 'off',
-
-        'node/no-extraneous-import': 'off',
-
-        // Typescript already checks for undef
-        'no-undef': 'off',
-
-        'jsdoc/no-types': 2,
-      },
-      parserOptions: {
-        // The __plugindir variable has to be defined by the plugin config.
-        project: path.resolve(__dirname, __plugindir, 'tsconfig.json'),
-      },
-    },
-  ],
-  plugins: [
-    'html',
-    'jsdoc',
-    'import',
-    'prettier',
-  ],
-  settings: {
-    'html/report-bad-indent': 'error',
-    'import/resolver': {
-      node: {},
-    },
-  },
-};
diff --git a/plugins/BUILD b/plugins/BUILD
index 0816fa1..e0400f8 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -11,8 +11,7 @@
 package(default_visibility = ["//visibility:public"])
 
 exports_files([
-    ".eslintrc.js",
-    ".eslintignore",
+    "eslint.config.js",
     ".prettierrc.js",
     "rollup.config.js",
     "tsconfig-plugins-base.json",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index efc967b..40606b3 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit efc967bbb3cf67ec95f70ae42e1943e428944dce
+Subproject commit 40606b33d0eca22d6fab9be560e5aa2bd07b0914
diff --git a/plugins/delete-project b/plugins/delete-project
index b1a05c0..e8fb93f 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit b1a05c0c2ef2d5fe60f0c4c5863576707540dbbf
+Subproject commit e8fb93fdae664baea65a1eba3231dd437eaa9f01
diff --git a/plugins/download-commands b/plugins/download-commands
index 471b944..bfb522f 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 471b9440b924ec32d87ff8a246f784dded5b345e
+Subproject commit bfb522f79240f4baf3398952998bded95a804af2
diff --git a/plugins/eslint.config.js b/plugins/eslint.config.js
new file mode 100644
index 0000000..d072a2a
--- /dev/null
+++ b/plugins/eslint.config.js
@@ -0,0 +1,328 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This is a base template for TypeScript plugins.
+ *
+ * When extending this template you have to:
+ * - Set the __plugindir variable.
+ */
+
+const path = require('path');
+const {defineConfig} = require('eslint/config');
+const js = require('@eslint/js');
+const {FlatCompat} = require('@eslint/eslintrc');
+
+const compat = new FlatCompat({
+  baseDirectory: __dirname,
+  recommendedConfig: js.configs.recommended,
+  allConfig: js.configs.all,
+});
+
+module.exports = defineConfig([
+  ...compat.config({
+    extends: ['eslint:recommended', 'google'],
+    parserOptions: {
+      ecmaVersion: 9,
+      sourceType: 'module',
+    },
+    env: {
+      browser: true,
+      es6: true,
+    },
+    rules: {
+      // https://eslint.org/docs/rules/no-confusing-arrow
+      'no-confusing-arrow': 'error',
+      // https://eslint.org/docs/rules/newline-per-chained-call
+      'newline-per-chained-call': ['error', {ignoreChainWithDepth: 2}],
+      // https://eslint.org/docs/rules/arrow-body-style
+      'arrow-body-style': ['error', 'as-needed',
+        {requireReturnForObjectLiteral: true}],
+      // https://eslint.org/docs/rules/arrow-parens
+      'arrow-parens': ['error', 'as-needed'],
+      // https://eslint.org/docs/rules/block-spacing
+      'block-spacing': ['error', 'always'],
+      // https://eslint.org/docs/rules/brace-style
+      'brace-style': ['error', '1tbs', {allowSingleLine: true}],
+      // https://eslint.org/docs/rules/camelcase
+      'camelcase': 'off',
+      // https://eslint.org/docs/rules/comma-dangle
+      'comma-dangle': ['error', {
+        arrays: 'always-multiline',
+        objects: 'always-multiline',
+        imports: 'always-multiline',
+        exports: 'always-multiline',
+        functions: 'never',
+      }],
+      // https://eslint.org/docs/rules/eol-last
+      'eol-last': 'off',
+      'guard-for-in': 'error',
+      // https://eslint.org/docs/rules/indent
+      'indent': ['error', 2, {
+        MemberExpression: 2,
+        FunctionDeclaration: {body: 1, parameters: 2},
+        FunctionExpression: {body: 1, parameters: 2},
+        CallExpression: {arguments: 2},
+        ArrayExpression: 1,
+        ObjectExpression: 1,
+        SwitchCase: 1,
+      }],
+      // https://eslint.org/docs/rules/keyword-spacing
+      'keyword-spacing': ['error', {after: true, before: true}],
+      // https://eslint.org/docs/rules/lines-between-class-members
+      'lines-between-class-members': ['error', 'always'],
+      // https://eslint.org/docs/rules/max-len
+      'max-len': [
+        'error',
+        80,
+        2,
+        {
+          ignoreComments: true,
+          ignorePattern: '^import .*;$',
+        },
+      ],
+      // https://eslint.org/docs/rules/new-cap
+      'new-cap': ['error', {
+        capIsNewExceptions: ['Polymer'],
+        capIsNewExceptionPattern: '^.*Mixin$',
+      }],
+      // https://eslint.org/docs/rules/no-console
+      'no-console': [
+        'error',
+        {allow: ['warn', 'error', 'info', 'assert', 'group', 'groupEnd']},
+      ],
+      // https://eslint.org/docs/rules/no-multiple-empty-lines
+      'no-multiple-empty-lines': ['error', {max: 1}],
+      // https://eslint.org/docs/rules/no-prototype-builtins
+      'no-prototype-builtins': 'off',
+      // https://eslint.org/docs/rules/no-redeclare
+      'no-redeclare': 'off',
+      // https://eslint.org/docs/rules/no-trailing-spaces
+      'no-trailing-spaces': 'error',
+      // https://eslint.org/docs/rules/no-irregular-whitespace
+      'no-irregular-whitespace': 'error',
+      // https://eslint.org/docs/rules/array-callback-return
+      'array-callback-return': ['error', {allowImplicit: true}],
+      // https://eslint.org/docs/rules/no-restricted-syntax
+      'no-restricted-syntax': [
+        'error',
+        {
+          selector: 'ExpressionStatement > CallExpression > ' +
+              'MemberExpression[object.name=\'test\'][property.name=\'only\']',
+          message: 'Remove test.only.',
+        },
+        {
+          selector: 'ExpressionStatement > CallExpression > ' +
+              'MemberExpression[object.name=\'suite\'][property.name=\'only\']',
+          message: 'Remove suite.only.',
+        },
+      ],
+      // no-undef disables global variable.
+      // "globals" declares allowed global variables.
+      // https://eslint.org/docs/rules/no-undef
+      'no-undef': ['error'],
+      // https://eslint.org/docs/rules/no-useless-escape
+      'no-useless-escape': 'off',
+      // https://eslint.org/docs/rules/no-var
+      'no-var': 'error',
+      // https://eslint.org/docs/rules/operator-linebreak
+      'operator-linebreak': 'off',
+      // https://eslint.org/docs/rules/object-shorthand
+      'object-shorthand': ['error', 'always'],
+      // https://eslint.org/docs/rules/padding-line-between-statements
+      'padding-line-between-statements': [
+        'error',
+        {
+          blankLine: 'always',
+          prev: 'class',
+          next: '*',
+        },
+        {
+          blankLine: 'always',
+          prev: '*',
+          next: 'class',
+        },
+      ],
+      // https://eslint.org/docs/rules/prefer-arrow-callback
+      'prefer-arrow-callback': 'error',
+      // https://eslint.org/docs/rules/prefer-const
+      'prefer-const': 'error',
+      // https://eslint.org/docs/rules/prefer-promise-reject-errors
+      'prefer-promise-reject-errors': 'error',
+      // https://eslint.org/docs/rules/prefer-spread
+      'prefer-spread': 'error',
+      // https://eslint.org/docs/rules/prefer-object-spread
+      'prefer-object-spread': 'error',
+      // https://eslint.org/docs/rules/quote-props
+      'quote-props': ['error', 'consistent-as-needed'],
+      // https://eslint.org/docs/rules/semi
+      'semi': ['error', 'always'],
+      // https://eslint.org/docs/rules/template-curly-spacing
+      'template-curly-spacing': 'error',
+
+      // https://eslint.org/docs/rules/require-jsdoc
+      'require-jsdoc': 0,
+      // https://eslint.org/docs/rules/valid-jsdoc
+      'valid-jsdoc': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-alignment
+      'jsdoc/check-alignment': 2,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-examples
+      'jsdoc/check-examples': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-indentation
+      'jsdoc/check-indentation': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-param-names
+      'jsdoc/check-param-names': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-syntax
+      'jsdoc/check-syntax': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-tag-names
+      'jsdoc/check-tag-names': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-types
+      'jsdoc/check-types': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-implements-on-classes
+      'jsdoc/implements-on-classes': 2,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-match-description
+      'jsdoc/match-description': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-types
+      'jsdoc/no-types': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-undefined-types
+      'jsdoc/no-undefined-types': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description
+      'jsdoc/require-description': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description-complete-sentence
+      'jsdoc/require-description-complete-sentence': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-example
+      'jsdoc/require-example': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-hyphen-before-param-description
+      'jsdoc/require-hyphen-before-param-description': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-jsdoc
+      'jsdoc/require-jsdoc': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param
+      'jsdoc/require-param': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-description
+      'jsdoc/require-param-description': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-name
+      'jsdoc/require-param-name': 2,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns
+      'jsdoc/require-returns': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-check
+      'jsdoc/require-returns-check': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description
+      'jsdoc/require-returns-description': 0,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types
+      'jsdoc/valid-types': 2,
+      // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-file-overview
+      'jsdoc/require-file-overview': ['error', {
+        tags: {
+          license: {
+            mustExist: true,
+            preventDuplicates: true,
+          },
+        },
+      }],
+      // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
+      'import/no-self-import': 2,
+      // The no-cycle rule is slow, because it doesn't cache dependencies.
+      // Disable it.
+      // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-cycle.md
+      'import/no-cycle': 0,
+      // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-useless-path-segments.md
+      'import/no-useless-path-segments': 2,
+      // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unused-modules.md
+      'import/no-unused-modules': 2,
+      // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
+      'import/no-default-export': 2,
+      // Prevents certain identifiers being used.
+      // Prefer flush() over flushAsynchronousOperations().
+      'id-blacklist': ['error', 'flushAsynchronousOperations'],
+    },
+
+    overrides: [
+      {
+        files: ['eslint.config.js'],
+        env: {
+          browser: false,
+          es6: true,
+          node: true,
+        },
+      },
+      {
+        // .js-only rules
+        files: ['**/*.js'],
+        rules: {
+          // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
+          'jsdoc/require-param-type': 2,
+          // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
+          'jsdoc/require-returns-type': 2,
+          // The rule is required for .js files only, because typescript compiler
+          // always checks import.
+          'import/no-unresolved': 2,
+          'import/named': 2,
+        },
+      },
+      {
+        files: ['**/*.ts'],
+        extends: [require.resolve('gts/.eslintrc.json')],
+        rules: {
+          'no-restricted-imports': ['error', {
+            name: '@polymer/decorators/lib/decorators',
+            message: 'Use @polymer/decorators instead',
+          }],
+          '@typescript-eslint/no-explicit-any': 'error',
+          // See https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/9
+          '@typescript-eslint/ban-ts-comment': 'off',
+          // The following rules is required to match internal google rules
+          '@typescript-eslint/restrict-plus-operands': 'error',
+          '@typescript-eslint/no-unused-vars': [
+            'error',
+            {argsIgnorePattern: '^_'},
+          ],
+          // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unsupported-features/node-builtins.md
+          'n/no-unsupported-features/node-builtins': 'off',
+          // Disable no-invalid-this for ts files, because it incorrectly reports
+          // errors in some cases (see https://github.com/typescript-eslint/typescript-eslint/issues/491)
+          // At the same time, we are using typescript in a strict mode and
+          // it catches almost all errors related to invalid usage of this.
+          'no-invalid-this': 'off',
+
+          'n/no-extraneous-import': 'off',
+
+          // Typescript already checks for undef
+          'no-undef': 'off',
+
+          'jsdoc/no-types': 2,
+        },
+        parserOptions: {
+          // The __plugindir variable has to be defined by the plugin config.
+          project: path.resolve(__dirname, __plugindir, 'tsconfig.json'),
+        },
+      },
+    ],
+    plugins: [
+      'html',
+      'jsdoc',
+      'import',
+      'prettier',
+    ],
+    settings: {
+      'html/report-bad-indent': 'error',
+      'import/resolver': {
+        node: {},
+      },
+    },
+  }),
+]);
+
diff --git a/plugins/hooks b/plugins/hooks
index 83d5aae..7d4f143 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 83d5aae0fce1956858c1b5595012e68867c53437
+Subproject commit 7d4f143a988c8f5595000684c1f0dd4501546e48
diff --git a/plugins/package.json b/plugins/package.json
index 41ff1af..422892f 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -5,40 +5,41 @@
   "dependencies": {
     "@codemirror/autocomplete": "^6.18.6",
     "@codemirror/commands": "^6.8.1",
-    "@codemirror/lang-cpp": "^6.0.2",
+    "@codemirror/lang-cpp": "^6.0.3",
     "@codemirror/lang-css": "^6.3.1",
     "@codemirror/lang-go": "^6.0.1",
     "@codemirror/lang-html": "^6.4.9",
-    "@codemirror/lang-java": "^6.0.1",
+    "@codemirror/lang-java": "^6.0.2",
     "@codemirror/lang-javascript": "^6.2.4",
-    "@codemirror/lang-json": "^6.0.1",
+    "@codemirror/lang-json": "^6.0.2",
     "@codemirror/lang-less": "^6.0.2",
-    "@codemirror/lang-markdown": "^6.3.2",
-    "@codemirror/lang-php": "^6.0.1",
+    "@codemirror/lang-markdown": "^6.3.4",
+    "@codemirror/lang-php": "^6.0.2",
     "@codemirror/lang-python": "^6.2.1",
-    "@codemirror/lang-rust": "^6.0.1",
+    "@codemirror/lang-rust": "^6.0.2",
     "@codemirror/lang-sass": "^6.0.2",
-    "@codemirror/lang-sql": "^6.8.0",
+    "@codemirror/lang-sql": "^6.9.1",
     "@codemirror/lang-vue": "^0.1.3",
     "@codemirror/lang-xml": "^6.1.0",
     "@codemirror/lang-yaml": "^6.1.2",
-    "@codemirror/language": "^6.11.0",
+    "@codemirror/language": "^6.11.3",
     "@codemirror/language-data": "^6.5.1",
     "@codemirror/legacy-modes": "^6.5.1",
     "@codemirror/lint": "^6.8.5",
     "@codemirror/search": "^6.5.11",
     "@codemirror/state": "^6.5.2",
-    "@codemirror/view": "^6.36.8",
+    "@codemirror/view": "^6.38.1",
+    "@gerritcodereview/typescript-api": "3.13.0",
     "@lezer/highlight": "^1.2.1",
-    "@gerritcodereview/typescript-api": "3.12.0",
-    "@open-wc/testing": "^3.2.2",
+    "@material/web": "^2.4.0",
+    "@open-wc/testing": "^4.0.0",
     "@polymer/decorators": "^3.0.0",
     "@polymer/polymer": "3.5.1",
-    "@web/dev-server-esbuild": "^0.3.6",
-    "@web/test-runner": "^0.15.3",
-    "lit": "^3.2.1",
+    "@web/dev-server-esbuild": "^1.0.4",
+    "@web/test-runner": "^0.20.2",
+    "lit": "^3.3.1",
     "rxjs": "^6.6.7",
-    "sinon": "^13.0.2"
+    "sinon": "^20.0.0"
   },
   "dependencies // comments": {
     "@polymer/polymer": [
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index ed7870e..578aa64 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit ed7870eb3c8b6e48511d0eb3bd54606927b46019
+Subproject commit 578aa64d05fa982f41c2db72253c9b43c3f0d03e
diff --git a/plugins/replication b/plugins/replication
index 7a8f342..33fc916 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 7a8f3429ed555d3576ddc598cf6d6c6ecb453cbd
+Subproject commit 33fc916b0425bdbfbba6fc96047de5b5a0bb8777
diff --git a/plugins/rollup.config.js b/plugins/rollup.config.js
index 6fb3784..4ab9907 100644
--- a/plugins/rollup.config.js
+++ b/plugins/rollup.config.js
@@ -38,7 +38,7 @@
   return require(pluginPath);
 }
 
-const resolve = requirePlugin('rollup-plugin-node-resolve');
+const {nodeResolve} = requirePlugin('@rollup/plugin-node-resolve');
 
 export default {
   treeshake: false,
@@ -51,9 +51,7 @@
   },
   // Context must be set to window to correctly process global variables
   context: 'window',
-  plugins: [resolve({
-    customResolveOptions: {
-      moduleDirectory: 'external/plugins_npm/node_modules',
-    },
+  plugins: [nodeResolve({
+    modulePaths: [path.join(process.cwd(), 'external/plugins_npm/node_modules')]
   })],
 };
diff --git a/plugins/tsconfig-plugins-base.json b/plugins/tsconfig-plugins-base.json
index b7e9d52..605c69a 100644
--- a/plugins/tsconfig-plugins-base.json
+++ b/plugins/tsconfig-plugins-base.json
@@ -1,8 +1,8 @@
 {
   "compilerOptions": {
     /* Basic Options */
-    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "inlineSourceMap": true, /* Generates corresponding '.map' file. */
     "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
     "removeComments": false, /* Emit comments to output */
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 1aff309..0a67116 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -3,18 +3,18 @@
 
 
 "@babel/code-frame@^7.12.11":
-  version "7.26.2"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
-  integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
+  version "7.27.1"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
+  integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
   dependencies:
-    "@babel/helper-validator-identifier" "^7.25.9"
+    "@babel/helper-validator-identifier" "^7.27.1"
     js-tokens "^4.0.0"
-    picocolors "^1.0.0"
+    picocolors "^1.1.1"
 
-"@babel/helper-validator-identifier@^7.25.9":
-  version "7.25.9"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
-  integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
+"@babel/helper-validator-identifier@^7.27.1":
+  version "7.27.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
+  integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
 
 "@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.18.6", "@codemirror/autocomplete@^6.3.2", "@codemirror/autocomplete@^6.7.1":
   version "6.18.6"
@@ -48,10 +48,10 @@
     "@lezer/highlight" "^1.0.0"
     "@lezer/lr" "^1.3.3"
 
-"@codemirror/lang-cpp@^6.0.0", "@codemirror/lang-cpp@^6.0.2":
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz#076c98340c3beabde016d7d83e08eebe17254ef9"
-  integrity sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==
+"@codemirror/lang-cpp@^6.0.0", "@codemirror/lang-cpp@^6.0.3":
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz#b175b59fcde8dd6e563b7feee8bbed81963a9491"
+  integrity sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==
   dependencies:
     "@codemirror/language" "^6.0.0"
     "@lezer/cpp" "^1.0.0"
@@ -93,10 +93,10 @@
     "@lezer/css" "^1.1.0"
     "@lezer/html" "^1.3.0"
 
-"@codemirror/lang-java@^6.0.0", "@codemirror/lang-java@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz#03bd06334da7c8feb9dff6db01ac6d85bd2e48bb"
-  integrity sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==
+"@codemirror/lang-java@^6.0.0", "@codemirror/lang-java@^6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-java/-/lang-java-6.0.2.tgz#601d5b3d774a4a997d11647ccb6c05702c54bd5b"
+  integrity sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==
   dependencies:
     "@codemirror/language" "^6.0.0"
     "@lezer/java" "^1.0.0"
@@ -114,10 +114,10 @@
     "@lezer/common" "^1.0.0"
     "@lezer/javascript" "^1.0.0"
 
-"@codemirror/lang-json@^6.0.0", "@codemirror/lang-json@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz#0a0be701a5619c4b0f8991f9b5e95fe33f462330"
-  integrity sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==
+"@codemirror/lang-json@^6.0.0", "@codemirror/lang-json@^6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.2.tgz#054b160671306667e25d80385286049841836179"
+  integrity sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==
   dependencies:
     "@codemirror/language" "^6.0.0"
     "@lezer/json" "^1.0.0"
@@ -134,9 +134,9 @@
     "@lezer/lr" "^1.0.0"
 
 "@codemirror/lang-liquid@^6.0.0":
-  version "6.2.3"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-liquid/-/lang-liquid-6.2.3.tgz#cbdb38cbf2c59bc334292420401f88aa6c4b4b27"
-  integrity sha512-yeN+nMSrf/lNii3FJxVVEGQwFG0/2eDyH6gNOj+TGCa0hlNO4bhQnoO5ISnd7JOG+7zTEcI/GOoyraisFVY7jQ==
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz#f77fdc4b6c5062ba7ce5525f9839d1e9d7a0e356"
+  integrity sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==
   dependencies:
     "@codemirror/autocomplete" "^6.0.0"
     "@codemirror/lang-html" "^6.0.0"
@@ -147,10 +147,10 @@
     "@lezer/highlight" "^1.0.0"
     "@lezer/lr" "^1.3.1"
 
-"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.3.2":
-  version "6.3.2"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz#841a922c9305c035678600de5187c1b7a80f6c21"
-  integrity sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==
+"@codemirror/lang-markdown@^6.0.0", "@codemirror/lang-markdown@^6.3.4":
+  version "6.3.4"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz#c1c021ba65ba21c4e9b1f6d5add809b4c9e02c8d"
+  integrity sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==
   dependencies:
     "@codemirror/autocomplete" "^6.7.1"
     "@codemirror/lang-html" "^6.0.0"
@@ -160,10 +160,10 @@
     "@lezer/common" "^1.2.1"
     "@lezer/markdown" "^1.0.0"
 
-"@codemirror/lang-php@^6.0.0", "@codemirror/lang-php@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-php/-/lang-php-6.0.1.tgz#fa34cc75562178325861a5731f79bd621f57ffaa"
-  integrity sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==
+"@codemirror/lang-php@^6.0.0", "@codemirror/lang-php@^6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-php/-/lang-php-6.0.2.tgz#bdc439d195c8e73513bc5b971a99a57b5c99ee55"
+  integrity sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==
   dependencies:
     "@codemirror/lang-html" "^6.0.0"
     "@codemirror/language" "^6.0.0"
@@ -182,10 +182,10 @@
     "@lezer/common" "^1.2.1"
     "@lezer/python" "^1.1.4"
 
-"@codemirror/lang-rust@^6.0.0", "@codemirror/lang-rust@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz#d6829fc7baa39a15bcd174a41a9e0a1bf7cf6ba8"
-  integrity sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==
+"@codemirror/lang-rust@^6.0.0", "@codemirror/lang-rust@^6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz#69146e6b3e8f961ef149059aecb9e07bfd7bf3bd"
+  integrity sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==
   dependencies:
     "@codemirror/language" "^6.0.0"
     "@lezer/rust" "^1.0.0"
@@ -201,10 +201,10 @@
     "@lezer/common" "^1.0.2"
     "@lezer/sass" "^1.0.0"
 
-"@codemirror/lang-sql@^6.0.0", "@codemirror/lang-sql@^6.8.0":
-  version "6.8.0"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz#1ae68ad49f378605ff88a4cc428ba667ce056068"
-  integrity sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==
+"@codemirror/lang-sql@^6.0.0", "@codemirror/lang-sql@^6.9.1":
+  version "6.9.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-sql/-/lang-sql-6.9.1.tgz#9eb9f7203afef6f0f344dbaf306c8a2c6279001d"
+  integrity sha512-ecSk3gm/mlINcURMcvkCZmXgdzPSq8r/yfCtTB4vgqGGIbBC2IJIAy7GqYTy5pgBEooTVmHP2GZK6Z7h63CDGg==
   dependencies:
     "@codemirror/autocomplete" "^6.0.0"
     "@codemirror/language" "^6.0.0"
@@ -288,10 +288,10 @@
     "@codemirror/language" "^6.0.0"
     "@codemirror/legacy-modes" "^6.4.0"
 
-"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.0", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.0.tgz#5ae90972601497f4575f30811519d720bf7232c9"
-  integrity sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.3", "@codemirror/language@^6.3.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0":
+  version "6.11.3"
+  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.3.tgz#8e6632df566a7ed13a1bd307f9837765bb1abfdd"
+  integrity sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==
   dependencies:
     "@codemirror/state" "^6.0.0"
     "@codemirror/view" "^6.23.0"
@@ -332,124 +332,145 @@
   dependencies:
     "@marijn/find-cluster-break" "^1.0.0"
 
-"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.36.8":
-  version "6.36.8"
-  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.8.tgz#d72e59d2b2d99bb5f177178f63e11cef6e605b94"
-  integrity sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.38.1":
+  version "6.38.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.38.1.tgz#74214434351719ec0710431363a85f7a01e80a73"
+  integrity sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==
   dependencies:
     "@codemirror/state" "^6.5.0"
+    crelt "^1.0.6"
     style-mod "^4.1.0"
     w3c-keyname "^2.2.4"
 
-"@esbuild/android-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
-  integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
+"@esbuild/aix-ppc64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
+  integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
 
-"@esbuild/android-arm@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
-  integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
+"@esbuild/android-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
+  integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
 
-"@esbuild/android-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
-  integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
+"@esbuild/android-arm@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
+  integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
 
-"@esbuild/darwin-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
-  integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
+"@esbuild/android-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
+  integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
 
-"@esbuild/darwin-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
-  integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
+"@esbuild/darwin-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae"
+  integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
 
-"@esbuild/freebsd-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
-  integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
+"@esbuild/darwin-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
+  integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
 
-"@esbuild/freebsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
-  integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
+"@esbuild/freebsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
+  integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
 
-"@esbuild/linux-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
-  integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
+"@esbuild/freebsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
+  integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
 
-"@esbuild/linux-arm@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
-  integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
+"@esbuild/linux-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
+  integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
 
-"@esbuild/linux-ia32@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
-  integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
+"@esbuild/linux-arm@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
+  integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
 
-"@esbuild/linux-loong64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
-  integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
+"@esbuild/linux-ia32@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
+  integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
 
-"@esbuild/linux-mips64el@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
-  integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
+"@esbuild/linux-loong64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
+  integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
 
-"@esbuild/linux-ppc64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
-  integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
+"@esbuild/linux-mips64el@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
+  integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
 
-"@esbuild/linux-riscv64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
-  integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
+"@esbuild/linux-ppc64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
+  integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
 
-"@esbuild/linux-s390x@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
-  integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
+"@esbuild/linux-riscv64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
+  integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
 
-"@esbuild/linux-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
-  integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
+"@esbuild/linux-s390x@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
+  integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
 
-"@esbuild/netbsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
-  integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
+"@esbuild/linux-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f"
+  integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
 
-"@esbuild/openbsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
-  integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
+"@esbuild/netbsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
+  integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
 
-"@esbuild/sunos-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
-  integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
+"@esbuild/netbsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
+  integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
 
-"@esbuild/win32-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
-  integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
+"@esbuild/openbsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
+  integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
 
-"@esbuild/win32-ia32@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
-  integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
+"@esbuild/openbsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
+  integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
 
-"@esbuild/win32-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
-  integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
+"@esbuild/openharmony-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
+  integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
+
+"@esbuild/sunos-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
+  integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
+
+"@esbuild/win32-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
+  integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
+
+"@esbuild/win32-ia32@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
+  integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
+
+"@esbuild/win32-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
+  integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
 
 "@esm-bundle/chai@^4.3.4-fix.0":
   version "4.3.4-fix.0"
@@ -458,10 +479,10 @@
   dependencies:
     "@types/chai" "^4.2.12"
 
-"@gerritcodereview/typescript-api@3.12.0":
-  version "3.12.0"
-  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.12.0.tgz#c851217a30a665aae1af81c5a050f4ad854298bc"
-  integrity sha512-GjPj9gshoN6KhC63e7/oa9sq0OgWbaqY4vOWJzu9Ho0IwG74j/AsB4mXNMdUC26obYwUMrjgWsdWdaAFB09UpQ==
+"@gerritcodereview/typescript-api@3.13.0":
+  version "3.13.0"
+  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.13.0.tgz#c7f5ac20f8b5b575424508eb1deb260a290ca0f1"
+  integrity sha512-1TvqARSNO9SXnnCzJKZVz9SFTr4hx1ZzBR0jyRZM6Xd4lLw3YwCfXozD9E8Pm4pfBWmN9WDkURboEG85p7ICag==
 
 "@hapi/bourne@^3.0.0":
   version "3.0.0"
@@ -474,14 +495,14 @@
   integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
 
 "@jridgewell/sourcemap-codec@^1.4.14":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
-  integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+  integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
 
 "@jridgewell/trace-mapping@^0.3.12":
-  version "0.3.25"
-  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
-  integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
+  version "0.3.30"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99"
+  integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==
   dependencies:
     "@jridgewell/resolve-uri" "^3.1.0"
     "@jridgewell/sourcemap-codec" "^1.4.14"
@@ -501,9 +522,9 @@
     "@lezer/lr" "^1.0.0"
 
 "@lezer/css@^1.1.0", "@lezer/css@^1.1.7":
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.2.1.tgz#b35f6d0459e9be4de1cdf4d3132a59efd7cf2ba3"
-  integrity sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg==
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.3.0.tgz#296f298814782c2fad42a936f3510042cdcd2034"
+  integrity sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==
   dependencies:
     "@lezer/common" "^1.2.0"
     "@lezer/highlight" "^1.0.0"
@@ -577,9 +598,9 @@
     "@lezer/highlight" "^1.0.0"
 
 "@lezer/php@^1.0.0":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@lezer/php/-/php-1.0.2.tgz#7c291631fc1e7f7efe99977522bc48bdc732658a"
-  integrity sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@lezer/php/-/php-1.0.4.tgz#b759a4ac71ca318aa7855971bdb65082b9d69e65"
+  integrity sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==
   dependencies:
     "@lezer/common" "^1.2.0"
     "@lezer/highlight" "^1.0.0"
@@ -630,23 +651,31 @@
     "@lezer/highlight" "^1.0.0"
     "@lezer/lr" "^1.4.0"
 
-"@lit-labs/ssr-dom-shim@^1.2.0":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz#a28799c463177d1a0b0e5cefdc173da5ac859eb4"
-  integrity sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==
+"@lit-labs/ssr-dom-shim@^1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz#55eb80ab5ef6e188f7e541c1e2bea1ef582413b8"
+  integrity sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==
 
-"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.4":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b"
-  integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==
+"@lit/reactive-element@^2.1.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.1.1.tgz#0662ac4a43d4898974aef9a6c5cd47b9e331919a"
+  integrity sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==
   dependencies:
-    "@lit-labs/ssr-dom-shim" "^1.2.0"
+    "@lit-labs/ssr-dom-shim" "^1.4.0"
 
 "@marijn/find-cluster-break@^1.0.0":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
   integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
 
+"@material/web@^2.4.0":
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/@material/web/-/web-2.4.0.tgz#4158df5afa2a36f55db5a5b2d77146cf6bdc84a6"
+  integrity sha512-2jYiPIYOuP2UcWXal4VdKRlkpBX02U3rgXMWp0yFQbJaOmK8MOIA3BsyuooM3VOYI+VCO70BeH1x43y5cBLvZQ==
+  dependencies:
+    lit "^2.8.0 || ^3.0.0"
+    tslib "^2.4.0"
+
 "@mdn/browser-compat-data@^4.0.0":
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8"
@@ -673,18 +702,18 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@open-wc/dedupe-mixin@^1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz#b3c58f8699b197bb5e923d624c720e67c9f324d6"
-  integrity sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==
+"@open-wc/dedupe-mixin@^2.0.0":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-2.0.1.tgz#3bcd009c63a4713d8ce4e304a72eb33977e374ac"
+  integrity sha512-+R4VxvceUxHAUJXJQipkkoV9fy10vNo+OnUnGKZnVmcwxMl460KLzytnUM4S35SI073R0yZQp9ra0MbPUwVcEA==
 
-"@open-wc/scoped-elements@^2.2.4":
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.2.4.tgz#081559b62d885ac0ec043546f17f1f680294d500"
-  integrity sha512-12X4F4QGPWcvPbxAiJ4v8wQFCOu+laZHRGfTrkoj+3JzACCtuxHG49YbuqVzQ135QPKCuhP9wA0kpGGEfUegyg==
+"@open-wc/scoped-elements@^3.0.2":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-3.0.6.tgz#9ba4a316d39f2b29dc188f34a7f5e4e1fc9dded1"
+  integrity sha512-w1ayJaUUmBw8tALtqQ6cBueld+op+bufujzbrOdH0uCTXnSQkONYZzOH+9jyQ8auVgKLqcxZ8oU6SzfqQhQkPg==
   dependencies:
-    "@lit/reactive-element" "^1.0.0 || ^2.0.0"
-    "@open-wc/dedupe-mixin" "^1.4.0"
+    "@open-wc/dedupe-mixin" "^2.0.0"
+    lit "^3.0.0"
 
 "@open-wc/semantic-dom-diff@^0.20.0":
   version "0.20.1"
@@ -694,23 +723,23 @@
     "@types/chai" "^4.3.1"
     "@web/test-runner-commands" "^0.9.0"
 
-"@open-wc/testing-helpers@^2.3.1":
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.3.2.tgz#c2bfa82cedd833608effa2d2367fe9524ddf4434"
-  integrity sha512-uZMGC/C1m5EiwQsff6KMmCW25TYMQlJt4ilAWIjnelWGFg9HPUiLnlFvAas3ESUP+4OXLO8Oft7p4mHvbYvAEQ==
+"@open-wc/testing-helpers@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-3.0.1.tgz#f356690fc3495467503a30a2172288aa8baef6f8"
+  integrity sha512-hyNysSatbgT2FNxHJsS3rGKcLEo6+HwDFu1UQL6jcSQUabp/tj3PyX7UnXL3H5YGv0lJArdYLSnvjLnjn3O2fw==
   dependencies:
-    "@open-wc/scoped-elements" "^2.2.4"
+    "@open-wc/scoped-elements" "^3.0.2"
     lit "^2.0.0 || ^3.0.0"
     lit-html "^2.0.0 || ^3.0.0"
 
-"@open-wc/testing@^3.2.2":
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.2.2.tgz#c952f4b20af0d201cc8cc436c2c3cdd338bf8177"
-  integrity sha512-byN4dJTd6ZyI9mWmI4lVj30uiu+rYvQr93g64Pd7UFBdAUgb02DHLj6fkJ1gjxA6LC/MeFd7K7mOZ4+vKrMptw==
+"@open-wc/testing@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-4.0.0.tgz#bfa51210799f86ad1a47c2dece6b9f15dd51bb94"
+  integrity sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==
   dependencies:
     "@esm-bundle/chai" "^4.3.4-fix.0"
     "@open-wc/semantic-dom-diff" "^0.20.0"
-    "@open-wc/testing-helpers" "^2.3.1"
+    "@open-wc/testing-helpers" "^3.0.0"
     "@types/chai-dom" "^1.11.0"
     "@types/sinon-chai" "^3.2.3"
     chai-a11y-axe "^1.5.0"
@@ -736,82 +765,165 @@
   dependencies:
     "@webcomponents/shadycss" "^1.9.1"
 
-"@puppeteer/browsers@0.5.0":
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-0.5.0.tgz#1a1ee454b84a986b937ca2d93146f25a3fe8b670"
-  integrity sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==
+"@puppeteer/browsers@2.10.7":
+  version "2.10.7"
+  resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.10.7.tgz#a28f622b2da0ee131c9a576d25d8c4e31fc24135"
+  integrity sha512-wHWLkQWBjHtajZeqCB74nsa/X70KheyOhySYBRmVQDJiNj0zjZR/naPCvdWjMhcG1LmjaMV/9WtTo5mpe8qWLw==
   dependencies:
-    debug "4.3.4"
-    extract-zip "2.0.1"
-    https-proxy-agent "5.0.1"
-    progress "2.0.3"
-    proxy-from-env "1.1.0"
-    tar-fs "2.1.1"
-    unbzip2-stream "1.4.3"
-    yargs "17.7.1"
+    debug "^4.4.1"
+    extract-zip "^2.0.1"
+    progress "^2.0.3"
+    proxy-agent "^6.5.0"
+    semver "^7.7.2"
+    tar-fs "^3.1.0"
+    yargs "^17.7.2"
 
-"@rollup/plugin-node-resolve@^13.0.4":
-  version "13.3.0"
-  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c"
-  integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==
+"@rollup/plugin-node-resolve@^15.0.1":
+  version "15.3.1"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78"
+  integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==
   dependencies:
-    "@rollup/pluginutils" "^3.1.0"
-    "@types/resolve" "1.17.1"
+    "@rollup/pluginutils" "^5.0.1"
+    "@types/resolve" "1.20.2"
     deepmerge "^4.2.2"
-    is-builtin-module "^3.1.0"
     is-module "^1.0.0"
-    resolve "^1.19.0"
+    resolve "^1.22.1"
 
-"@rollup/pluginutils@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
-  integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
+"@rollup/pluginutils@^5.0.1":
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz#eac25ca5b0bdda4ba735ddaca5fbf26bd435f602"
+  integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==
   dependencies:
-    "@types/estree" "0.0.39"
-    estree-walker "^1.0.1"
-    picomatch "^2.2.2"
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^4.0.2"
 
-"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3":
-  version "1.8.6"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9"
-  integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==
-  dependencies:
-    type-detect "4.0.8"
+"@rollup/rollup-android-arm-eabi@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz#ba432433f5e7b419dba2be407d1d59fea6b8de48"
+  integrity sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==
 
-"@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1":
+"@rollup/rollup-android-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz#4e05c86e0fb9af6eaf52fc298dcdec577477e35c"
+  integrity sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==
+
+"@rollup/rollup-darwin-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz#788fad425b4129875639e0c14b6441c5f3b69d46"
+  integrity sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==
+
+"@rollup/rollup-darwin-x64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz#d44e05bee55b781d7c2cf535d9f9169787c3599d"
+  integrity sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==
+
+"@rollup/rollup-freebsd-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz#107786b4d604495224c3543bfd2cae33ddf76500"
+  integrity sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==
+
+"@rollup/rollup-freebsd-x64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz#54e105c3da27f31084ca6913fed603627755abde"
+  integrity sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz#725c23e0766b5d9368180bc2c427a51e31d0e147"
+  integrity sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==
+
+"@rollup/rollup-linux-arm-musleabihf@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz#6946b0d2f132f2baf5657945b81565d8abd51cc0"
+  integrity sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==
+
+"@rollup/rollup-linux-arm64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz#83510a6d03e748619241a17f5a879418a963c5ed"
+  integrity sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==
+
+"@rollup/rollup-linux-arm64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz#085b98d44c10908626dd40f26bf924433bbd8471"
+  integrity sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz#13e0a4808e9f7924f2cc8c133603f627c7a00543"
+  integrity sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==
+
+"@rollup/rollup-linux-ppc64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz#aeee4e47fc9ca5d6687e686fea4696202af6b2f4"
+  integrity sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==
+
+"@rollup/rollup-linux-riscv64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz#603e4591643f1d7851a96d096cf7fcd273f7b0e1"
+  integrity sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==
+
+"@rollup/rollup-linux-riscv64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz#f8fd9b01f1888e1816d5a398789d430511286c00"
+  integrity sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==
+
+"@rollup/rollup-linux-s390x-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz#37a1fd372d9b93d2b75b2f37c482ecf52f52849b"
+  integrity sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==
+
+"@rollup/rollup-linux-x64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz#131e66dbf7e71cb2a389acc45319bd4c990e093a"
+  integrity sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==
+
+"@rollup/rollup-linux-x64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz#b7245a5ea57db9679e8bf3032c25a5d2c5f54056"
+  integrity sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==
+
+"@rollup/rollup-win32-arm64-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz#768a128bb5da3c5472c3c56aec77507d28bc7209"
+  integrity sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==
+
+"@rollup/rollup-win32-ia32-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz#ce3f3b2eebe585340631498666718f00983a6a62"
+  integrity sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==
+
+"@rollup/rollup-win32-x64-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz#c2a0e3b81262a7e9dd12ce18b350a97558dd50bc"
+  integrity sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==
+
+"@sinonjs/commons@^3.0.1":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd"
   integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==
   dependencies:
     type-detect "4.0.8"
 
-"@sinonjs/fake-timers@^11.2.2":
-  version "11.3.1"
-  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz#51d6e8d83ca261ff02c0ab0e68e9db23d5cd5999"
-  integrity sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==
+"@sinonjs/fake-timers@^13.0.5":
+  version "13.0.5"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5"
+  integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==
   dependencies:
     "@sinonjs/commons" "^3.0.1"
 
-"@sinonjs/fake-timers@^9.1.2":
-  version "9.1.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c"
-  integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==
+"@sinonjs/samsam@^8.0.1":
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.3.tgz#eb6ffaef421e1e27783cc9b52567de20cb28072d"
+  integrity sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==
   dependencies:
-    "@sinonjs/commons" "^1.7.0"
+    "@sinonjs/commons" "^3.0.1"
+    type-detect "^4.1.0"
 
-"@sinonjs/samsam@^6.1.1":
-  version "6.1.3"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.3.tgz#4e30bcd4700336363302a7d72cbec9b9ab87b104"
-  integrity sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==
-  dependencies:
-    "@sinonjs/commons" "^1.6.0"
-    lodash.get "^4.4.2"
-    type-detect "^4.0.8"
-
-"@sinonjs/text-encoding@^0.7.2":
-  version "0.7.3"
-  resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f"
-  integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==
+"@tootallnate/quickjs-emscripten@^0.23.0":
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
+  integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
 
 "@types/accepts@*":
   version "1.3.7"
@@ -826,9 +938,9 @@
   integrity sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==
 
 "@types/body-parser@*":
-  version "1.19.5"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
-  integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
+  version "1.19.6"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
+  integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==
   dependencies:
     "@types/connect" "*"
     "@types/node" "*"
@@ -841,9 +953,9 @@
     "@types/chai" "*"
 
 "@types/chai@*":
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.0.1.tgz#2c3705555cf11f5f59c836a84c44afcfe4e5689d"
-  integrity sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.2.tgz#6f14cea18180ffc4416bc0fd12be05fdd73bdd6b"
+  integrity sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==
   dependencies:
     "@types/deep-eql" "*"
 
@@ -873,9 +985,9 @@
     "@types/node" "*"
 
 "@types/content-disposition@*":
-  version "0.5.8"
-  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537"
-  integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==
+  version "0.5.9"
+  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.9.tgz#00ca14939432869de829a4ccf6fd380fa9181750"
+  integrity sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==
 
 "@types/convert-source-map@^2.0.0":
   version "2.0.3"
@@ -883,9 +995,9 @@
   integrity sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==
 
 "@types/cookies@*":
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.0.tgz#a2290cfb325f75f0f28720939bee854d4142aee2"
-  integrity sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.1.tgz#083924ca1266e34ff240ca4d4fd6732ee5b81886"
+  integrity sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==
   dependencies:
     "@types/connect" "*"
     "@types/express" "*"
@@ -902,15 +1014,15 @@
   resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd"
   integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
 
-"@types/estree@0.0.39":
-  version "0.0.39"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
-  integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
+"@types/estree@1.0.8", "@types/estree@^1.0.0":
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+  integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
 
 "@types/express-serve-static-core@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8"
-  integrity sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==
+  version "5.0.7"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6"
+  integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==
   dependencies:
     "@types/node" "*"
     "@types/qs" "*"
@@ -918,13 +1030,12 @@
     "@types/send" "*"
 
 "@types/express@*":
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c"
-  integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956"
+  integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==
   dependencies:
     "@types/body-parser" "*"
     "@types/express-serve-static-core" "^5.0.0"
-    "@types/qs" "*"
     "@types/serve-static" "*"
 
 "@types/http-assert@*":
@@ -932,10 +1043,10 @@
   resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.6.tgz#b6b657c38a2350d21ce213139f33b03b2b5fa431"
   integrity sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==
 
-"@types/http-errors@*":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
-  integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
+"@types/http-errors@*", "@types/http-errors@^2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
+  integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
 
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.3":
   version "2.0.6"
@@ -968,7 +1079,21 @@
   dependencies:
     "@types/koa" "*"
 
-"@types/koa@*", "@types/koa@^2.11.6":
+"@types/koa@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-3.0.0.tgz#5df2b3b30da902f01b6ec78ef56403e6eb62fa6e"
+  integrity sha512-MOcVYdVYmkSutVHZZPh8j3+dAjLyR5Tl59CN0eKgpkE1h/LBSmPAsQQuWs+bKu7WtGNn+hKfJH9Gzml+PulmDg==
+  dependencies:
+    "@types/accepts" "*"
+    "@types/content-disposition" "*"
+    "@types/cookies" "*"
+    "@types/http-assert" "*"
+    "@types/http-errors" "^2"
+    "@types/keygrip" "*"
+    "@types/koa-compose" "*"
+    "@types/node" "*"
+
+"@types/koa@^2.11.6":
   version "2.15.0"
   resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.15.0.tgz#eca43d76f527c803b491731f95df575636e7b6f2"
   integrity sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==
@@ -987,17 +1112,12 @@
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
   integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
 
-"@types/mocha@^8.2.0":
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
-  integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
-
 "@types/node@*":
-  version "22.13.4"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a"
-  integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==
+  version "24.3.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec"
+  integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
   dependencies:
-    undici-types "~6.20.0"
+    undici-types "~7.10.0"
 
 "@types/parse5@^6.0.1":
   version "6.0.3"
@@ -1005,34 +1125,32 @@
   integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
 
 "@types/qs@*":
-  version "6.9.18"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2"
-  integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==
+  version "6.14.0"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
+  integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==
 
 "@types/range-parser@*":
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
   integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
 
-"@types/resolve@1.17.1":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
-  integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
-  dependencies:
-    "@types/node" "*"
+"@types/resolve@1.20.2":
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
+  integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
 
 "@types/send@*":
-  version "0.17.4"
-  resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
-  integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
+  version "0.17.5"
+  resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74"
+  integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==
   dependencies:
     "@types/mime" "^1"
     "@types/node" "*"
 
 "@types/serve-static@*":
-  version "1.15.7"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714"
-  integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==
+  version "1.15.8"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877"
+  integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==
   dependencies:
     "@types/http-errors" "*"
     "@types/node" "*"
@@ -1047,9 +1165,9 @@
     "@types/sinon" "*"
 
 "@types/sinon@*":
-  version "17.0.3"
-  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa"
-  integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==
+  version "17.0.4"
+  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.4.tgz#fd9a3e8e07eea1a3f4a6f82a972c899e5778f369"
+  integrity sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==
   dependencies:
     "@types/sinonjs__fake-timers" "*"
 
@@ -1077,13 +1195,6 @@
   dependencies:
     "@types/node" "*"
 
-"@web/browser-logs@^0.2.6":
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.2.6.tgz#ec936f78c7cf7b0ef9fb990c0097a3da1a756b20"
-  integrity sha512-CNjNVhd4FplRY8PPWIAt02vAowJAVcOoTNrR/NNb/o9pka7yI9qdjpWrWhEbPr2pOXonWb52AeAgdK66B8ZH7w==
-  dependencies:
-    errorstacks "^2.2.0"
-
 "@web/browser-logs@^0.4.0":
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.4.1.tgz#1be4464c9b3dca537b459d49cb1873a6deef0609"
@@ -1091,38 +1202,12 @@
   dependencies:
     errorstacks "^2.4.1"
 
-"@web/config-loader@^0.1.3":
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.1.3.tgz#8325ea54f75ef2ee7166783e64e66936db25bff7"
-  integrity sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==
-  dependencies:
-    semver "^7.3.4"
+"@web/config-loader@^0.3.0":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.3.3.tgz#13d4852c479d1f3adbc1f24d79c7e6eeedeb6e5d"
+  integrity sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==
 
-"@web/dev-server-core@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.4.1.tgz#803faff45281ee296d0dda02dfdd905c330db4d8"
-  integrity sha512-KdYwejXZwIZvb6tYMCqU7yBiEOPfKLQ3V9ezqqEz8DA9V9R3oQWaowckvCpFB9IxxPfS/P8/59OkdzGKQjcIUw==
-  dependencies:
-    "@types/koa" "^2.11.6"
-    "@types/ws" "^7.4.0"
-    "@web/parse5-utils" "^1.3.1"
-    chokidar "^3.4.3"
-    clone "^2.1.2"
-    es-module-lexer "^1.0.0"
-    get-stream "^6.0.0"
-    is-stream "^2.0.0"
-    isbinaryfile "^5.0.0"
-    koa "^2.13.0"
-    koa-etag "^4.0.0"
-    koa-send "^5.0.1"
-    koa-static "^5.0.0"
-    lru-cache "^6.0.0"
-    mime-types "^2.1.27"
-    parse5 "^6.0.1"
-    picomatch "^2.2.2"
-    ws "^7.4.2"
-
-"@web/dev-server-core@^0.7.3":
+"@web/dev-server-core@^0.7.2", "@web/dev-server-core@^0.7.3", "@web/dev-server-core@^0.7.4":
   version "0.7.5"
   resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.7.5.tgz#b283d46eb2c0384e848311ec9da25d06bbbc47df"
   integrity sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==
@@ -1146,57 +1231,49 @@
     picomatch "^2.2.2"
     ws "^7.5.10"
 
-"@web/dev-server-esbuild@^0.3.6":
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-0.3.6.tgz#838100894937443b96bfc4266c7795d27ed4afac"
-  integrity sha512-VDcZOzvmbg/z/8Q54hHqFwt9U4cacQJZxgS8YXAvyFuG85HAJ/Q55P7Tr++1NlRS8wQEos6QK2ERUWNjEVOhqQ==
+"@web/dev-server-esbuild@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.4.tgz#7ac39eb4350da39fb3f7c5ce5c0fd423a8511346"
+  integrity sha512-ia1LxBwwRiQBYhJ7/RtLenHyPjzle3SvTw3jOZaeGv8UGXVPOkQV8fR05caOtW/DPPZaZovNAybzRKVnNiYIZg==
   dependencies:
     "@mdn/browser-compat-data" "^4.0.0"
-    "@web/dev-server-core" "^0.4.1"
-    esbuild "^0.16 || ^0.17"
+    "@web/dev-server-core" "^0.7.4"
+    esbuild "^0.25.0"
     parse5 "^6.0.1"
     ua-parser-js "^1.0.33"
 
-"@web/dev-server-rollup@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.4.1.tgz#3c6606bac8e497498b5b47bf9e0c544c321b38ef"
-  integrity sha512-Ebsv7Ovd9MufeH3exvikBJ7GmrZA5OmHnOgaiHcwMJ2eQBJA5/I+/CbRjsLX97ICj/ZwZG//p2ITRz8W3UfSqg==
+"@web/dev-server-rollup@^0.6.1":
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz#d0a4f69e4a659d2b79f172e86236cea6c872e81c"
+  integrity sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==
   dependencies:
-    "@rollup/plugin-node-resolve" "^13.0.4"
-    "@web/dev-server-core" "^0.4.1"
+    "@rollup/plugin-node-resolve" "^15.0.1"
+    "@web/dev-server-core" "^0.7.2"
     nanocolors "^0.2.1"
     parse5 "^6.0.1"
-    rollup "^2.67.0"
-    whatwg-url "^11.0.0"
+    rollup "^4.4.0"
+    whatwg-url "^14.0.0"
 
-"@web/dev-server@^0.1.38":
-  version "0.1.38"
-  resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.1.38.tgz#d755092d66aeb923c546237a6c460439ea3ddd29"
-  integrity sha512-WUq7Zi8KeJ5/UZmmpZ+kzUpUlFlMP/rcreJKYg9Lxiz998KYl4G5Rv24akX0piTuqXG7r6h+zszg8V/hdzjCoA==
+"@web/dev-server@^0.4.0":
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.4.6.tgz#18a4421d474a9bd1bfc90419a7a798ed0b5538c5"
+  integrity sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==
   dependencies:
     "@babel/code-frame" "^7.12.11"
     "@types/command-line-args" "^5.0.0"
-    "@web/config-loader" "^0.1.3"
-    "@web/dev-server-core" "^0.4.1"
-    "@web/dev-server-rollup" "^0.4.1"
+    "@web/config-loader" "^0.3.0"
+    "@web/dev-server-core" "^0.7.2"
+    "@web/dev-server-rollup" "^0.6.1"
     camelcase "^6.2.0"
     command-line-args "^5.1.1"
     command-line-usage "^7.0.1"
     debounce "^1.2.0"
     deepmerge "^4.2.2"
-    ip "^1.1.5"
+    internal-ip "^6.2.0"
     nanocolors "^0.2.1"
     open "^8.0.2"
     portfinder "^1.0.32"
 
-"@web/parse5-utils@^1.3.1":
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-1.3.1.tgz#6727be4d7875a9ecb96a5b3003bd271da763f8b4"
-  integrity sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==
-  dependencies:
-    "@types/parse5" "^6.0.1"
-    parse5 "^6.0.1"
-
 "@web/parse5-utils@^2.1.0":
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.1.0.tgz#3d33aca62c66773492f2fba89d23a45f8b57ba4a"
@@ -1205,23 +1282,15 @@
     "@types/parse5" "^6.0.1"
     parse5 "^6.0.1"
 
-"@web/test-runner-chrome@^0.12.1":
-  version "0.12.1"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.12.1.tgz#3f4d7b83565807d75a84907ffd91ae1bd2298a52"
-  integrity sha512-QxzinqYHelZQpMHAuc5TYyWVhtHUEGhL3m1p2U+mTTTWrZYX3D0s6Q0oL2+XYT1dsja5sd71h7yiBTb9ctkKOg==
+"@web/test-runner-chrome@^0.18.1":
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.18.1.tgz#490a371990084b8d050dd778bd58e47d21522332"
+  integrity sha512-eO6ctCaqSguGM6G3cFobGHnrEs9wlv9Juj/Akyr4XLjeEMTheNULdvOXw9Bygi+QC/ir/0snMmt+/YKnfy8rYA==
   dependencies:
-    "@web/test-runner-core" "^0.10.29"
-    "@web/test-runner-coverage-v8" "^0.5.0"
+    "@web/test-runner-core" "^0.13.0"
+    "@web/test-runner-coverage-v8" "^0.8.0"
     chrome-launcher "^0.15.0"
-    puppeteer-core "^19.8.1"
-
-"@web/test-runner-commands@^0.6.6":
-  version "0.6.6"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.6.6.tgz#e0e8c4ce6dcd91e5b18cf2212511ee6108e31070"
-  integrity sha512-2DcK/+7f8QTicQpGFq/TmvKHDK/6Zald6rn1zqRlmj3pcH8fX6KHNVMU60Za9QgAKdorMBPfd8dJwWba5otzdw==
-  dependencies:
-    "@web/test-runner-core" "^0.10.29"
-    mkdirp "^1.0.4"
+    puppeteer-core "^24.0.0"
 
 "@web/test-runner-commands@^0.9.0":
   version "0.9.0"
@@ -1231,38 +1300,6 @@
     "@web/test-runner-core" "^0.13.0"
     mkdirp "^1.0.4"
 
-"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.29":
-  version "0.10.29"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.10.29.tgz#d8d909c849151cf19013d6084f89a31e400557d5"
-  integrity sha512-0/ZALYaycEWswHhpyvl5yqo0uIfCmZe8q14nGPi1dMmNiqLcHjyFGnuIiLexI224AW74ljHcHllmDlXK9FUKGA==
-  dependencies:
-    "@babel/code-frame" "^7.12.11"
-    "@types/babel__code-frame" "^7.0.2"
-    "@types/co-body" "^6.1.0"
-    "@types/convert-source-map" "^2.0.0"
-    "@types/debounce" "^1.2.0"
-    "@types/istanbul-lib-coverage" "^2.0.3"
-    "@types/istanbul-reports" "^3.0.0"
-    "@web/browser-logs" "^0.2.6"
-    "@web/dev-server-core" "^0.4.1"
-    chokidar "^3.4.3"
-    cli-cursor "^3.1.0"
-    co-body "^6.1.0"
-    convert-source-map "^2.0.0"
-    debounce "^1.2.0"
-    dependency-graph "^0.11.0"
-    globby "^11.0.1"
-    ip "^1.1.5"
-    istanbul-lib-coverage "^3.0.0"
-    istanbul-lib-report "^3.0.0"
-    istanbul-reports "^3.0.2"
-    log-update "^4.0.0"
-    nanocolors "^0.2.1"
-    nanoid "^3.1.25"
-    open "^8.0.2"
-    picomatch "^2.2.2"
-    source-map "^0.7.3"
-
 "@web/test-runner-core@^0.13.0":
   version "0.13.4"
   resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.13.4.tgz#df6a76b3e970adbbd3ba39e870f4bf657d6dc43f"
@@ -1295,36 +1332,36 @@
     picomatch "^2.2.2"
     source-map "^0.7.3"
 
-"@web/test-runner-coverage-v8@^0.5.0":
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.5.0.tgz#d1b033fd4baddaf5636a41cd017e321a338727a6"
-  integrity sha512-4eZs5K4JG7zqWEhVSO8utlscjbVScV7K6JVwoWWcObFTGAaBMbDVzwGRimyNSzvmfTdIO/Arze4CeUUfCl4iLQ==
+"@web/test-runner-coverage-v8@^0.8.0":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz#783e9f685f14cafc34a6bf323f7d9268c1477933"
+  integrity sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==
   dependencies:
-    "@web/test-runner-core" "^0.10.20"
+    "@web/test-runner-core" "^0.13.0"
     istanbul-lib-coverage "^3.0.0"
+    lru-cache "^8.0.4"
     picomatch "^2.2.2"
     v8-to-istanbul "^9.0.1"
 
-"@web/test-runner-mocha@^0.7.5":
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-mocha/-/test-runner-mocha-0.7.5.tgz#696f8cb7f5118a72bd7ac5778367ae3bd3fb92cd"
-  integrity sha512-12/OBq6efPCAvJpcz3XJs2OO5nHe7GtBibZ8Il1a0QtsGpRmuJ4/m1EF0Fj9f6KHg7JdpGo18A37oE+5hXjHwg==
+"@web/test-runner-mocha@^0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-mocha/-/test-runner-mocha-0.9.0.tgz#4fbfa5c3222c8c787fdcc057dd932a0763267b10"
+  integrity sha512-ZL9F6FXd0DBQvo/h/+mSfzFTSRVxzV9st/AHhpgABtUtV/AIpVE9to6+xdkpu6827kwjezdpuadPfg+PlrBWqQ==
   dependencies:
-    "@types/mocha" "^8.2.0"
-    "@web/test-runner-core" "^0.10.20"
+    "@web/test-runner-core" "^0.13.0"
 
-"@web/test-runner@^0.15.3":
-  version "0.15.3"
-  resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.15.3.tgz#8cf51293e5889c5db63c75fb7d422b5c820dcf01"
-  integrity sha512-unwBymuQpI8yc/129K9H0aIzLIIQFrr2/mhdcIWFeZjjw5X3TJh57p5NFOA76nhlBSjFHyu0U0FXw9uOzXUCuQ==
+"@web/test-runner@^0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.20.2.tgz#3045de8f14eb9f9b8aa51aa7fd9849dd95bb0a93"
+  integrity sha512-zfEGYEDnS0EI8qgoWFjmtkIXhqP15W40NW3dCaKtbxj5eU0a7E53f3GV/tZGD0GlZKF8d4Fyw+AFrwOJU9Z4GA==
   dependencies:
-    "@web/browser-logs" "^0.2.6"
-    "@web/config-loader" "^0.1.3"
-    "@web/dev-server" "^0.1.38"
-    "@web/test-runner-chrome" "^0.12.1"
-    "@web/test-runner-commands" "^0.6.6"
-    "@web/test-runner-core" "^0.10.29"
-    "@web/test-runner-mocha" "^0.7.5"
+    "@web/browser-logs" "^0.4.0"
+    "@web/config-loader" "^0.3.0"
+    "@web/dev-server" "^0.4.0"
+    "@web/test-runner-chrome" "^0.18.1"
+    "@web/test-runner-commands" "^0.9.0"
+    "@web/test-runner-core" "^0.13.0"
+    "@web/test-runner-mocha" "^0.9.0"
     camelcase "^6.2.0"
     command-line-args "^5.1.1"
     command-line-usage "^7.0.1"
@@ -1348,12 +1385,10 @@
     mime-types "~2.1.34"
     negotiator "0.6.3"
 
-agent-base@6:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
-  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
-  dependencies:
-    debug "4"
+agent-base@^7.1.0, agent-base@^7.1.2:
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
+  integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
 
 ansi-escapes@^4.3.0:
   version "4.3.2"
@@ -1374,14 +1409,6 @@
   dependencies:
     color-convert "^2.0.1"
 
-anymatch@~3.1.2:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
-  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
-  dependencies:
-    normalize-path "^3.0.0"
-    picomatch "^2.0.4"
-
 array-back@^3.0.1, array-back@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
@@ -1397,43 +1424,72 @@
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
   integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
 
+ast-types@^0.13.4:
+  version "0.13.4"
+  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782"
+  integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==
+  dependencies:
+    tslib "^2.0.1"
+
 astral-regex@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
-async@^2.6.4:
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
-  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
-  dependencies:
-    lodash "^4.17.14"
+async@^3.2.6:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+  integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
 
 axe-core@^4.3.3:
-  version "4.10.2"
-  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df"
-  integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==
+  version "4.10.3"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc"
+  integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==
 
-base64-js@^1.3.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
-  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+b4a@^1.6.4:
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4"
+  integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==
 
-binary-extensions@^2.0.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
-  integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
+bare-events@^2.2.0, bare-events@^2.5.4:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.6.1.tgz#f793b28bdc3dcf147d7cf01f882a6f0b12ccc4a2"
+  integrity sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==
 
-bl@^4.0.3:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
-  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+bare-fs@^4.0.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.2.1.tgz#bd9c2b02a07b9206ccba97f6ea9ce9658582f119"
+  integrity sha512-mELROzV0IhqilFgsl1gyp48pnZsaV9xhQapHLDsvn4d4ZTfbFhcghQezl7FTEDNBcGqLUnNI3lUlm6ecrLWdFA==
   dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
+    bare-events "^2.5.4"
+    bare-path "^3.0.0"
+    bare-stream "^2.6.4"
 
-braces@^3.0.3, braces@~3.0.2:
+bare-os@^3.0.1:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.2.tgz#b3c4f5ad5e322c0fd0f3c29fc97d19009e2796e5"
+  integrity sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==
+
+bare-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-3.0.0.tgz#b59d18130ba52a6af9276db3e96a2e3d3ea52178"
+  integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==
+  dependencies:
+    bare-os "^3.0.1"
+
+bare-stream@^2.6.4:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.7.0.tgz#5b9e7dd0a354d06e82d6460c426728536c35d789"
+  integrity sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==
+  dependencies:
+    streamx "^2.21.0"
+
+basic-ftp@^5.0.2:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
+  integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
+
+braces@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
   integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@@ -1445,19 +1501,6 @@
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
   integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
 
-buffer@^5.2.1, buffer@^5.5.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
-  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.1.13"
-
-builtin-modules@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
-  integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
-
 bytes@3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@@ -1471,7 +1514,7 @@
     mime-types "^2.1.18"
     ylru "^1.2.0"
 
-call-bind-apply-helpers@^1.0.1:
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
   integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
@@ -1480,12 +1523,12 @@
     function-bind "^1.1.2"
 
 call-bound@^1.0.2, call-bound@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681"
-  integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+  integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
   dependencies:
-    call-bind-apply-helpers "^1.0.1"
-    get-intrinsic "^1.2.6"
+    call-bind-apply-helpers "^1.0.2"
+    get-intrinsic "^1.3.0"
 
 camelcase@^6.2.0:
   version "6.3.0"
@@ -1514,21 +1557,6 @@
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chokidar@^3.4.3:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
-  integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
-  dependencies:
-    anymatch "~3.1.2"
-    braces "~3.0.2"
-    glob-parent "~5.1.2"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.6.0"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
 chokidar@^4.0.1:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
@@ -1536,11 +1564,6 @@
   dependencies:
     readdirp "^4.0.1"
 
-chownr@^1.1.1:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
-  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
-
 chrome-launcher@^0.15.0:
   version "0.15.2"
   resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da"
@@ -1551,12 +1574,13 @@
     is-wsl "^2.2.0"
     lighthouse-logger "^1.0.0"
 
-chromium-bidi@0.4.7:
-  version "0.4.7"
-  resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.7.tgz#4c022c2b0fb1d1c9b571fadf373042160e71d236"
-  integrity sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==
+chromium-bidi@8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-8.0.0.tgz#d73c9beed40317adf2bcfeb9a47087003cd467ec"
+  integrity sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==
   dependencies:
-    mitt "3.0.0"
+    mitt "^3.0.1"
+    zod "^3.24.1"
 
 cli-cursor@^3.1.0:
   version "3.1.0"
@@ -1652,18 +1676,11 @@
     depd "~2.0.0"
     keygrip "~1.1.0"
 
-crelt@^1.0.5:
+crelt@^1.0.5, crelt@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
   integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
 
-cross-fetch@3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
-  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
-  dependencies:
-    node-fetch "2.6.7"
-
 cross-spawn@^7.0.3:
   version "7.0.6"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -1673,25 +1690,23 @@
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+data-uri-to-buffer@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b"
+  integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==
+
 debounce@^1.2.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
   integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
 
-debug@4, debug@^4.1.1, debug@^4.3.2:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
-  integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.1:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+  integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
   dependencies:
     ms "^2.1.3"
 
-debug@4.3.4:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
-  dependencies:
-    ms "2.1.2"
-
 debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -1699,7 +1714,7 @@
   dependencies:
     ms "2.0.0"
 
-debug@^3.1.0, debug@^3.2.7:
+debug@^3.1.0:
   version "3.2.7"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
   integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -1728,6 +1743,15 @@
   resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
   integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
 
+degenerator@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5"
+  integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==
+  dependencies:
+    ast-types "^0.13.4"
+    escodegen "^2.1.0"
+    esprima "^4.0.1"
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -1753,16 +1777,21 @@
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
   integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
 
-devtools-protocol@0.0.1107588:
-  version "0.0.1107588"
-  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz#f8cac707840b97cc30b029359341bcbbb0ad8ffa"
-  integrity sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==
+devtools-protocol@0.0.1475386:
+  version "0.0.1475386"
+  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz#5378401a2c5698ab68c3482c9b7816ff62ec652b"
+  integrity sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==
 
 diff@^5.0.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
   integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
 
+diff@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a"
+  integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -1794,14 +1823,14 @@
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
 
-end-of-stream@^1.1.0, end-of-stream@^1.4.1:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
-  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+end-of-stream@^1.1.0:
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
+  integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
   dependencies:
     once "^1.4.0"
 
-errorstacks@^2.2.0, errorstacks@^2.4.1:
+errorstacks@^2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.1.tgz#05adf6de1f5b04a66f2c12cc0593e1be2b18cd0f"
   integrity sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==
@@ -1817,44 +1846,48 @@
   integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
 
 es-module-lexer@^1.0.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21"
-  integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
+  integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
 
-es-object-atoms@^1.0.0:
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
   integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
   dependencies:
     es-errors "^1.3.0"
 
-"esbuild@^0.16 || ^0.17":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
-  integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
+esbuild@^0.25.0:
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976"
+  integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==
   optionalDependencies:
-    "@esbuild/android-arm" "0.17.19"
-    "@esbuild/android-arm64" "0.17.19"
-    "@esbuild/android-x64" "0.17.19"
-    "@esbuild/darwin-arm64" "0.17.19"
-    "@esbuild/darwin-x64" "0.17.19"
-    "@esbuild/freebsd-arm64" "0.17.19"
-    "@esbuild/freebsd-x64" "0.17.19"
-    "@esbuild/linux-arm" "0.17.19"
-    "@esbuild/linux-arm64" "0.17.19"
-    "@esbuild/linux-ia32" "0.17.19"
-    "@esbuild/linux-loong64" "0.17.19"
-    "@esbuild/linux-mips64el" "0.17.19"
-    "@esbuild/linux-ppc64" "0.17.19"
-    "@esbuild/linux-riscv64" "0.17.19"
-    "@esbuild/linux-s390x" "0.17.19"
-    "@esbuild/linux-x64" "0.17.19"
-    "@esbuild/netbsd-x64" "0.17.19"
-    "@esbuild/openbsd-x64" "0.17.19"
-    "@esbuild/sunos-x64" "0.17.19"
-    "@esbuild/win32-arm64" "0.17.19"
-    "@esbuild/win32-ia32" "0.17.19"
-    "@esbuild/win32-x64" "0.17.19"
+    "@esbuild/aix-ppc64" "0.25.9"
+    "@esbuild/android-arm" "0.25.9"
+    "@esbuild/android-arm64" "0.25.9"
+    "@esbuild/android-x64" "0.25.9"
+    "@esbuild/darwin-arm64" "0.25.9"
+    "@esbuild/darwin-x64" "0.25.9"
+    "@esbuild/freebsd-arm64" "0.25.9"
+    "@esbuild/freebsd-x64" "0.25.9"
+    "@esbuild/linux-arm" "0.25.9"
+    "@esbuild/linux-arm64" "0.25.9"
+    "@esbuild/linux-ia32" "0.25.9"
+    "@esbuild/linux-loong64" "0.25.9"
+    "@esbuild/linux-mips64el" "0.25.9"
+    "@esbuild/linux-ppc64" "0.25.9"
+    "@esbuild/linux-riscv64" "0.25.9"
+    "@esbuild/linux-s390x" "0.25.9"
+    "@esbuild/linux-x64" "0.25.9"
+    "@esbuild/netbsd-arm64" "0.25.9"
+    "@esbuild/netbsd-x64" "0.25.9"
+    "@esbuild/openbsd-arm64" "0.25.9"
+    "@esbuild/openbsd-x64" "0.25.9"
+    "@esbuild/openharmony-arm64" "0.25.9"
+    "@esbuild/sunos-x64" "0.25.9"
+    "@esbuild/win32-arm64" "0.25.9"
+    "@esbuild/win32-ia32" "0.25.9"
+    "@esbuild/win32-x64" "0.25.9"
 
 escalade@^3.1.1:
   version "3.2.0"
@@ -1871,10 +1904,36 @@
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
-estree-walker@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
-  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
+escodegen@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
+  integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^5.2.0"
+    esutils "^2.0.2"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
 etag@^1.8.1:
   version "1.8.1"
@@ -1896,7 +1955,7 @@
     signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
-extract-zip@2.0.1:
+extract-zip@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
   integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@@ -1907,6 +1966,11 @@
   optionalDependencies:
     "@types/yauzl" "^2.9.1"
 
+fast-fifo@^1.2.0, fast-fifo@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
+  integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
+
 fast-glob@^3.2.9:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
@@ -1919,9 +1983,9 @@
     micromatch "^4.0.8"
 
 fastq@^1.6.0:
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.0.tgz#a82c6b7c2bb4e44766d865f07997785fecfdcb89"
-  integrity sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+  integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
   dependencies:
     reusify "^1.0.4"
 
@@ -1951,11 +2015,6 @@
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
 
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
 fsevents@~2.3.2:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -1971,23 +2030,23 @@
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-intrinsic@^1.2.5, get-intrinsic@^1.2.6:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
-  integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
+get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+  integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
   dependencies:
-    call-bind-apply-helpers "^1.0.1"
+    call-bind-apply-helpers "^1.0.2"
     es-define-property "^1.0.1"
     es-errors "^1.3.0"
-    es-object-atoms "^1.0.0"
+    es-object-atoms "^1.1.1"
     function-bind "^1.1.2"
-    get-proto "^1.0.0"
+    get-proto "^1.0.1"
     gopd "^1.2.0"
     has-symbols "^1.1.0"
     hasown "^2.0.2"
     math-intrinsics "^1.1.0"
 
-get-proto@^1.0.0:
+get-proto@^1.0.0, get-proto@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
   integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
@@ -2007,7 +2066,16 @@
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
-glob-parent@^5.1.2, glob-parent@~5.1.2:
+get-uri@^6.0.1:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16"
+  integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==
+  dependencies:
+    basic-ftp "^5.0.2"
+    data-uri-to-buffer "^6.0.2"
+    debug "^4.3.4"
+
+glob-parent@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -2100,12 +2168,20 @@
     setprototypeof "1.1.0"
     statuses ">= 1.4.0 < 2"
 
-https-proxy-agent@5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
-  integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
+  integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
   dependencies:
-    agent-base "6"
+    agent-base "^7.1.0"
+    debug "^4.3.4"
+
+https-proxy-agent@^7.0.6:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
+  integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
+  dependencies:
+    agent-base "^7.1.2"
     debug "4"
 
 human-signals@^2.1.0:
@@ -2120,11 +2196,6 @@
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.13:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
-  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
 ignore@^5.2.0:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -2140,7 +2211,7 @@
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
 
-inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
+inherits@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -2155,35 +2226,21 @@
     is-ip "^3.1.0"
     p-event "^4.2.0"
 
+ip-address@^10.0.1:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed"
+  integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
+
 ip-regex@^4.0.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
   integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
 
-ip@^1.1.5:
-  version "1.1.9"
-  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396"
-  integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==
-
 ipaddr.js@^1.9.1:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
-is-binary-path@~2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
-  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
-  dependencies:
-    binary-extensions "^2.0.0"
-
-is-builtin-module@^3.1.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
-  integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==
-  dependencies:
-    builtin-modules "^3.3.0"
-
 is-core-module@^2.16.0:
   version "2.16.1"
   resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
@@ -2216,7 +2273,7 @@
     has-tostringtag "^1.0.2"
     safe-regex-test "^1.1.0"
 
-is-glob@^4.0.1, is-glob@~4.0.1:
+is-glob@^4.0.1:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
   integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -2263,9 +2320,9 @@
     is-docker "^2.0.0"
 
 isbinaryfile@^5.0.0:
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.4.tgz#2a2edefa76cafa66613fe4c1ea52f7f031017bdf"
-  integrity sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.5.tgz#5a29c56b97ef337394c3ec4cc304066042ae81e8"
+  integrity sha512-vh9MWXjhhblwrHlt/yutrubDuBD01kKFscyVndE2/VEeEU5aAizrzuWAsEaCjo997k+IluhN6C4jgxfS69SCIw==
 
 isexe@^2.0.0:
   version "2.0.0"
@@ -2287,9 +2344,9 @@
     supports-color "^7.1.0"
 
 istanbul-reports@^3.0.2:
-  version "3.1.7"
-  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b"
-  integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
+  integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
   dependencies:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
@@ -2299,11 +2356,6 @@
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
-just-extend@^6.2.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947"
-  integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==
-
 keygrip@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
@@ -2349,9 +2401,9 @@
     koa-send "^5.0.0"
 
 koa@^2.13.0:
-  version "2.15.4"
-  resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.4.tgz#7000b3d8354558671adb1ba1b1c09bedb5f8da75"
-  integrity sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ==
+  version "2.16.2"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.16.2.tgz#1292a3b415a9b9f3dd6872c1835229630a4ecae3"
+  integrity sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==
   dependencies:
     accepts "^1.3.5"
     cache-content-type "^1.0.0"
@@ -2385,46 +2437,36 @@
     debug "^2.6.9"
     marky "^1.2.2"
 
-lit-element@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.1.1.tgz#07905992815076e388cf6f1faffc7d6866c82007"
-  integrity sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==
+lit-element@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.2.1.tgz#0a3782f36eaa545862fe07f84abcb14b2903a042"
+  integrity sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==
   dependencies:
-    "@lit-labs/ssr-dom-shim" "^1.2.0"
-    "@lit/reactive-element" "^2.0.4"
-    lit-html "^3.2.0"
+    "@lit-labs/ssr-dom-shim" "^1.4.0"
+    "@lit/reactive-element" "^2.1.0"
+    lit-html "^3.3.0"
 
-"lit-html@^2.0.0 || ^3.0.0", lit-html@^3.2.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.2.1.tgz#8fc49e3531ee5947e4d93e8a5aa642ab1649833b"
-  integrity sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==
+"lit-html@^2.0.0 || ^3.0.0", lit-html@^3.3.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.3.1.tgz#f0a7e4b9ea0a1d034eb28a4bf2d1b0a0096253e3"
+  integrity sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==
   dependencies:
     "@types/trusted-types" "^2.0.2"
 
-"lit@^2.0.0 || ^3.0.0", lit@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/lit/-/lit-3.2.1.tgz#d6dd15eac20db3a098e81e2c85f70a751ff55592"
-  integrity sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==
+"lit@^2.0.0 || ^3.0.0", "lit@^2.8.0 || ^3.0.0", lit@^3.0.0, lit@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lit/-/lit-3.3.1.tgz#9dc79be626bc9a3b824de98b107dd662cabdeda6"
+  integrity sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==
   dependencies:
-    "@lit/reactive-element" "^2.0.4"
-    lit-element "^4.1.0"
-    lit-html "^3.2.0"
+    "@lit/reactive-element" "^2.1.0"
+    lit-element "^4.2.0"
+    lit-html "^3.3.0"
 
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
   integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
 
-lodash.get@^4.4.2:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-  integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
-
-lodash@^4.17.14:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
 log-update@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
@@ -2435,12 +2477,10 @@
     slice-ansi "^4.0.0"
     wrap-ansi "^6.2.0"
 
-lru-cache@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
-  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
-  dependencies:
-    yallist "^4.0.0"
+lru-cache@^7.14.1:
+  version "7.18.3"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
+  integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
 
 lru-cache@^8.0.4:
   version "8.0.5"
@@ -2455,9 +2495,9 @@
     semver "^7.5.3"
 
 marky@^1.2.2:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0"
-  integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/marky/-/marky-1.3.0.tgz#422b63b0baf65022f02eda61a238eccdbbc14997"
+  integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==
 
 math-intrinsics@^1.1.0:
   version "1.1.0"
@@ -2504,27 +2544,10 @@
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-minimist@^1.2.6:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
-  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
-
-mitt@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
-  integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
-
-mkdirp-classic@^0.5.2:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
-  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-
-mkdirp@^0.5.6:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
-  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
-  dependencies:
-    minimist "^1.2.6"
+mitt@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
+  integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
 
 mkdirp@^1.0.4:
   version "1.0.4"
@@ -2536,11 +2559,6 @@
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
   integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
 
-ms@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
-  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
 ms@^2.1.1, ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -2552,37 +2570,19 @@
   integrity sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==
 
 nanoid@^3.1.25:
-  version "3.3.8"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
-  integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+  version "3.3.11"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
+  integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
 
 negotiator@0.6.3:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
   integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
 
-nise@^5.1.1:
-  version "5.1.9"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.9.tgz#0cb73b5e4499d738231a473cd89bd8afbb618139"
-  integrity sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==
-  dependencies:
-    "@sinonjs/commons" "^3.0.0"
-    "@sinonjs/fake-timers" "^11.2.2"
-    "@sinonjs/text-encoding" "^0.7.2"
-    just-extend "^6.2.0"
-    path-to-regexp "^6.2.1"
-
-node-fetch@2.6.7:
-  version "2.6.7"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
-  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
-  dependencies:
-    whatwg-url "^5.0.0"
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
-  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+netmask@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
+  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
 
 npm-run-path@^4.0.1:
   version "4.0.1"
@@ -2650,6 +2650,28 @@
   dependencies:
     p-finally "^1.0.0"
 
+pac-proxy-agent@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df"
+  integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
+  dependencies:
+    "@tootallnate/quickjs-emscripten" "^0.23.0"
+    agent-base "^7.1.2"
+    debug "^4.3.4"
+    get-uri "^6.0.1"
+    http-proxy-agent "^7.0.0"
+    https-proxy-agent "^7.0.6"
+    pac-resolver "^7.0.1"
+    socks-proxy-agent "^8.0.5"
+
+pac-resolver@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6"
+  integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==
+  dependencies:
+    degenerator "^5.0.0"
+    netmask "^2.0.2"
+
 parse5@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
@@ -2675,11 +2697,6 @@
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
-path-to-regexp@^6.2.1:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
-  integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
-
 path-type@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -2690,64 +2707,77 @@
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
   integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
 
-picocolors@^1.0.0:
+picocolors@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
   integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
 
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
+picomatch@^2.2.2, picomatch@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 
-portfinder@^1.0.32:
-  version "1.0.32"
-  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
-  integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
-  dependencies:
-    async "^2.6.4"
-    debug "^3.2.7"
-    mkdirp "^0.5.6"
+picomatch@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
+  integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
 
-progress@2.0.3:
+portfinder@^1.0.32:
+  version "1.0.37"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.37.tgz#92b754ef89a11801c8efe4b0e5cd845b0064c212"
+  integrity sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==
+  dependencies:
+    async "^3.2.6"
+    debug "^4.3.6"
+
+progress@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
-proxy-from-env@1.1.0:
+proxy-agent@^6.5.0:
+  version "6.5.0"
+  resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d"
+  integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==
+  dependencies:
+    agent-base "^7.1.2"
+    debug "^4.3.4"
+    http-proxy-agent "^7.0.1"
+    https-proxy-agent "^7.0.6"
+    lru-cache "^7.14.1"
+    pac-proxy-agent "^7.1.0"
+    proxy-from-env "^1.1.0"
+    socks-proxy-agent "^8.0.5"
+
+proxy-from-env@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
   integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
 
 pump@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
-  integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d"
+  integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==
   dependencies:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
-punycode@^2.1.1:
+punycode@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
   integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
 
-puppeteer-core@^19.8.1:
-  version "19.11.1"
-  resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.11.1.tgz#4c63d7a0a6cd268ff054ebcac315b646eee32667"
-  integrity sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==
+puppeteer-core@^24.0.0:
+  version "24.17.0"
+  resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-24.17.0.tgz#66eaa3532d06c22fbbdc36e09f416cb69b8c8637"
+  integrity sha512-RYOBKFiF+3RdwIZTEacqNpD567gaFcBAOKTT7742FdB1icXudrPI7BlZbYTYWK2wgGQUXt9Zi1Yn+D5PmCs4CA==
   dependencies:
-    "@puppeteer/browsers" "0.5.0"
-    chromium-bidi "0.4.7"
-    cross-fetch "3.1.5"
-    debug "4.3.4"
-    devtools-protocol "0.0.1107588"
-    extract-zip "2.0.1"
-    https-proxy-agent "5.0.1"
-    proxy-from-env "1.1.0"
-    tar-fs "2.1.1"
-    unbzip2-stream "1.4.3"
-    ws "8.13.0"
+    "@puppeteer/browsers" "2.10.7"
+    chromium-bidi "8.0.0"
+    debug "^4.4.1"
+    devtools-protocol "0.0.1475386"
+    typed-query-selector "^2.12.0"
+    ws "^8.18.3"
 
 qs@^6.5.2:
   version "6.14.0"
@@ -2771,27 +2801,11 @@
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-readable-stream@^3.1.1, readable-stream@^3.4.0:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
-  integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
-  dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
-
 readdirp@^4.0.1:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
   integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
 
-readdirp@~3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
-  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
-  dependencies:
-    picomatch "^2.2.1"
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -2805,7 +2819,7 @@
     http-errors "~1.6.2"
     path-is-absolute "1.0.1"
 
-resolve@^1.19.0:
+resolve@^1.22.1:
   version "1.22.10"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
   integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
@@ -2823,15 +2837,37 @@
     signal-exit "^3.0.2"
 
 reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+  integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
 
-rollup@^2.67.0:
-  version "2.79.2"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090"
-  integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
+rollup@^4.4.0:
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.49.0.tgz#9751ad9d06a47a4496c3c5c238b27b1422c8b0eb"
+  integrity sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==
+  dependencies:
+    "@types/estree" "1.0.8"
   optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.49.0"
+    "@rollup/rollup-android-arm64" "4.49.0"
+    "@rollup/rollup-darwin-arm64" "4.49.0"
+    "@rollup/rollup-darwin-x64" "4.49.0"
+    "@rollup/rollup-freebsd-arm64" "4.49.0"
+    "@rollup/rollup-freebsd-x64" "4.49.0"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.49.0"
+    "@rollup/rollup-linux-arm-musleabihf" "4.49.0"
+    "@rollup/rollup-linux-arm64-gnu" "4.49.0"
+    "@rollup/rollup-linux-arm64-musl" "4.49.0"
+    "@rollup/rollup-linux-loongarch64-gnu" "4.49.0"
+    "@rollup/rollup-linux-ppc64-gnu" "4.49.0"
+    "@rollup/rollup-linux-riscv64-gnu" "4.49.0"
+    "@rollup/rollup-linux-riscv64-musl" "4.49.0"
+    "@rollup/rollup-linux-s390x-gnu" "4.49.0"
+    "@rollup/rollup-linux-x64-gnu" "4.49.0"
+    "@rollup/rollup-linux-x64-musl" "4.49.0"
+    "@rollup/rollup-win32-arm64-msvc" "4.49.0"
+    "@rollup/rollup-win32-ia32-msvc" "4.49.0"
+    "@rollup/rollup-win32-x64-msvc" "4.49.0"
     fsevents "~2.3.2"
 
 run-parallel@^1.1.9:
@@ -2848,7 +2884,7 @@
   dependencies:
     tslib "^1.9.0"
 
-safe-buffer@5.2.1, safe-buffer@~5.2.0:
+safe-buffer@5.2.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -2867,10 +2903,10 @@
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-semver@^7.3.4, semver@^7.5.3:
-  version "7.7.1"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
-  integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+semver@^7.5.3, semver@^7.7.2:
+  version "7.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+  integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
 
 setprototypeof@1.1.0:
   version "1.1.0"
@@ -2939,16 +2975,15 @@
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
   integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
 
-sinon@^13.0.2:
-  version "13.0.2"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-13.0.2.tgz#c6a8ddd655dc1415bbdc5ebf0e5b287806850c3a"
-  integrity sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA==
+sinon@^20.0.0:
+  version "20.0.0"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-20.0.0.tgz#4b653468735f7152ba694d05498c2b5d024ab006"
+  integrity sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==
   dependencies:
-    "@sinonjs/commons" "^1.8.3"
-    "@sinonjs/fake-timers" "^9.1.2"
-    "@sinonjs/samsam" "^6.1.1"
-    diff "^5.0.0"
-    nise "^5.1.1"
+    "@sinonjs/commons" "^3.0.1"
+    "@sinonjs/fake-timers" "^13.0.5"
+    "@sinonjs/samsam" "^8.0.1"
+    diff "^7.0.0"
     supports-color "^7.2.0"
 
 slash@^3.0.0:
@@ -2965,10 +3000,37 @@
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
+smart-buffer@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
+  integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+
+socks-proxy-agent@^8.0.5:
+  version "8.0.5"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee"
+  integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==
+  dependencies:
+    agent-base "^7.1.2"
+    debug "^4.3.4"
+    socks "^2.8.3"
+
+socks@^2.8.3:
+  version "2.8.7"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea"
+  integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==
+  dependencies:
+    ip-address "^10.0.1"
+    smart-buffer "^4.2.0"
+
 source-map@^0.7.3:
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
-  integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
+  version "0.7.6"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02"
+  integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==
+
+source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
 statuses@2.0.1:
   version "2.0.1"
@@ -2980,6 +3042,16 @@
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
 
+streamx@^2.15.0, streamx@^2.21.0:
+  version "2.22.1"
+  resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.22.1.tgz#c97cbb0ce18da4f4db5a971dc9ab68ff5dc7f5a5"
+  integrity sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==
+  dependencies:
+    fast-fifo "^1.3.2"
+    text-decoder "^1.1.0"
+  optionalDependencies:
+    bare-events "^2.2.0"
+
 string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -2989,13 +3061,6 @@
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.1"
 
-string_decoder@^1.1.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
-  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
-  dependencies:
-    safe-buffer "~5.2.0"
-
 strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -3033,31 +3098,32 @@
     array-back "^6.2.2"
     wordwrapjs "^5.1.0"
 
-tar-fs@2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
-  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+tar-fs@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.0.tgz#4675e2254d81410e609d91581a762608de999d25"
+  integrity sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==
   dependencies:
-    chownr "^1.1.1"
-    mkdirp-classic "^0.5.2"
     pump "^3.0.0"
-    tar-stream "^2.1.4"
+    tar-stream "^3.1.5"
+  optionalDependencies:
+    bare-fs "^4.0.1"
+    bare-path "^3.0.0"
 
-tar-stream@^2.1.4:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
-  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+tar-stream@^3.1.5:
+  version "3.1.7"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b"
+  integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==
   dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
+    b4a "^1.6.4"
+    fast-fifo "^1.2.0"
+    streamx "^2.15.0"
 
-through@^2.3.8:
-  version "2.3.8"
-  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+text-decoder@^1.1.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65"
+  integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==
+  dependencies:
+    b4a "^1.6.4"
 
 to-regex-range@^5.0.1:
   version "5.0.1"
@@ -3071,23 +3137,23 @@
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
-tr46@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
-  integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
+tr46@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca"
+  integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
   dependencies:
-    punycode "^2.1.1"
-
-tr46@~0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
-  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+    punycode "^2.3.1"
 
 tslib@^1.9.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
+tslib@^2.0.1, tslib@^2.4.0:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
 tsscmp@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
@@ -3098,7 +3164,7 @@
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-detect@^4.0.8:
+type-detect@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
   integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
@@ -3116,6 +3182,11 @@
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+typed-query-selector@^2.12.0:
+  version "2.12.0"
+  resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2"
+  integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==
+
 typical@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
@@ -3127,33 +3198,20 @@
   integrity sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==
 
 ua-parser-js@^1.0.33:
-  version "1.0.40"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.40.tgz#ac6aff4fd8ea3e794a6aa743ec9c2fc29e75b675"
-  integrity sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==
+  version "1.0.41"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.41.tgz#bd04dc9ec830fcf9e4fad35cf22dcedd2e3b4e9c"
+  integrity sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==
 
-unbzip2-stream@1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
-  integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
-  dependencies:
-    buffer "^5.2.1"
-    through "^2.3.8"
-
-undici-types@~6.20.0:
-  version "6.20.0"
-  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
-  integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
+undici-types@~7.10.0:
+  version "7.10.0"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350"
+  integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
 
 unpipe@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
   integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
 
-util-deprecate@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
 v8-to-istanbul@^9.0.1:
   version "9.3.0"
   resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"
@@ -3173,32 +3231,19 @@
   resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
   integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
 
-webidl-conversions@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
-  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
-
 webidl-conversions@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
   integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
 
-whatwg-url@^11.0.0:
-  version "11.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
-  integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+whatwg-url@^14.0.0:
+  version "14.2.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663"
+  integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
   dependencies:
-    tr46 "^3.0.0"
+    tr46 "^5.1.0"
     webidl-conversions "^7.0.0"
 
-whatwg-url@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
-  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
-  dependencies:
-    tr46 "~0.0.3"
-    webidl-conversions "^3.0.0"
-
 which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -3234,35 +3279,30 @@
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
-ws@8.13.0:
-  version "8.13.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
-  integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
-
-ws@^7.4.2, ws@^7.5.10:
+ws@^7.5.10:
   version "7.5.10"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
   integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
 
+ws@^8.18.3:
+  version "8.18.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
+  integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
+
 y18n@^5.0.5:
   version "5.0.8"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
-yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
 yargs-parser@^21.1.1:
   version "21.1.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
   integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
 
-yargs@17.7.1:
-  version "17.7.1"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"
-  integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==
+yargs@^17.7.2:
+  version "17.7.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
   dependencies:
     cliui "^8.0.1"
     escalade "^3.1.1"
@@ -3284,3 +3324,8 @@
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.4.0.tgz#0cf0aa57e9c24f8a2cbde0cc1ca2c9592ac4e0f6"
   integrity sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==
+
+zod@^3.24.1:
+  version "3.25.76"
+  resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
+  integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index ceea5d0..3c4270b 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -21,21 +21,6 @@
     output_to_bindir = 1,
 )
 
-# This is a dependency for karma_test rule in js.bzl that is only used by
-# plugins.
-sh_binary(
-    name = "karma_bin",
-    srcs = ["@ui_dev_npm//:node_modules/karma/bin/karma"],
-    data = [
-        "@ui_dev_npm//@open-wc/karma-esm",
-        "@ui_dev_npm//karma-chrome-launcher",
-        "@ui_dev_npm//karma-mocha",
-        "@ui_dev_npm//karma-mocha-reporter",
-        "@ui_dev_npm//karma/bin:karma",
-        "@ui_dev_npm//mocha",
-    ],
-)
-
 # This is a dependency for web_test_runner rule in js.bzl that is only used by
 # plugins.
 sh_binary(
@@ -60,7 +45,4 @@
 )
 
 # This is used by plugins.
-exports_files([
-    "karma.conf.js",
-    "web-test-runner.config.mjs",
-])
+exports_files(["web-test-runner.config.mjs"])
diff --git a/polygerrit-ui/GEMINI.md b/polygerrit-ui/GEMINI.md
new file mode 100644
index 0000000..6259e9f
--- /dev/null
+++ b/polygerrit-ui/GEMINI.md
@@ -0,0 +1,263 @@
+# Gemini Project Profile: gerrit/polygerrit-ui
+
+This document provides a summary of the front-end development environment for the `polygerrit-ui` part of the gerrit project.
+
+## Overview
+
+- **Framework**: The project is built with Web Components, using Lit
+- **Language**: TypeScript
+- **Build Tool**: Bazel is the primary build tool for the overall project.
+- **Testing**:
+  - **Runner**: `@web/test-runner`
+  - **Framework**: `mocha`
+  - **Assertions/Mocks**: `sinon`, `@open-wc/semantic-dom-diff`
+  - **Browser Testing**: `playwright`
+- **Package Manager**: `yarn`
+
+## Key Commands
+
+The following commands should be run from the project root directory.
+
+### Checking for TypeScript Errors
+
+To check for TypeScript errors without running the full test suite, use the `compile` script:
+
+```bash
+yarn compile
+```
+
+### Running Tests
+
+- `yarn test`: Run all tests.
+- `yarn test:screenshot`: Run visual regression tests.
+
+## Running Single Tests
+
+Running the full test suite can be slow. For a faster feedback loop during development, you can run tests for a single file. Use the `test:single:nowatch` script with the path to the test file. This will run the test once and exit.
+
+**Command:**
+
+```bash
+yarn test:single:nowatch "**/test_file.ts"
+```
+
+If you want to run the test in watch mode, you can use `test:single`.
+
+**Examples:**
+
+```bash
+yarn test:single:nowatch "**/gr-account-list_test.ts"
+yarn test:single:nowatch "**/gr-user-suggestion_test.ts"
+```
+
+## UI Elements
+
+Here is a list of the UI elements found in `polygerrit-ui/app/elements` with a brief description of their purpose.
+
+- `gr-access-section`: Manages a section of the access rights page for a repository.
+- `gr-admin-group-list`: Displays a list of user groups in the admin section.
+- `gr-admin-view`: The main view for the admin section, containing navigation and sub-views.
+- `gr-confirm-delete-item-dialog`: A confirmation dialog for deleting an item.
+- `gr-create-change-dialog`: A dialog for creating a new change.
+- `gr-create-file-edit-dialog`: A dialog for creating a new file edit.
+- `gr-create-group-dialog`: A dialog for creating a new user group.
+- `gr-create-pointer-dialog`: A dialog for creating a new branch or tag.
+- `gr-create-repo-dialog`: A dialog for creating a new repository.
+- `gr-group-audit-log`: Displays the audit log for a user group.
+- `gr-group-members`: Manages the members of a user group.
+- `gr-group`: Displays and manages the details of a user group.
+- `gr-permission`: Manages a single permission within an access section.
+- `gr-plugin-config-array-editor`: An editor for a plugin configuration option that is an array.
+- `gr-plugin-list`: Displays a list of installed plugins.
+- `gr-repo-access`: Manages the access rights for a repository.
+- `gr-repo-commands`: Displays repository-related commands.
+- `gr-repo-dashboards`: Manages the dashboards for a repository.
+- `gr-repo-detail-list`: Displays a list of branches or tags for a repository.
+- `gr-repo-list`: Displays a list of repositories.
+- `gr-repo-plugin-config`: Manages the plugin configuration for a repository.
+- `gr-repo-submit-requirements`: Manages the submit requirements for a repository.
+- `gr-repo`: Displays and manages the details of a repository.
+- `gr-rule-editor`: An editor for a single access rule.
+- `gr-server-info`: Displays information about the Gerrit server.
+- `gr-change-list-action-bar`: An action bar for the top of a change list section.
+- `gr-change-list-bulk-abandon-flow`: A flow for abandoning multiple changes in bulk.
+- `gr-change-list-bulk-vote-flow`: A flow for voting on multiple changes in bulk.
+- `gr-change-list-column-requirement`: Displays a single submit requirement in a change list column.
+- `gr-change-list-column-requirements-summary`: Displays a summary of submit requirements in a change list column.
+- `gr-change-list-copy-link-flow`: A flow for copying links to multiple changes.
+- `gr-change-list-hashtag-flow`: A flow for adding hashtags to multiple changes.
+- `gr-change-list-item`: A single item in a change list.
+- `gr-change-list-reviewer-flow`: A flow for adding reviewers to multiple changes.
+- `gr-change-list-section`: A section of a change list.
+- `gr-change-list-topic-flow`: A flow for setting the topic of multiple changes.
+- `gr-change-list-view`: The main view for a list of changes.
+- `gr-change-list`: A list of changes.
+- `gr-create-change-help`: A help dialog for creating a new change.
+- `gr-create-commands-dialog`: A dialog that displays the commands for creating a new change.
+- `gr-create-destination-dialog`: A dialog for selecting the destination for a new change.
+- `gr-dashboard-view`: The main view for a user's dashboard.
+- `gr-repo-header`: The header for a repository page.
+- `gr-user-header`: The header for a user's page.
+- `gr-ai-prompt-dialog`: A dialog for generating AI prompts.
+- `gr-change-actions`: The action bar for a single change.
+- `gr-change-metadata`: The metadata section for a single change.
+- `gr-change-summary`: A summary of a single change.
+- `gr-checks-chip`: A chip for displaying the status of a check.
+- `gr-summary-chip`: A chip for displaying a summary of information.
+- `gr-change-view`: The main view for a single change.
+- `gr-comments-summary`: A summary of the comments on a change.
+- `gr-commit-info`: Displays information about a single commit.
+- `gr-confirm-abandon-dialog`: A confirmation dialog for abandoning a change.
+- `gr-confirm-cherrypick-conflict-dialog`: A confirmation dialog for a cherry-pick conflict.
+- `gr-confirm-cherrypick-dialog`: A confirmation dialog for cherry-picking a change.
+- `gr-confirm-move-dialog`: A confirmation dialog for moving a change.
+- `gr-confirm-rebase-dialog`: A confirmation dialog for rebasing a change.
+- `gr-confirm-revert-dialog`: A confirmation dialog for reverting a change.
+- `gr-confirm-submit-dialog`: A confirmation dialog for submitting a change.
+- `gr-copy-links`: A dialog for copying links related to a change.
+- `gr-download-dialog`: A dialog for downloading a patch set.
+- `gr-file-list-header`: The header for a file list.
+- `gr-file-list`: A list of files in a patch set.
+- `gr-included-in-dialog`: A dialog that displays where a change is included.
+- `gr-label-score-row`: A row in the label scores table.
+- `gr-label-scores`: A table of label scores.
+- `gr-message-scores`: Displays the scores from a change message.
+- `gr-message`: A single message in a change's history.
+- `gr-messages-list`: A list of messages in a change's history.
+- `gr-related-change`: A single related change.
+- `gr-related-changes-list`: A list of related changes.
+- `gr-related-collapse`: A collapsible section of related changes.
+- `gr-reply-dialog`: The reply dialog for a change.
+- `gr-reviewer-list`: A list of reviewers on a change.
+- `gr-revision-parents`: Displays the parents of a revision.
+- `gr-submit-requirement-dashboard-hovercard`: A hovercard for a submit requirement on the dashboard.
+- `gr-submit-requirement-hovercard`: A hovercard for a submit requirement.
+- `gr-submit-requirements`: A list of submit requirements.
+- `gr-thread-list`: A list of comment threads.
+- `gr-trigger-vote-hovercard`: A hovercard for a trigger vote.
+- `gr-trigger-vote`: A trigger vote chip.
+- `gr-validation-options`: Options for validation.
+- `gr-checks-action`: An action for a check.
+- `gr-checks-attempt`: An attempt for a check.
+- `gr-checks-chip-for-label`: A chip for a check on a label.
+- `gr-checks-fix-preview`: A preview of a fix for a check.
+- `gr-checks-results`: The results of a check.
+- `gr-checks-runs`: The runs of a check.
+- `gr-checks-tab`: The checks tab.
+- `gr-diff-check-result`: The result of a diff check.
+- `gr-hovercard-run`: A hovercard for a check run.
+- `gr-account-dropdown`: A dropdown for the user's account.
+- `gr-error-dialog`: A dialog for displaying an error.
+- `gr-error-manager`: Manages errors.
+- `gr-key-binding-display`: Displays a key binding.
+- `gr-keyboard-shortcuts-dialog`: A dialog for displaying keyboard shortcuts.
+- `gr-main-header`: The main header of the application.
+- `gr-navigation`: Manages navigation.
+- `gr-notifications-prompt`: A prompt for notifications.
+- `gr-router`: The main router for the application.
+- `gr-search-bar`: The search bar.
+- `gr-smart-search`: A search bar with smart suggestions.
+- `gr-apply-fix-dialog`: A dialog for applying a fix.
+- `gr-comment-api`: The API for comments.
+- `gr-diff-host`: The host for a diff.
+- `gr-diff-mode-selector`: A selector for the diff mode.
+- `gr-diff-preferences-dialog`: A dialog for diff preferences.
+- `gr-diff-view`: The main view for a diff.
+- `gr-patch-range-select`: A selector for the patch range.
+- `gr-documentation-search`: A search bar for documentation.
+- `gr-default-editor`: The default editor.
+- `gr-edit-controls`: Controls for editing.
+- `gr-edit-file-controls`: Controls for editing a file.
+- `gr-edit-preferences-dialog`: A dialog for edit preferences.
+- `gr-editor-view`: The main view for an editor.
+- `gr-app-element`: The main application element.
+- `gr-app`: The main application component.
+- `gr-css-mixins`: CSS mixins.
+- `fit-controller`: A controller for fitting an element.
+- `incremental-repeat`: A directive for incrementally repeating an element.
+- `shortcut-controller`: A controller for shortcuts.
+- `subscription-controller`: A controller for subscriptions.
+- `gr-admin-api`: The API for admin functions.
+- `gr-attribute-helper`: A helper for attributes.
+- `gr-change-updates-api`: The API for change updates.
+- `gr-checks-api`: The API for checks.
+- `gr-dom-hooks`: Hooks for the DOM.
+- `gr-endpoint-decorator`: A decorator for endpoints.
+- `gr-endpoint-param`: A parameter for an endpoint.
+- `gr-endpoint-slot`: A slot for an endpoint.
+- `gr-event-helper`: A helper for events.
+- `gr-plugin-host`: The host for a plugin.
+- `gr-plugin-screen`: The screen for a plugin.
+- `gr-plugin-popup`: A popup for a plugin.
+- `gr-popup-interface`: The interface for a popup.
+- `gr-suggestions-api`: The API for suggestions.
+- `gr-account-info`: Displays information about an account.
+- `gr-agreements-list`: A list of agreements.
+- `gr-change-table-editor`: An editor for the change table.
+- `gr-cla-view`: The view for a CLA.
+- `gr-edit-preferences`: Preferences for editing.
+- `gr-email-editor`: An editor for emails.
+- `gr-gpg-editor`: An editor for GPG keys.
+- `gr-group-list`: A list of groups.
+- `gr-http-password`: A password for HTTP.
+- `gr-identities`: A list of identities.
+- `gr-menu-editor`: An editor for the menu.
+- `gr-preferences`: Preferences.
+- `gr-registration-dialog`: A dialog for registration.
+- `gr-settings-view`: The main view for settings.
+- `gr-ssh-editor`: An editor for SSH keys.
+- `gr-watched-projects-editor`: An editor for watched projects.
+- `gr-account-chip`: A chip for an account.
+- `gr-account-entry`: An entry for an account.
+- `gr-account-label`: A label for an account.
+- `gr-account-list`: A list of accounts.
+- `gr-alert`: An alert.
+- `gr-autocomplete-dropdown`: A dropdown for an autocomplete.
+- `gr-autocomplete`: An autocomplete.
+- `gr-avatar`: An avatar.
+- `gr-avatar-stack`: A stack of avatars.
+- `gr-button`: A button.
+- `gr-change-star`: A star for a change.
+- `gr-change-status`: The status of a change.
+- `gr-comment-model`: The model for a comment.
+- `gr-comment-thread`: A thread of comments.
+- `gr-comment`: A single comment.
+- `gr-confirm-delete-comment-dialog`: A confirmation dialog for deleting a comment.
+- `gr-copy-clipboard`: A button for copying to the clipboard.
+- `gr-cursor-manager`: Manages the cursor.
+- `gr-date-formatter`: Formats a date.
+- `gr-dialog`: A dialog.
+- `gr-diff-preferences`: Preferences for a diff.
+- `gr-download-commands`: Commands for downloading.
+- `gr-dropdown-list`: A list for a dropdown.
+- `gr-dropdown`: A dropdown.
+- `gr-editable-content`: Editable content.
+- `gr-editable-label`: An editable label.
+- `gr-file-status`: The status of a file.
+- `gr-fix-suggestions`: Suggestions for a fix.
+- `gr-formatted-text`: Formatted text.
+- `gr-hovercard-account`: A hovercard for an account.
+- `gr-hovercard`: A hovercard.
+- `gr-icon`: An icon.
+- `gr-js-api-interface`: The interface for the JS API.
+- `gr-label-info`: Information about a label.
+- `gr-labeled-autocomplete`: An autocomplete with a label.
+- `gr-lib-loader`: Loads a library.
+- `gr-limited-text`: Text with a limited length.
+- `gr-linked-chip`: A chip with a link.
+- `gr-list-view`: A view for a list.
+- `gr-marked-element`: An element for markdown.
+- `gr-page-nav`: Navigation for a page.
+- `gr-repo-branch-picker`: A picker for a repo and branch.
+- `gr-rest-api-interface`: The interface for the REST API.
+- `gr-select`: A select.
+- `gr-selector`: A selector.
+- `gr-shell-command`: A shell command.
+- `gr-suggestion-diff-preview`: A preview of a suggestion diff.
+- `gr-suggestion-textarea`: A textarea with suggestions.
+- `gr-tooltip-content`: Content for a tooltip.
+- `gr-tooltip`: A tooltip.
+- `gr-user-suggestion-fix`: A fix for a user suggestion.
+- `gr-vote-chip`: A chip for a vote.
+- `gr-weblink`: A weblink.
+- `revision-info`: Information about a revision.
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index a9d10be..ac51959 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -176,6 +176,26 @@
 yarn test:single "**/gr-comment_test.ts"
 ```
 
+### Screenshot Tests
+
+We use screenshot tests to prevent unintended visual regressions.
+
+To run the screenshot tests:
+```sh
+yarn test:screenshot
+```
+
+If a test fails, it means the component's appearance has changed. New screenshots will be generated in the `polygerrit-ui/screenshots/Chromium/failed/` directory. In case of a mismatch with an existing baseline, a diff image will also be created there.
+
+If the change is intended, you need to approve the new screenshots as the baseline. To do this, move the new screenshot files from the `failed` directory to the `baseline` directory, overwriting the old ones. The diff images in the `failed` directory can be deleted.
+
+```sh
+# Move all failed screenshots at once:
+mv polygerrit-ui/screenshots/Chromium/failed/*.png polygerrit-ui/screenshots/Chromium/baseline/
+```
+
+After moving the file(s), run `yarn test:screenshot` again to confirm that they pass.
+
 Compiling code:
 ```sh
 # Compile frontend once to check for type errors:
@@ -197,7 +217,7 @@
 `eslint-config-google` is a port of the Google JS Style Guide to an ESLint
 config module, and `eslint-plugin-html` allows ESLint to lint scripts inside
 HTML.
-We have an .eslintrc.json config file in the polygerrit-ui/ directory configured
+We have an eslint-bazel.config.js config file in the polygerrit-ui/ directory configured
 to enforce the preferred style of the PolyGerrit project.
 After installing, you can use `eslint` on any new file you create.
 In addition, you can supply the `--fix` flag to apply some suggested fixes for
diff --git a/polygerrit-ui/app/.eslint-ts-resolver.js b/polygerrit-ui/app/.eslint-ts-resolver.config.js
similarity index 100%
rename from polygerrit-ui/app/.eslint-ts-resolver.js
rename to polygerrit-ui/app/.eslint-ts-resolver.config.js
diff --git a/polygerrit-ui/app/.eslintignore b/polygerrit-ui/app/.eslintignore
deleted file mode 100644
index 087a049..0000000
--- a/polygerrit-ui/app/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-**/node_modules
-**/rollup.config.js
-node_modules_licenses
-!.eslintrc-bazel.js
diff --git a/polygerrit-ui/app/.eslintrc-bazel.js b/polygerrit-ui/app/.eslintrc-bazel.js
deleted file mode 100644
index fa6c274..0000000
--- a/polygerrit-ui/app/.eslintrc-bazel.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-// This file has a special settings for bazel.
-// The settings is required because bazel uses different location
-// for node_modules.
-
-function getBazelSettings() {
-  const runFilesDir = process.env['RUNFILES_DIR'];
-  if (!runFilesDir) {
-    // eslint is executed with 'bazel run ...' to fix the source code. It runs
-    // against real source code, no special paths for node_modules is set.
-    return {};
-  }
-  // eslint is executed with 'bazel test...'. Set path to required node_modules
-  return {
-    'import/resolver': {
-      node: {
-        paths: [
-          `${runFilesDir}/ui_npm/node_modules`,
-          `${runFilesDir}/ui_dev_npm/node_modules`,
-        ],
-      },
-    },
-  };
-}
-
-module.exports = {
-  extends: './.eslintrc.js',
-  settings: getBazelSettings(),
-};
diff --git a/polygerrit-ui/app/.prettierrc.js b/polygerrit-ui/app/.prettierrc.js
index 8f353bf..8920451 100644
--- a/polygerrit-ui/app/.prettierrc.js
+++ b/polygerrit-ui/app/.prettierrc.js
@@ -9,7 +9,8 @@
     {
       "files": ["**/*.ts"],
       "options": {
-          ...require('gts/.prettierrc.json')
+          ...require('gts/.prettierrc.json'),
+          "trailingComma": "es5"
       }
     }
   ]
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 925820c..d94c10c 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -73,6 +73,8 @@
             "node_modules/**",
             "node_modules_licenses/**",
             "rollup.config.js",
+            "eslint-bazel.config.js",
+            "eslint.config.js",
         ],
     ),
     allow_js = True,
@@ -141,12 +143,12 @@
 eslint(
     name = "lint",
     srcs = [":eslint_src_code"],
-    config = ".eslintrc-bazel.js",
+    config = "eslint-bazel.config.js",
     data = [
-        # The .eslintrc-bazel.js extends the .eslintrc.js config, pass it as a dependency
-        ".eslintrc.js",
+        # The eslint-bazel.config.js extends the eslint.config.js config, pass it as a dependency
+        "eslint.config.js",
         ".prettierrc.js",
-        ".eslint-ts-resolver.js",
+        ".eslint-ts-resolver.config.js",
         "tsconfig_eslint.json",
         # tsconfig_eslint.json extends tsconfig.json, pass it as a dependency
         "tsconfig.json",
@@ -157,7 +159,6 @@
         ".js",
         ".ts",
     ],
-    ignore = ".eslintignore",
     plugins = [
         "@npm//@typescript-eslint/eslint-plugin",
         "@npm//eslint-config-google",
@@ -165,8 +166,10 @@
         "@npm//eslint-plugin-import",
         "@npm//eslint-plugin-jsdoc",
         "@npm//eslint-plugin-lit",
+        "@npm//eslint-plugin-n",
         "@npm//eslint-plugin-prettier",
         "@npm//eslint-plugin-regex",
+        "@npm//eslint-plugin-es-x",
         "@npm//gts",
     ],
 )
@@ -208,7 +211,9 @@
         # would be possible, if we would run the lit-analyzer as a ts plugin.
         # In tsconfig.json there is an option `globalAttributes` that we could
         # use. But that is not available when running lit-analyzer as cli.
-        "--rules.no-unknown-attribute warn",
+        "--rules.no-unknown-attribute off",
+        # TODO: enable once we migrate from iron elements to material-web
+        "--rules.no-unknown-tag-name off",
     ],
 )
 
diff --git a/polygerrit-ui/app/api/change-updates.ts b/polygerrit-ui/app/api/change-updates.ts
new file mode 100644
index 0000000..6ed9a8d
--- /dev/null
+++ b/polygerrit-ui/app/api/change-updates.ts
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export declare interface ChangeUpdatesPluginApi {
+  /**
+   * Must only be called once. You cannot register twice. You cannot unregister.
+   */
+  register(publisher: ChangeUpdatesPublisher): void;
+}
+
+/**
+ * Publisher that notifies subscribers whenever the change updates.
+ *
+ * This update could be change being submitted, new patchset being uploaded etc.
+ * Plugins are expected to use the ChangeUpdatesPluginApi to register a publisher.
+ * Gerrit will use the "subscribe" method to register a callback to this publisher.
+ * One use case is updating the ChangeModel state whenever the change is updated.
+ */
+export declare interface ChangeUpdatesPublisher {
+  /**
+   * Subcribers can use this method to add a callback that will be triggered when updates to the change happen.
+   *
+   * @param repo Repository containing the change.
+   * @param change The change number of the change for which update events are published.
+   * @param callback The callback to be called when the change is updated.
+   */
+  subscribe(repo: string, change: number, callback: () => void): void;
+  /**
+   * Remove existing callbacks. Does nothing if no subscriber was added.
+   */
+  unsubscribe(): void;
+}
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index 0d5d073..d90a302 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -462,6 +462,14 @@
    * not have to be part of the change yet.
    */
   fixes?: Fix[];
+
+  /**
+   * If the result has a code pointer that points to an *unchanged* piece of
+   * code, then we hide the result, if this value is `false`.
+   *
+   * Default: true
+   */
+  show_on_unchanged_lines?: boolean;
 }
 
 export declare interface Fix {
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 000519b..e5d0af7 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -346,6 +346,14 @@
   range: CommentRange | undefined;
 }
 
+/** The detail of the 'range-selected-mouse-up' event dispatched by gr-diff. */
+export declare interface RangeSelectedEventDetail {
+  side: Side;
+  lineNum: LineNumber;
+  range: CommentRange | undefined;
+  isMouseUp: boolean;
+}
+
 export declare interface ContentLoadNeededEventDetail {
   lineRange: {
     left: LineRange;
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index d6425be..37e68e3 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -68,3 +68,11 @@
   hint?: string;
   setRangeText: (replacement: string, start: number, end: number) => void;
 }
+
+/** <gr-autogrow-textarea> interface that external users can rely on */
+export declare interface GrAutogrowTextarea extends HTMLElement {
+  value?: string;
+  nativeElement?: HTMLElement;
+  placeholder?: string;
+  setRangeText: (replacement: string, start: number, end: number) => void;
+}
diff --git a/polygerrit-ui/app/api/package.json b/polygerrit-ui/app/api/package.json
index f8d8ee3..e653794 100644
--- a/polygerrit-ui/app/api/package.json
+++ b/polygerrit-ui/app/api/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@gerritcodereview/typescript-api",
-  "version": "3.12.0",
+  "version": "3.13.0",
   "description": "Gerrit Code Review - TypeScript API",
   "homepage": "https://www.gerritcodereview.com/",
   "browser": true,
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 88d4376..7695b60 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -17,6 +17,7 @@
 import {HookApi, RegisterOptions} from './hook';
 import {StylePluginApi} from './styles';
 import {SuggestionsPluginApi} from './suggestions';
+import {ChangeUpdatesPluginApi} from './change-updates';
 
 export enum TargetElement {
   CHANGE_ACTIONS = 'changeactions',
@@ -25,10 +26,14 @@
 
 // Note: for new events, naming convention should be: `a-b`
 export enum EventType {
+  BEFORE_CHANGE_ACTION = 'before-change-action',
   LABEL_CHANGE = 'labelchange',
   SHOW_CHANGE = 'showchange',
   SUBMIT_CHANGE = 'submitchange',
+  // Fires GerritView values such as 'change', 'dashboard', 'admin', ...
+  VIEW_CHANGE = 'view-change',
   SHOW_REVISION_ACTIONS = 'show-revision-actions',
+  BEFORE_COMMIT_MSG_EDIT = 'before-commit-msg-edit',
   COMMIT_MSG_EDIT = 'commitmsgedit',
   CUSTOM_EMOJIS = 'custom-emojis',
   REVERT = 'revert',
@@ -36,6 +41,7 @@
   POST_REVERT = 'postrevert',
   ADMIN_MENU_LINKS = 'admin-menu-links',
   SHOW_DIFF = 'showdiff',
+  BEFORE_REPLY_SENT = 'before-reply-sent',
   REPLY_SENT = 'replysent',
   PUBLISH_EDIT = 'publish-edit',
 }
@@ -62,6 +68,7 @@
   attributeHelper(element: Element): AttributeHelperPluginApi;
   changeActions(): ChangeActionsPluginApi;
   changeReply(): ChangeReplyPluginApi;
+  changeUpdates(): ChangeUpdatesPluginApi;
   checks(): ChecksPluginApi;
   suggestions(): SuggestionsPluginApi;
   eventHelper(element: Node): EventHelperPluginApi;
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index b31c2c8..3d716f7 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -195,6 +195,16 @@
 }
 
 /**
+ * The state of a flow stage.
+ */
+export enum FlowStageState {
+  DONE = 'DONE',
+  FAILED = 'FAILED',
+  PENDING = 'PENDING',
+  TERMINATED = 'TERMINATED',
+}
+
+/**
  * types and interfaces ========================================================
  */
 
@@ -226,6 +236,7 @@
   status?: string; // status message of the account
   inactive?: boolean; // not set if false
   tags?: string[];
+  deleted?: boolean; // not set if false
 }
 
 /**
@@ -324,17 +335,17 @@
   edit_full_name_url?: string;
   http_password_url?: string;
   git_basic_auth_policy?: string;
+  max_token_lifetime?: string;
 }
 
 /**
- * The AvartarInfo entity contains information about an avatar image ofan
+ * The AvatarInfo entity contains information about an avatar image of an
  * account.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#avatar-info
  */
 export declare interface AvatarInfo {
   url: string;
   height: number;
-  width: number;
 }
 
 // The refs/heads/ prefix is omitted in Branch name
@@ -353,7 +364,7 @@
   disable_private_changes?: boolean;
   mergeability_computation_behavior: MergeabilityComputationBehavior;
   conflicts_predicate_enabled?: boolean;
-  enable_robot_comments?: boolean;
+  allow_markdown_base64_images_in_comments?: boolean;
 }
 
 export type ChangeId = BrandType<string, '_changeId'>;
@@ -696,6 +707,54 @@
 }
 
 /**
+ * The FlowActionInfo entity contains information about a flow action.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#flow-action-info
+ */
+export declare interface FlowActionInfo {
+  name: string;
+  parameters?: string[];
+}
+
+/**
+ * The FlowExpressionInfo entity contains information about a flow expression.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#flow-expression-info
+ */
+export declare interface FlowExpressionInfo {
+  condition: string;
+  action?: FlowActionInfo;
+}
+
+/**
+ * The FlowInfo entity contains information about a flow.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#flow-info
+ */
+export declare interface FlowInfo {
+  uuid: string;
+  owner: AccountInfo;
+  created: Timestamp;
+  last_evaluated?: Timestamp;
+  stages: FlowStageInfo[];
+}
+
+/**
+ * The FlowInput entity is used for creating a new flow.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#flow-input
+ */
+export declare interface FlowInput {
+  stage_expressions: FlowExpressionInfo[];
+}
+
+/**
+ * The FlowStageInfo entity contains information about a flow stage.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#flow-stage-info
+ */
+export declare interface FlowStageInfo {
+  expression: FlowExpressionInfo;
+  state: FlowStageState;
+  message?: string;
+}
+
+/**
  * The FileInfo entity contains information about a file in a patch set.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#file-info
  */
@@ -709,6 +768,8 @@
   size?: number; // in bytes
   old_mode?: number;
   new_mode?: number;
+  old_sha?: string;
+  new_sha?: string;
 }
 
 /**
@@ -784,6 +845,15 @@
   includes?: GroupInfo[];
 }
 
+/**
+ * The GroupsInfo entity contains information about Gerrit configuration from
+ * the gerrit section.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#groups-info
+ */
+export declare interface GroupsConfigInfo {
+  enable_delete_group?: boolean;
+}
+
 export type GroupName = BrandType<string, '_groupName'>;
 
 /**
@@ -1090,6 +1160,7 @@
   change: ChangeConfigInfo;
   download: DownloadInfo;
   gerrit: GerritInfo;
+  groups: GroupsConfigInfo;
   note_db_enabled?: boolean;
   plugin: PluginConfigInfo;
   receive?: ReceiveInfo;
@@ -1098,6 +1169,7 @@
   user: UserConfigInfo;
   default_theme?: string;
   submit_requirement_dashboard_columns?: string[];
+  dashboard_show_all_labels?: boolean;
   metadata?: MetadataInfo[];
 }
 
@@ -1391,6 +1463,75 @@
   AnyWithBlock = 'AnyWithBlock',
   MaxNoBlock = 'MaxNoBlock',
   NoBlock = 'NoBlock',
-  Noop = 'Noop',
+  NoOp = 'NoOp',
   PatchSetLock = 'PatchSetLock',
 }
+
+/**
+ * The LabelDefinitionInput entity describes a label definition input.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#label-definition-input
+ */
+export interface LabelDefinitionInput {
+  commit_message?: string;
+  name?: string;
+  description?: string;
+  function?: LabelDefinitionInfoFunction;
+  values?: LabelValueToDescriptionMap;
+  default_value?: number;
+  branches?: string[];
+  can_override?: boolean;
+  copy_condition?: string;
+  unset_copy_condition?: boolean;
+  allow_post_submit?: boolean;
+  ignore_self_approval?: boolean;
+}
+
+/*
+ * The DeleteLabelInput entity describes a delete label input
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#delete-label-input
+ */
+export interface DeleteLabelInput {
+  commit_message?: string;
+}
+
+/**
+ * The BatchLabelInput entity describes a batch label input
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#batch-label-input
+ */
+export interface BatchLabelInput {
+  create?: LabelDefinitionInput[];
+  update?: {[labelName: string]: Partial<LabelDefinitionInput>};
+  delete?: string[];
+}
+
+/**
+ * The SubmitRequirementInput entity describes a submit requirement input.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-requirement-input
+ */
+export interface SubmitRequirementInput {
+  name: string;
+  description?: string;
+  applicability_expression?: string;
+  submittability_expression: string;
+  override_expression?: string;
+  allow_override_in_child_projects?: boolean;
+}
+
+/**
+ * The BatchSubmitRequirementInput entity describes a batch submit requirement input
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#batch-submit-requirement-input
+ */
+export interface BatchSubmitRequirementInput {
+  create?: SubmitRequirementInput[];
+  update?: {[submitRequirementName: string]: Partial<SubmitRequirementInput>};
+  delete?: string[];
+  commit_message?: string;
+}
+
+/**
+ * The IsFlowsEnabledInfo entity contains information about whether flows are
+ * enabled.
+ */
+export declare interface IsFlowsEnabledInfo {
+  enabled: boolean;
+}
diff --git a/polygerrit-ui/app/api/rest.ts b/polygerrit-ui/app/api/rest.ts
index a09f711..b8d6325 100644
--- a/polygerrit-ui/app/api/rest.ts
+++ b/polygerrit-ui/app/api/rest.ts
@@ -3,18 +3,17 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {AccountDetailInfo, ProjectInfoWithName, ServerInfo} from './rest-api';
+import {
+  AccountDetailInfo,
+  HttpMethod,
+  ProjectInfoWithName,
+  ServerInfo,
+} from './rest-api';
+
+export {HttpMethod};
 
 export type RequestPayload = string | object;
 
-export enum HttpMethod {
-  HEAD = 'HEAD',
-  POST = 'POST',
-  GET = 'GET',
-  DELETE = 'DELETE',
-  PUT = 'PUT',
-}
-
 export type ErrorCallback = (
   response?: Response | null,
   err?: Error
diff --git a/polygerrit-ui/app/api/styles.ts b/polygerrit-ui/app/api/styles.ts
index 6ca8496..764cc48 100644
--- a/polygerrit-ui/app/api/styles.ts
+++ b/polygerrit-ui/app/api/styles.ts
@@ -26,7 +26,7 @@
 export declare interface Styles {
   font: Style;
   form: Style;
-  icon: Style;
+  material: Style;
   menuPage: Style;
   spinner: Style;
   subPage: Style;
diff --git a/polygerrit-ui/app/api/suggestions.ts b/polygerrit-ui/app/api/suggestions.ts
index 1aa4ebe..8ea6302 100644
--- a/polygerrit-ui/app/api/suggestions.ts
+++ b/polygerrit-ui/app/api/suggestions.ts
@@ -7,8 +7,8 @@
 import {
   ChangeInfo,
   CommentRange,
-  RevisionPatchSetNum,
   FixSuggestionInfo,
+  RevisionPatchSetNum,
 } from './rest-api';
 
 export declare interface SuggestionsPluginApi {
@@ -67,6 +67,37 @@
    * List of supported file extensions. If undefined, all file extensions supported.
    */
   supportedFileExtensions?: string[];
+  /**
+   * Sends feedback on autocompletion suggestions.
+   * This method allows the plugin to report which suggestions were accepted or
+   * rejected by the user, which can be used for improving future suggestions.
+   */
+  sendAutocompleteFeedback?(feedbackEntries: SuggestionFeedbackEntry[]): void;
+  /**
+   * Sends feedback on 'suggest fix' (code repair) suggestions.
+   * This method allows the plugin to report which suggestions were accepted or
+   * rejected by the user, which can be used for improving future suggestions.
+   *
+   */
+  sendSuggestFixFeedback?(feedbackEntries: SuggestionFeedbackEntry[]): void;
+}
+
+/**
+ * Represents a 128-bit unique identifier for a suggestion, used for tracking feedback.
+ * The ID is split into two 64-bit BigInt components.
+ */
+export declare interface Feedback {
+  /** The least significant 64 bits of the 128-bit identifier. */
+  id1: bigint;
+  /** The most significant 64 bits of the 128-bit identifier. */
+  id2: bigint;
+}
+
+export declare interface SuggestionFeedbackEntry {
+  /** The unique identifier for the suggestion. */
+  feedbackId: Feedback;
+  /** True if the suggestion was accepted by the user, false otherwise. */
+  accepted: boolean;
 }
 
 export declare interface AutocompleteCommentResponse {
@@ -74,6 +105,7 @@
   completion?: string;
   modelVersion?: string;
   outcome?: number;
+  feedback?: Feedback;
 }
 
 export declare interface SuggestCodeResponse {
@@ -96,4 +128,9 @@
   NO_SUGGESTION = 'NO_SUGGESTION',
   OUT_OF_RANGE = 'OUT_OF_RANGE',
   ERROR = 'ERROR',
+  FILE_TOO_LARGE = 'FILE_TOO_LARGE',
+  INVALID_RANGE = 'INVALID_RANGE',
+  UNSUPPORTED_FILE_TYPE = 'UNSUPPORTED_FILE_TYPE',
+  USER_QUOTA_EXCEEDED = 'USER_QUOTA_EXCEEDED',
+  OK_LOW_CONFIDENCE = 'OK_LOW_CONFIDENCE',
 }
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 07af328..f51032c 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -7,7 +7,7 @@
 /**
  * Tab names for primary tabs on change view page.
  */
-import {CopyInfoEventDetail, TextRange, DiffViewMode} from '../api/diff';
+import {CopyInfoEventDetail, DiffViewMode, TextRange} from '../api/diff';
 import {DiffPreferencesInfo} from '../types/diff';
 import {EditPreferencesInfo, PreferencesInfo} from '../types/common';
 import {
@@ -59,11 +59,11 @@
 export enum Tab {
   FILES = 'files',
   /**
-   * When renaming 'comments' or 'findings', UrlFormatter.java must be updated.
+   * When renaming 'comments' UrlFormatter.java must be updated.
    */
   COMMENT_THREADS = 'comments',
-  FINDINGS = 'findings',
   CHECKS = 'checks',
+  FLOWS = 'flows',
 }
 
 /**
diff --git a/polygerrit-ui/app/constants/messages.ts b/polygerrit-ui/app/constants/messages.ts
deleted file mode 100644
index dca1e61..0000000
--- a/polygerrit-ui/app/constants/messages.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-/** Message shown when no threads in gr-thread-list for robot comments */
-export const NO_ROBOT_COMMENTS_THREADS_MSG =
-  'There are no findings for this patchset.';
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index ee9b041..1ea6ec2 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -27,6 +27,7 @@
   CHECKS_API_ERROR = 'checks-api error',
   USER_PREFERENCES_COLUMNS = 'user-preferences-columns',
   PREFER_MERGE_FIRST_PARENT = 'prefer-merge-first-parent',
+  PUBLISHED_DRAFTS_DEDUPLICATED = 'published-drafts-deduplicated',
 }
 
 export enum Timing {
@@ -60,7 +61,7 @@
   STARTUP_DIFF_VIEW_DISPLAYED = 'StartupDiffViewDisplayed',
   // Time from startup to showing initial content of the file list.
   STARTUP_FILE_LIST_DISPLAYED = 'StartupFileListDisplayed',
-  // Time from startup to when the webcomponentsready event is fired. If the event is fired from the webcomponents-lite polyfill, this may be arbitrarily long after the app has started.
+  // Time from startup to when the webcomponentsready event is fired. If the event is fired from the webcomponents-bundle polyfill, this may be arbitrarily long after the app has started.
   WEB_COMPONENTS_READY = 'WebComponentsReady',
   // Time to received all data for change view
   CHANGE_DATA = 'ChangeDataLoaded',
@@ -113,7 +114,6 @@
   COMMENT_SAVED = 'comment-saved',
   DISCARD_COMMENT = 'discard-comment',
   COMMENT_DISCARDED = 'comment-discarded',
-  ROBOT_COMMENTS_STATS = 'robot-comments-stats',
   CHECKS_TAB_RENDERED = 'checks-tab-rendered',
   CHECKS_CHIP_CLICKED = 'checks-chip-clicked',
   CHECKS_CHIP_LINK_CLICKED = 'checks-chip-link-clicked',
@@ -139,8 +139,6 @@
   LINK_CLICK = 'link-click',
   USER_ACTIVE = 'user-active',
   USER_PASSIVE = 'user-passive',
-  // User added generated suggestion to comment
-  GENERATE_SUGGESTION_ADDED = 'generate_suggestion_added',
   // Request for generating suggestion (usually after user typed draft comment)
   GENERATE_SUGGESTION_REQUEST = 'generate_suggestion_request',
   // Response with suggestions
@@ -152,6 +150,9 @@
   GENERATE_SUGGESTION_EDITED = 'generate_suggestion_edited',
   GENERATE_SUGGESTION_COLLAPSED = 'generate_suggestion_collapsed',
   GENERATE_SUGGESTION_EXPANDED = 'generate_suggestion_expanded',
+  GENERATE_SUGGESTION_THUMB_UP = 'generate_suggestion_thumb_up',
+  GENERATE_SUGGESTION_THUMB_DOWN = 'generate_suggestion_thumb_down',
+
   START_REVIEW = 'start-review',
   CODE_REVIEW_APPROVAL = 'code-review-approval',
   FILE_LIST_DIFF_COLLAPSED = 'file-list-diff-collapsed',
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index f1f063c..fc9b2f7 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -3,7 +3,6 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../gr-permission/gr-permission';
@@ -15,8 +14,8 @@
 } from '../../../utils/access-util';
 import {
   EditablePermissionInfo,
-  PermissionAccessSection,
   EditableRepoAccessGroups,
+  PermissionAccessSection,
 } from '../gr-repo-access/gr-repo-access-interfaces';
 import {
   CapabilityInfoMap,
@@ -25,15 +24,20 @@
   RepoName,
 } from '../../../types/common';
 import {fire} from '../../../utils/event-util';
-import {IronInputElement} from '@polymer/iron-input/iron-input';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {ValueChangedEvent} from '../../../types/events';
 import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {repeat} from 'lit/directives/repeat.js';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
+import {MdOutlinedSelect} from '@material/web/select/outlined-select';
 
 const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
 
@@ -43,7 +47,7 @@
 
 @customElement('gr-access-section')
 export class GrAccessSection extends LitElement {
-  @query('#permissionSelect') private permissionSelect?: HTMLSelectElement;
+  @query('#permissionSelect') private permissionSelect?: MdOutlinedSelect;
 
   @property({type: String})
   repo?: RepoName;
@@ -81,6 +85,8 @@
   // private but used in test
   @state() permissions?: PermissionArray<EditablePermissionInfo>;
 
+  @state() private selectedIndex = 0;
+
   constructor() {
     super();
     this.addEventListener('access-saved', () => this.handleAccessSaved());
@@ -88,6 +94,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       fontStyles,
       sharedStyles,
@@ -151,6 +158,8 @@
 
   override render() {
     if (!this.section) return;
+
+    const permissions = this.computePermissions();
     return html`
       <fieldset
         id="section"
@@ -169,18 +178,12 @@
                 <gr-icon id="icon" icon="edit" filled small></gr-icon>
               </gr-button>
             </div>
-            <iron-input
-              class="editRefInput"
-              .bindValue=${this.section?.id}
+            <md-outlined-text-field
+              class="editRefInput showBlueFocusBorder"
+              .value=${this.section?.id ?? ''}
               @input=${this.handleValueChange}
-              @bind-value-changed=${this.handleIdBindValueChanged}
             >
-              <input
-                class="editRefInput"
-                type="text"
-                @input=${this.handleValueChange}
-              />
-            </iron-input>
+            </md-outlined-text-field>
             <gr-button link id="deleteBtn" @click=${this.handleRemoveReference}
               >Remove</gr-button
             >
@@ -194,11 +197,26 @@
             )}
             <div id="addPermission">
               Add permission:
-              <select id="permissionSelect">
-                ${this.computePermissions().map(item =>
-                  this.renderPermissionOptions(item)
+              <md-outlined-select
+                id="permissionSelect"
+                value=${permissions[this.selectedIndex]?.value.id ??
+                permissions[0]?.value.id ??
+                ''}
+                @change=${(e: Event) => {
+                  const sel = e.target as HTMLSelectElement;
+                  this.setSelectionIndex(sel.value);
+                }}
+              >
+                ${repeat(
+                  permissions,
+                  item => item.value.id,
+                  item => html`
+                    <md-select-option value=${item.value.id}>
+                      <div slot="headline">${item.value.name}</div>
+                    </md-select-option>
+                  `
                 )}
-              </select>
+              </md-outlined-select>
               <gr-button link id="addBtn" @click=${this.handleAddPermission}
                 >Add</gr-button
               >
@@ -210,7 +228,7 @@
         <!-- end mainContainer -->
         <div id="deletedContainer">
           <span>${this.computeSectionName()} was deleted</span>
-          <gr-button link="" id="undoRemoveBtn" @click=${this._handleUndoRemove}
+          <gr-button link="" id="undoRemoveBtn" @click=${this.handleUndoRemove}
             >Undo</gr-button
           >
         </div>
@@ -245,13 +263,6 @@
     `;
   }
 
-  private renderPermissionOptions(item: {
-    id: string;
-    value: {name: string; id: string};
-  }) {
-    return html`<option value=${item.value.id}>${item.value.name}</option>`;
-  }
-
   override willUpdate(changedProperties: PropertyValues) {
     if (changedProperties.has('section')) {
       this.updateSection();
@@ -278,10 +289,17 @@
   }
 
   // private but used in test
-  handleValueChange() {
+  handleValueChange(e?: InputEvent) {
+    if (e) {
+      this.section!.id = (e.target as HTMLInputElement).value as GitRef;
+      this.requestUpdate();
+      fire(this, 'section-changed', {value: this.section!});
+    }
+
     if (!this.section) {
       return;
     }
+
     if (!this.section.value.added) {
       this.section.value.modified = this.section.id !== this.originalId;
       this.requestUpdate();
@@ -437,7 +455,7 @@
     fire(this, 'access-modified', {});
   }
 
-  _handleUndoRemove() {
+  private handleUndoRemove() {
     if (!this.section) {
       return;
     }
@@ -447,7 +465,10 @@
   }
 
   editRefInput() {
-    return queryAndAssert<IronInputElement>(this, 'iron-input.editRefInput');
+    return queryAndAssert<MdOutlinedTextField>(
+      this,
+      'md-outlined-text-field.editRefInput'
+    );
   }
 
   editReference() {
@@ -480,7 +501,9 @@
   // private but used in test
   handleAddPermission() {
     assertIsDefined(this.permissionSelect, 'permissionSelect');
-    const value = this.permissionSelect.value as GitRef;
+    const value = this.permissionSelect.getAttribute('value') as GitRef;
+    this.setSelectionIndex(value);
+
     const permission: PermissionArrayItem<EditablePermissionInfo> = {
       id: value,
       value: {rules: {}, added: true},
@@ -515,12 +538,6 @@
     fire(this, 'section-changed', {value: this.section!});
   }
 
-  private handleIdBindValueChanged = (e: BindValueChangeEvent) => {
-    this.section!.id = e.detail.value as GitRef;
-    this.requestUpdate();
-    fire(this, 'section-changed', {value: this.section!});
-  };
-
   private handlePermissionChanged = (
     e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>>,
     index: number
@@ -528,6 +545,14 @@
     this.permissions![index] = e.detail.value;
     this.requestUpdate();
   };
+
+  private setSelectionIndex(value: string) {
+    const currentIndex = this.computePermissions().findIndex(
+      p => p.value.id === value
+    );
+    const nextIndex = currentIndex % this.computePermissions().length;
+    this.selectedIndex = nextIndex;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
index 7656671..a4760d8 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
@@ -14,7 +14,7 @@
 import {GitRef} from '../../../types/common';
 import {queryAndAssert} from '../../../utils/common-util';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-access-section tests', () => {
   let element: GrAccessSection;
@@ -87,12 +87,17 @@
                     role="button"
                     tabindex="0"
                   >
-                    <gr-icon icon="edit" id="icon" small filled></gr-icon>
+                    <gr-icon filled="" icon="edit" id="icon" small="">
+                    </gr-icon>
                   </gr-button>
                 </div>
-                <iron-input class="editRefInput">
-                  <input class="editRefInput" type="text" />
-                </iron-input>
+                <md-outlined-text-field
+                  autocomplete=""
+                  class="editRefInput showBlueFocusBorder"
+                  inputmode=""
+                  type="text"
+                >
+                </md-outlined-text-field>
                 <gr-button
                   aria-disabled="false"
                   id="deleteBtn"
@@ -107,46 +112,103 @@
                 <gr-permission> </gr-permission>
                 <div id="addPermission">
                   Add permission:
-                  <select id="permissionSelect">
-                    <option value="label-Code-Review">Label Code-Review</option>
-                    <option value="labelAs-Code-Review">
-                      Label Code-Review (On Behalf Of)
-                    </option>
-                    <option value="removeLabel-Code-Review">
-                      Remove Label Code-Review
-                    </option>
-                    <option value="abandon">Abandon</option>
-                    <option value="addPatchSet">Add Patch Set</option>
-                    <option value="create">Create Reference</option>
-                    <option value="createSignedTag">Create Signed Tag</option>
-                    <option value="createTag">Create Annotated Tag</option>
-                    <option value="delete">Delete Reference</option>
-                    <option value="deleteChanges">Delete Changes</option>
-                    <option value="deleteOwnChanges">Delete Own Changes</option>
-                    <option value="editHashtags">Edit Hashtags</option>
-                    <option value="editTopicName">Edit Topic Name</option>
-                    <option value="forgeAuthor">Forge Author Identity</option>
-                    <option value="forgeCommitter">
-                      Forge Committer Identity
-                    </option>
-                    <option value="forgeServerAsCommitter">
-                      Forge Server Identity
-                    </option>
-                    <option value="owner">Owner</option>
-                    <option value="push">Push</option>
-                    <option value="pushMerge">Push Merge Commit</option>
-                    <option value="rebase">Rebase</option>
-                    <option value="removeReviewer">Remove Reviewer</option>
-                    <option value="revert">Revert</option>
-                    <option value="submit">Submit</option>
-                    <option value="submitAs">Submit (On Behalf Of)</option>
-                    <option value="toggleWipState">
-                      Toggle Work In Progress State
-                    </option>
-                    <option value="viewPrivateChanges">
-                      View Private Changes
-                    </option>
-                  </select>
+                  <md-outlined-select
+                    id="permissionSelect"
+                    value="label-Code-Review"
+                  >
+                    <md-select-option md-menu-item="" value="label-Code-Review">
+                      <div slot="headline">Label Code-Review</div>
+                    </md-select-option>
+                    <md-select-option
+                      md-menu-item=""
+                      value="labelAs-Code-Review"
+                    >
+                      <div slot="headline">
+                        Label Code-Review (On Behalf Of)
+                      </div>
+                    </md-select-option>
+                    <md-select-option
+                      md-menu-item=""
+                      value="removeLabel-Code-Review"
+                    >
+                      <div slot="headline">Remove Label Code-Review</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="abandon">
+                      <div slot="headline">Abandon</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="addPatchSet">
+                      <div slot="headline">Add Patch Set</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="create">
+                      <div slot="headline">Create Reference</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="createSignedTag">
+                      <div slot="headline">Create Signed Tag</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="createTag">
+                      <div slot="headline">Create Annotated Tag</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="delete">
+                      <div slot="headline">Delete Reference</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="deleteChanges">
+                      <div slot="headline">Delete Changes</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="deleteOwnChanges">
+                      <div slot="headline">Delete Own Changes</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="editHashtags">
+                      <div slot="headline">Edit Hashtags</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="editTopicName">
+                      <div slot="headline">Edit Topic Name</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="forgeAuthor">
+                      <div slot="headline">Forge Author Identity</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="forgeCommitter">
+                      <div slot="headline">Forge Committer Identity</div>
+                    </md-select-option>
+                    <md-select-option
+                      md-menu-item=""
+                      value="forgeServerAsCommitter"
+                    >
+                      <div slot="headline">Forge Server Identity</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="owner">
+                      <div slot="headline">Owner</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="push">
+                      <div slot="headline">Push</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="pushMerge">
+                      <div slot="headline">Push Merge Commit</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="rebase">
+                      <div slot="headline">Rebase</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="removeReviewer">
+                      <div slot="headline">Remove Reviewer</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="revert">
+                      <div slot="headline">Revert</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="submit">
+                      <div slot="headline">Submit</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="submitAs">
+                      <div slot="headline">Submit (On Behalf Of)</div>
+                    </md-select-option>
+                    <md-select-option md-menu-item="" value="toggleWipState">
+                      <div slot="headline">Toggle Work In Progress State</div>
+                    </md-select-option>
+                    <md-select-option
+                      md-menu-item=""
+                      value="viewPrivateChanges"
+                    >
+                      <div slot="headline">View Private Changes</div>
+                    </md-select-option>
+                  </md-outlined-select>
                   <gr-button
                     aria-disabled="false"
                     id="addBtn"
@@ -571,8 +633,18 @@
 
       test('add permission', async () => {
         element.editing = true;
-        queryAndAssert<HTMLSelectElement>(element, '#permissionSelect').value =
-          'label-Code-Review';
+        let permissionSelect = queryAndAssert<HTMLSelectElement>(
+          element,
+          '#permissionSelect'
+        );
+        permissionSelect.value = 'label-Code-Review';
+        permissionSelect.dispatchEvent(
+          new CustomEvent('change', {
+            composed: true,
+            bubbles: true,
+          })
+        );
+        await element.updateComplete;
         assert.equal(element.permissions!.length, 1);
         assert.equal(Object.keys(element.section!.value.permissions).length, 1);
         queryAndAssert<GrButton>(element, '#addBtn').click();
@@ -599,8 +671,18 @@
           permission.value
         );
 
-        queryAndAssert<HTMLSelectElement>(element, '#permissionSelect').value =
-          'abandon';
+        permissionSelect = queryAndAssert<HTMLSelectElement>(
+          element,
+          '#permissionSelect'
+        );
+        permissionSelect.value = 'abandon';
+        permissionSelect.dispatchEvent(
+          new CustomEvent('change', {
+            composed: true,
+            bubbles: true,
+          })
+        );
+        await element.updateComplete;
         queryAndAssert<GrButton>(element, '#addBtn').click();
         await element.updateComplete;
 
@@ -651,7 +733,10 @@
         );
         assert.isFalse(element.editingRef);
         queryAndAssert<GrButton>(element, '#editBtn').click();
-        element.editRefInput().bindValue = 'new/ref';
+        element.editRefInput().value = 'new/ref';
+        element
+          .editRefInput()
+          .dispatchEvent(new Event('input', {bubbles: true, composed: true}));
         await element.updateComplete;
         assert.equal(element.section.id, 'new/ref');
         assert.isTrue(element.editingRef);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index b30619e..7150c9e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -12,8 +12,8 @@
 import {getAppContext} from '../../../services/app-context';
 import {tableStyles} from '../../../styles/gr-table-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
-import {customElement, query, property, state} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {
   AdminChildView,
@@ -81,6 +81,7 @@
     return html`
       <gr-list-view
         .createNew=${this.createNewCapability}
+        .createNewText=${'Create Group'}
         .filter=${this.filter}
         .items=${this.groups}
         .itemsPerPage=${this.groupsPerPage}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_screenshot_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_screenshot_test.ts
new file mode 100644
index 0000000..2b6be36
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_screenshot_test.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-admin-group-list';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrAdminGroupList} from './gr-admin-group-list';
+import {createGroupInfo} from '../../../test/test-data-generators';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+import {GroupInfo, GroupName} from '../../../types/common';
+
+suite('gr-admin-group-list screenshot tests', () => {
+  let element: GrAdminGroupList;
+
+  setup(async () => {
+    const groups: GroupInfo[] = [
+      {
+        ...createGroupInfo('group-1'),
+        name: 'Admins' as GroupName,
+        description: 'Gerrit Site Administrators',
+        options: {visible_to_all: true},
+      },
+      {
+        ...createGroupInfo('group-2'),
+        name: 'Project Owners' as GroupName,
+        description: 'Owners of a particular project',
+        options: {visible_to_all: true},
+      },
+      {
+        ...createGroupInfo('group-3'),
+        name: 'Service Users' as GroupName,
+        description: 'Users that perform automated tasks',
+        options: {visible_to_all: false},
+      },
+    ];
+
+    element = await fixture<GrAdminGroupList>(
+      html`<gr-admin-group-list></gr-admin-group-list>`
+    );
+    element.groups = groups;
+    element.createNewCapability = true;
+    element.loading = false;
+    await element.updateComplete;
+  });
+
+  test('screenshot', async () => {
+    await visualDiff(element, 'gr-admin-group-list');
+    await visualDiffDarkTheme(element, 'gr-admin-group-list');
+  });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
index 750beab..ba9204e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
@@ -16,7 +16,7 @@
 import {GerritView} from '../../../services/router/router-model';
 import {GrListView} from '../../shared/gr-list-view/gr-list-view';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {AdminChildView, AdminViewState} from '../../../models/views/admin';
 
 function createGroup(name: string, counter: number) {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 58827fb..9610b9d 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -16,6 +16,8 @@
 import '../gr-repo-commands/gr-repo-commands';
 import '../gr-repo-dashboards/gr-repo-dashboards';
 import '../gr-repo-detail-list/gr-repo-detail-list';
+import '../gr-repo-submit-requirements/gr-repo-submit-requirements';
+import '../gr-repo-labels/gr-repo-labels';
 import '../gr-repo-list/gr-repo-list';
 import '../gr-server-info/gr-server-info';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -34,15 +36,15 @@
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
 import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {ValueChangedEvent} from '../../../types/events';
 import {
   AdminChildView,
+  AdminNavLinksOption,
   adminViewModelToken,
   AdminViewState,
-  AdminNavLinksOption,
   getAdminLinks,
   NavLink,
   SubsectionInterface,
@@ -86,6 +88,8 @@
   @state()
   repoViewState?: RepoViewState;
 
+  @state() canDeleteCapability = false;
+
   @state() private breadcrumbParentName?: string;
 
   // private but used in test
@@ -214,6 +218,7 @@
       ${this.renderGroupMembers()} ${this.renderGroupAuditLog()}
       ${this.renderRepoDetailList()} ${this.renderRepoCommands()}
       ${this.renderRepoAccess()} ${this.renderRepoDashboards()}
+      ${this.renderRepoSubmitRequirements()} ${this.renderRepoLabels()}
       ${this.renderServerInfo()}
     `;
   }
@@ -358,6 +363,7 @@
       <div class="main breadcrumbs">
         <gr-group
           .groupId=${this.groupViewState?.groupId}
+          .canDelete=${this.canDeleteCapability}
           @name-changed=${(e: CustomEvent<GroupNameChangedDetail>) => {
             this.updateGroupName(e);
           }}
@@ -444,11 +450,41 @@
       <div class="main table breadcrumbs">
         <gr-repo-dashboards
           .repo=${this.repoViewState.repo}
+          .params=${this.repoViewState}
         ></gr-repo-dashboards>
       </div>
     `;
   }
 
+  private renderRepoSubmitRequirements() {
+    if (this.view !== GerritView.REPO) return nothing;
+    if (this.repoViewState?.detail !== RepoDetailView.SUBMIT_REQUIREMENTS)
+      return nothing;
+
+    return html`
+      <div class="main table breadcrumbs">
+        <gr-repo-submit-requirements
+          .repo=${this.repoViewState.repo}
+          .params=${this.repoViewState}
+        ></gr-repo-submit-requirements>
+      </div>
+    `;
+  }
+
+  private renderRepoLabels() {
+    if (this.view !== GerritView.REPO) return nothing;
+    if (this.repoViewState?.detail !== RepoDetailView.LABELS) return nothing;
+
+    return html`
+      <div class="main table breadcrumbs">
+        <gr-repo-labels
+          .repo=${this.repoViewState.repo}
+          .params=${this.repoViewState}
+        ></gr-repo-labels>
+      </div>
+    `;
+  }
+
   private renderServerInfo() {
     if (this.view !== GerritView.ADMIN) return nothing;
     if (this.adminViewState?.adminView !== AdminChildView.SERVER_INFO)
@@ -482,6 +518,12 @@
         ];
       const result = await Promise.all(promises);
       const account = result[0];
+      if (account) {
+        const capabilities = await this.restApiService.getAccountCapabilities([
+          'deleteGroup',
+        ]);
+        this.canDeleteCapability = !!capabilities?.deleteGroup;
+      }
       let options: AdminNavLinksOption | undefined = undefined;
       if (this.repoName) {
         options = {repoName: this.repoName};
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index 34e8d24..2f9d901 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -14,7 +14,7 @@
 import {GroupId, GroupName, RepoName, Timestamp} from '../../../types/common';
 import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 import {GrGroup} from '../gr-group/gr-group';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {AdminChildView} from '../../../models/views/admin';
 import {GroupDetailView} from '../../../models/views/group';
 import {RepoDetailView} from '../../../models/views/repo';
@@ -170,7 +170,7 @@
     );
     assert.equal(
       queryAndAssert<GrDropdownList>(element, '#pageSelect').items!.length,
-      7
+      9
     );
   });
 
@@ -370,6 +370,18 @@
               detailType: RepoDetailView.DASHBOARDS,
               url: '/admin/repos/my-repo,dashboards',
             },
+            {
+              name: 'Submit Requirements',
+              view: GerritView.REPO,
+              detailType: RepoDetailView.SUBMIT_REQUIREMENTS,
+              url: '/admin/repos/my-repo,submit-requirements',
+            },
+            {
+              name: 'Labels',
+              view: GerritView.REPO,
+              detailType: RepoDetailView.LABELS,
+              url: '/admin/repos/my-repo,labels',
+            },
           ],
         },
       },
@@ -450,6 +462,22 @@
         detailType: RepoDetailView.DASHBOARDS,
         parent: 'my-repo' as RepoName,
       },
+      {
+        text: 'Submit Requirements',
+        value: 'reposubmit-requirements',
+        view: GerritView.REPO,
+        url: '/admin/repos/my-repo,submit-requirements',
+        detailType: RepoDetailView.SUBMIT_REQUIREMENTS,
+        parent: 'my-repo' as RepoName,
+      },
+      {
+        text: 'Labels',
+        value: 'repolabels',
+        view: GerritView.REPO,
+        url: '/admin/repos/my-repo,labels',
+        detailType: RepoDetailView.LABELS,
+        parent: 'my-repo' as RepoName,
+      },
     ];
     const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
     const selectedIsCurrentPageSpy = sinon.spy(
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.ts
index a30b0df..58d40d3 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.ts
@@ -9,7 +9,7 @@
 import {GrConfirmDeleteItemDialog} from './gr-confirm-delete-item-dialog';
 import {queryAndAssert} from '../../../test/test-utils';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-confirm-delete-item-dialog tests', () => {
   let element: GrConfirmDeleteItemDialog;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index beec0b5..59e0b40 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -3,28 +3,26 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../../styles/gr-form-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-select/gr-select';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
-  RepoName,
   BranchName,
   ChangeId,
-  InheritedBooleanInfo,
+  ConfigInfo,
+  RepoName,
 } from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
-import {fire} from '../../../utils/event-util';
+import {ValueChangedEvent} from '../../../types/events';
+import {fire, firePageError} from '../../../utils/event-util';
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
 import {resolve} from '../../../models/dependency';
@@ -32,6 +30,12 @@
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {formStyles} from '../../../styles/form-styles';
 import {branchName} from '../../../utils/patch-set-util';
+import {GrAutogrowTextarea} from '../../../api/embed';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {ErrorCallback} from '../../../api/rest';
 
 const SUGGESTIONS_LIMIT = 15;
 
@@ -47,7 +51,7 @@
 @customElement('gr-create-change-dialog')
 export class GrCreateChangeDialog extends LitElement {
   // private but used in test
-  @query('#privateChangeCheckBox') privateChangeCheckBox!: HTMLInputElement;
+  @query('#privateChangeCheckBox') privateChangeCheckBox!: MdCheckbox;
 
   @property({type: String})
   repoName?: RepoName;
@@ -66,11 +70,11 @@
 
   @state() private baseCommit?: string;
 
-  @property({type: Object})
-  privateByDefault?: InheritedBooleanInfo;
-
   @state() private privateChangesEnabled = false;
 
+  // Private but used in test
+  @state() repoConfig?: ConfigInfo;
+
   private readonly query: (input: string) => Promise<{name: BranchName}[]>;
 
   private readonly restApiService = getAppContext().restApiService;
@@ -95,13 +99,15 @@
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       formStyles,
       sharedStyles,
       css`
         input:not([type='checkbox']),
+        md-outlined-text-field,
         gr-autocomplete,
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           width: 100%;
         }
         .value {
@@ -113,6 +119,9 @@
         #messageInput {
           min-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
         }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
         @media only screen and (max-width: 40em) {
           .value {
             width: 29em;
@@ -130,9 +139,10 @@
           <span class="value">
             <gr-autocomplete
               id="branchInput"
-              .text=${this.branch}
+              .text=${this.branch ?? ''}
               .query=${this.query}
               placeholder="Destination branch"
+              .showBlueFocusBorder=${true}
               @text-changed=${(e: ValueChangedEvent<BranchName>) => {
                 this.branch = e.detail.value;
               }}
@@ -143,53 +153,54 @@
         <section class=${this.baseChange ? 'hide' : ''}>
           <span class="title">Provide base commit sha1 for change</span>
           <span class="value">
-            <iron-input
-              .bindValue=${this.baseCommit}
-              @bind-value-changed=${(e: BindValueChangeEvent) => {
-                this.baseCommit = e.detail.value;
+            <md-outlined-text-field
+              id="baseCommitInput"
+              class="showBlueFocusBorder"
+              maxlength="40"
+              placeholder="(optional)"
+              .value=${this.baseCommit ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                this.baseCommit = target.value;
               }}
             >
-              <input
-                id="baseCommitInput"
-                maxlength="40"
-                placeholder="(optional)"
-              />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
           <span class="title">Enter topic for new change</span>
           <span class="value">
-            <iron-input
-              .bindValue=${this.topic}
-              @bind-value-changed=${(e: BindValueChangeEvent) => {
-                this.topic = e.detail.value;
+            <md-outlined-text-field
+              id="tagNameInput"
+              class="showBlueFocusBorder"
+              maxlength="1024"
+              placeholder="(optional)"
+              .value=${this.topic ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                this.topic = target.value;
               }}
             >
-              <input
-                id="tagNameInput"
-                maxlength="1024"
-                placeholder="(optional)"
-              />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section id="description">
           <span class="title">Description</span>
           <span class="value">
-            <iron-autogrow-textarea
+            <gr-autogrow-textarea
               id="messageInput"
               class="message"
               autocomplete="on"
-              rows="4"
-              maxRows="15"
-              .bindValue=${this.subject}
+              .rows=${4}
+              .maxRows=${15}
+              .value=${this.subject}
               placeholder="Insert the description of the change."
-              @bind-value-changed=${(e: BindValueChangeEvent) => {
-                this.subject = e.detail.value ?? '';
+              @input=${(e: InputEvent) => {
+                const value = (e.target as GrAutogrowTextarea).value ?? '';
+                this.subject = value;
               }}
             >
-            </iron-autogrow-textarea>
+            </gr-autogrow-textarea>
           </span>
         </section>
         <section class=${this.privateChangesEnabled ? 'hide' : ''}>
@@ -197,11 +208,10 @@
             >Private change</label
           >
           <span class="value">
-            <input
-              type="checkbox"
+            <md-checkbox
               id="privateChangeCheckBox"
               ?checked=${this.formatPrivateByDefaultBoolean()}
-            />
+            ></md-checkbox>
           </span>
         </section>
       </div>
@@ -209,11 +219,27 @@
   }
 
   override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('repoName')) {
+      this.loadRepo();
+    }
     if (changedProperties.has('branch') || changedProperties.has('subject')) {
       this.allowCreate();
     }
   }
 
+  private loadRepo() {
+    if (!this.repoName) return;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    this.restApiService.getProjectConfig(this.repoName, errFn).then(config => {
+      if (!config) return;
+      this.repoConfig = config;
+    });
+  }
+
   private allowCreate() {
     fire(this, 'can-create-change', {});
   }
@@ -266,7 +292,7 @@
 
   // private but used in test
   formatPrivateByDefaultBoolean() {
-    const config = this.privateByDefault;
+    const config = this.repoConfig?.private_by_default;
     if (config === undefined) return false;
     switch (config.configured_value) {
       case InheritedBooleanInfoConfiguredValue.TRUE:
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index d26df6e..aa807f2 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -9,10 +9,10 @@
 import {GrCreateChangeDialog} from './gr-create-change-dialog';
 import {BranchName, GitRef, RepoName} from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {createChange} from '../../../test/test-data-generators';
+import {createChange, createConfig} from '../../../test/test-data-generators';
 import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 
 suite('gr-create-change-dialog tests', () => {
   let element: GrCreateChangeDialog;
@@ -55,40 +55,43 @@
           <section>
             <span class="title"> Provide base commit sha1 for change </span>
             <span class="value">
-              <iron-input>
-                <input
-                  id="baseCommitInput"
-                  maxlength="40"
-                  placeholder="(optional)"
-                />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="baseCommitInput"
+                inputmode=""
+                maxlength="40"
+                placeholder="(optional)"
+                type="text"
+              >
+              </md-outlined-text-field>
             </span>
           </section>
           <section>
             <span class="title"> Enter topic for new change </span>
             <span class="value">
-              <iron-input>
-                <input
-                  id="tagNameInput"
-                  maxlength="1024"
-                  placeholder="(optional)"
-                />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="tagNameInput"
+                inputmode=""
+                maxlength="1024"
+                placeholder="(optional)"
+                type="text"
+              >
+              </md-outlined-text-field>
             </span>
           </section>
           <section id="description">
             <span class="title"> Description </span>
             <span class="value">
-              <iron-autogrow-textarea
-                aria-disabled="false"
+              <gr-autogrow-textarea
                 autocomplete="on"
                 class="message"
                 id="messageInput"
-                maxrows="15"
                 placeholder="Insert the description of the change."
-                rows="4"
               >
-              </iron-autogrow-textarea>
+              </gr-autogrow-textarea>
             </span>
           </section>
           <section>
@@ -96,7 +99,7 @@
               Private change
             </label>
             <span class="value">
-              <input id="privateChangeCheckBox" type="checkbox" />
+              <md-checkbox id="privateChangeCheckBox"> </md-checkbox>
             </span>
           </section>
         </div>
@@ -122,11 +125,14 @@
     element.subject = 'first change created with polygerrit ui';
     assert.isFalse(element.privateChangeCheckBox.checked);
 
-    const messageInput = queryAndAssert<IronAutogrowTextareaElement>(
+    const messageInput = queryAndAssert<GrAutogrowTextarea>(
       element,
       '#messageInput'
     );
-    messageInput.bindValue = configInputObj.subject;
+    messageInput.value = configInputObj.subject;
+    messageInput.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
 
     await element.handleCreateChange();
     // Private change
@@ -137,10 +143,13 @@
   });
 
   test('new change created with private', async () => {
-    element.privateByDefault = {
-      configured_value: InheritedBooleanInfoConfiguredValue.TRUE,
-      inherited_value: false,
-      value: true,
+    element.repoConfig = {
+      ...createConfig(),
+      private_by_default: {
+        configured_value: InheritedBooleanInfoConfiguredValue.TRUE,
+        inherited_value: false,
+        value: true,
+      },
     };
     sinon.stub(element, 'formatPrivateByDefaultBoolean').callsFake(() => true);
     await element.updateComplete;
@@ -162,11 +171,14 @@
     element.subject = 'first change created with polygerrit ui';
     assert.isTrue(element.privateChangeCheckBox.checked);
 
-    const messageInput = queryAndAssert<IronAutogrowTextareaElement>(
+    const messageInput = queryAndAssert<GrAutogrowTextarea>(
       element,
       '#messageInput'
     );
-    messageInput.bindValue = configInputObj.subject;
+    messageInput.value = configInputObj.subject;
+    messageInput.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
 
     await element.handleCreateChange();
     // Private change
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
index 0cfbaa4..615b69f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
@@ -5,13 +5,13 @@
  */
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
-  RepoName,
   BranchName,
   ChangeInfo,
   PatchSetNumber,
+  RepoName,
 } from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
-import {LitElement, html, nothing} from 'lit';
+import {html, LitElement, nothing} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {resolve} from '../../../models/dependency';
 import {createEditUrl} from '../../../models/views/change';
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog_test.ts
index 885e1cf..1a8f049 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog_test.ts
@@ -7,7 +7,7 @@
 import '../../../test/common-test-setup';
 import './gr-create-file-edit-dialog';
 import {createChange} from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrCreateFileEditDialog} from './gr-create-file-edit-dialog';
 import {stubRestApi, waitUntilCalled} from '../../../test/test-utils';
 import {BranchName, RepoName} from '../../../api/rest-api';
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
index 372e1e0..3f5ecdc 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
@@ -3,20 +3,22 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../../styles/gr-form-styles';
 import '../../../styles/shared-styles';
 import {GroupName} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
-import {customElement, query, property} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {createGroupUrl} from '../../../models/views/group';
 import {resolve} from '../../../models/dependency';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import '@material/web/textfield/outlined-text-field';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -29,24 +31,40 @@
 
 @customElement('gr-create-group-dialog')
 export class GrCreateGroupDialog extends LitElement {
-  @query('input') private input!: HTMLInputElement;
+  @query('md-outlined-text-field') private input!: MdOutlinedTextField;
 
   @property({type: String})
   name: GroupName | '' = '';
 
+  @state()
+  visibleToAll = false;
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly getNavigation = resolve(this, navigationToken);
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       sharedStyles,
       css`
         :host {
           display: inline-block;
         }
-        input {
+        div.title-flex,
+        div.value-flex {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+        }
+        div.gr-form-styles section {
+          margin: var(--spacing-m) 0;
+        }
+        div.gr-form-styles span.title {
+          width: 13em;
+        }
+        md-outlined-text-field {
           width: 20em;
         }
       `,
@@ -58,13 +76,39 @@
       <div class="gr-form-styles">
         <div id="form">
           <section>
-            <span class="title">Group name</span>
-            <iron-input
-              .bindValue=${this.name}
-              @bind-value-changed=${this.handleGroupNameBindValueChanged}
-            >
-              <input />
-            </iron-input>
+            <div class="title-flex">
+              <span class="title">Group name</span>
+            </div>
+            <div class="value-flex">
+              <span class="value">
+                <md-outlined-text-field
+                  class="showBlueFocusBorder"
+                  .value=${this.name ?? ''}
+                  @input=${(e: InputEvent) => {
+                    const target = e.target as MdOutlinedTextField;
+                    this.name = target.value as GroupName;
+                  }}
+                >
+                </md-outlined-text-field>
+              </span>
+            </div>
+          </section>
+          <section>
+            <div class="title-flex">
+              <span class="title">
+                Make group visible to all registered users
+              </span>
+            </div>
+            <div class="value-flex">
+              <span class="value">
+                <md-checkbox
+                  ?checked=${this.visibleToAll}
+                  @change=${() => {
+                    this.visibleToAll = !this.visibleToAll;
+                  }}
+                ></md-checkbox>
+              </span>
+            </div>
           </section>
         </div>
       </div>
@@ -87,16 +131,15 @@
 
   handleCreateGroup() {
     const name = this.name as GroupName;
-    return this.restApiService.createGroup({name}).then(groupRegistered => {
-      if (groupRegistered.status !== 201) return;
-      return this.restApiService.getGroupConfig(name).then(group => {
-        if (!group) return;
-        this.getNavigation().setUrl(createGroupUrl({groupId: group.id}));
+    const visible_to_all = this.visibleToAll;
+    return this.restApiService
+      .createGroup({name, visible_to_all})
+      .then(groupRegistered => {
+        if (groupRegistered.status !== 201) return;
+        return this.restApiService.getGroupConfig(name).then(group => {
+          if (!group) return;
+          this.getNavigation().setUrl(createGroupUrl({groupId: group.id}));
+        });
       });
-    });
-  }
-
-  private handleGroupNameBindValueChanged(e: BindValueChangeEvent) {
-    this.name = e.detail.value as GroupName;
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
index 407d015..f908455 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
@@ -12,11 +12,11 @@
   queryAndAssert,
   stubRestApi,
 } from '../../../test/test-utils';
-import {IronInputElement} from '@polymer/iron-input';
 import {GroupId} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-create-group-dialog tests', () => {
   let element: GrCreateGroupDialog;
@@ -36,10 +36,32 @@
         <div class="gr-form-styles">
           <div id="form">
             <section>
-              <span class="title"> Group name </span>
-              <iron-input>
-                <input />
-              </iron-input>
+              <div class="title-flex">
+                <span class="title"> Group name </span>
+              </div>
+              <div class="value-flex">
+                <span class="value">
+                  <md-outlined-text-field
+                    autocomplete=""
+                    class="showBlueFocusBorder"
+                    inputmode=""
+                    type="text"
+                  >
+                  </md-outlined-text-field>
+                </span>
+              </div>
+            </section>
+            <section>
+              <div class="title-flex">
+                <span class="title">
+                  Make group visible to all registered users
+                </span>
+              </div>
+              <div class="value-flex">
+                <span class="value">
+                  <md-checkbox> </md-checkbox>
+                </span>
+              </div>
             </section>
           </div>
         </div>
@@ -53,8 +75,11 @@
       promise.resolve();
     });
 
-    const inputEl = queryAndAssert<IronInputElement>(element, 'iron-input');
-    inputEl.bindValue = GROUP_NAME;
+    const inputEl = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field'
+    );
+    inputEl.value = GROUP_NAME;
     inputEl.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
 
     await promise;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
index 27bb836..5572fad 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
@@ -3,18 +3,21 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-select/gr-select';
 import {BranchName, RepoName} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
-import {fireAlert, fire, fireReload} from '../../../utils/event-util';
+import {fire, fireAlert, fireReload} from '../../../utils/event-util';
 import {RepoDetailView} from '../../../models/views/repo';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
+import {convertToString} from '../../../utils/string-util';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -45,10 +48,13 @@
   /* private but used in test */
   @state() itemAnnotation?: string;
 
+  @state() createEmptyCommit?: boolean;
+
   private readonly restApiService = getAppContext().restApiService;
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       sharedStyles,
       css`
@@ -58,11 +64,8 @@
         input {
           width: 20em;
         }
-        /* Add css selector with #id to increase priority
-          (otherwise ".gr-form-styles section" rule wins) */
-        .hideItem,
-        #itemAnnotationSection.hideItem {
-          display: none;
+        md-outlined-text-field {
+          width: 20em;
         }
       `,
     ];
@@ -74,35 +77,72 @@
         <div id="form">
           <section id="itemNameSection">
             <span class="title">${this.detailType} name</span>
-            <iron-input
-              .bindValue=${this.itemName}
-              @bind-value-changed=${this.handleItemNameBindValueChanged}
+            <md-outlined-text-field
+              class="showBlueFocusBorder"
+              placeholder="${this.detailType} Name"
+              .value=${this.itemName ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                this.itemName = target.value as BranchName;
+              }}
             >
-              <input placeholder="${this.detailType} Name" />
-            </iron-input>
+            </md-outlined-text-field>
           </section>
-          <section id="itemRevisionSection">
+          <section
+            id="createEmptyCommitSection"
+            ?hidden=${this.itemDetail === RepoDetailView.TAGS}
+          >
+            <div>
+              <span class="title">Point to</span>
+            </div>
+            <div>
+              <span class="value">
+                <md-outlined-select
+                  id="initialCommit"
+                  value=${convertToString(this.createEmptyCommit ?? false)}
+                  @change=${(e: Event) => {
+                    const select = e.target as HTMLSelectElement;
+                    this.createEmptyCommit = select.value === 'true';
+                  }}
+                >
+                  <md-select-option value="false">
+                    <div slot="headline">Existing Revision</div>
+                  </md-select-option>
+                  <md-select-option value="true">
+                    <div slot="headline">Initial empty commit</div>
+                  </md-select-option>
+                </md-outlined-select>
+              </span>
+            </div>
+          </section>
+          <section id="itemRevisionSection" ?hidden=${!!this.createEmptyCommit}>
             <span class="title">Initial Revision</span>
-            <iron-input
-              .bindValue=${this.itemRevision}
-              @bind-value-changed=${this.handleItemRevisionBindValueChanged}
+            <md-outlined-text-field
+              class="showBlueFocusBorder"
+              placeholder="Revision (Branch or SHA-1)"
+              .value=${this.itemRevision ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                this.itemRevision = target.value;
+              }}
             >
-              <input placeholder="Revision (Branch or SHA-1)" />
-            </iron-input>
+            </md-outlined-text-field>
           </section>
           <section
             id="itemAnnotationSection"
-            class=${this.itemDetail === RepoDetailView.BRANCHES
-              ? 'hideItem'
-              : ''}
+            ?hidden=${this.itemDetail === RepoDetailView.BRANCHES}
           >
             <span class="title">Annotation</span>
-            <iron-input
-              .bindValue=${this.itemAnnotation}
-              @bind-value-changed=${this.handleItemAnnotationBindValueChanged}
+            <md-outlined-text-field
+              class="showBlueFocusBorder"
+              placeholder="Annotation (Optional)"
+              .value=${this.itemAnnotation ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                this.itemAnnotation = target.value;
+              }}
             >
-              <input placeholder="Annotation (Optional)" />
-            </iron-input>
+            </md-outlined-text-field>
           </section>
         </div>
       </div>
@@ -126,10 +166,14 @@
     if (!this.itemName) {
       throw new Error('itemName name is not set');
     }
-    const USE_HEAD = this.itemRevision ? this.itemRevision : 'HEAD';
+    const useHead = this.itemRevision ? this.itemRevision : 'HEAD';
+    const createEmptyCommit = !!this.createEmptyCommit;
     if (this.itemDetail === RepoDetailView.BRANCHES) {
+      const createBranchInput = createEmptyCommit
+        ? {create_empty_commit: true}
+        : {revision: useHead};
       return this.restApiService
-        .createRepoBranch(this.repoName, this.itemName, {revision: USE_HEAD})
+        .createRepoBranch(this.repoName, this.itemName, createBranchInput)
         .then(itemRegistered => {
           if (itemRegistered.status === 201) {
             fireAlert(this, 'Branch created successfully. Reloading...');
@@ -139,7 +183,7 @@
     } else if (this.itemDetail === RepoDetailView.TAGS) {
       return this.restApiService
         .createRepoTag(this.repoName, this.itemName, {
-          revision: USE_HEAD,
+          revision: useHead,
           message: this.itemAnnotation || undefined,
         })
         .then(itemRegistered => {
@@ -151,16 +195,4 @@
     }
     throw new Error(`Invalid itemDetail: ${this.itemDetail}`);
   }
-
-  private handleItemNameBindValueChanged(e: BindValueChangeEvent) {
-    this.itemName = e.detail.value as BranchName;
-  }
-
-  private handleItemRevisionBindValueChanged(e: BindValueChangeEvent) {
-    this.itemRevision = e.detail.value;
-  }
-
-  private handleItemAnnotationBindValueChanged(e: BindValueChangeEvent) {
-    this.itemAnnotation = e.detail.value;
-  }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
index 9e455d1..5da0369 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
@@ -12,15 +12,15 @@
   stubRestApi,
 } from '../../../test/test-utils';
 import {BranchName} from '../../../types/common';
-import {IronInputElement} from '@polymer/iron-input';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {RepoDetailView} from '../../../models/views/repo';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-create-pointer-dialog tests', () => {
   let element: GrCreatePointerDialog;
 
-  const ironInput = (element: Element) =>
-    queryAndAssert<IronInputElement>(element, 'iron-input');
+  const queryInput = (element: Element) =>
+    queryAndAssert<MdOutlinedTextField>(element, 'md-outlined-text-field');
 
   setup(async () => {
     element = await fixture(
@@ -36,21 +36,62 @@
           <div id="form">
             <section id="itemNameSection">
               <span class="title"> name </span>
-              <iron-input>
-                <input placeholder=" Name" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                inputmode=""
+                placeholder=" Name"
+                type="text"
+              >
+              </md-outlined-text-field>
+            </section>
+            <section id="createEmptyCommitSection">
+              <div>
+                <span class="title"> Point to </span>
+              </div>
+              <div>
+                <span class="value">
+                  <md-outlined-select id="initialCommit" value="false">
+                    <md-select-option
+                      data-aria-selected="true"
+                      md-menu-item=""
+                      tabindex="0"
+                      value="false"
+                    >
+                      <div slot="headline">Existing Revision</div>
+                    </md-select-option>
+                    <md-select-option
+                      md-menu-item=""
+                      tabindex="-1"
+                      value="true"
+                    >
+                      <div slot="headline">Initial empty commit</div>
+                    </md-select-option>
+                  </md-outlined-select>
+                </span>
+              </div>
             </section>
             <section id="itemRevisionSection">
               <span class="title"> Initial Revision </span>
-              <iron-input>
-                <input placeholder="Revision (Branch or SHA-1)" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                inputmode=""
+                placeholder="Revision (Branch or SHA-1)"
+                type="text"
+              >
+              </md-outlined-text-field>
             </section>
             <section id="itemAnnotationSection">
               <span class="title"> Annotation </span>
-              <iron-input>
-                <input placeholder="Annotation (Optional)" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                inputmode=""
+                placeholder="Annotation (Optional)"
+                type="text"
+              >
+              </md-outlined-text-field>
             </section>
           </div>
         </div>
@@ -69,10 +110,23 @@
     element.itemName = 'test-branch' as BranchName;
     element.itemDetail = 'branches' as RepoDetailView.BRANCHES;
 
-    ironInput(queryAndAssert(element, '#itemNameSection')).bindValue =
-      'test-branch2';
-    ironInput(queryAndAssert(element, '#itemRevisionSection')).bindValue =
-      'HEAD';
+    await element.updateComplete;
+
+    const itemNameSection = queryInput(
+      queryAndAssert(element, '#itemNameSection')
+    );
+    itemNameSection.value = 'test-branch2';
+    itemNameSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
+
+    const itemRevisionSection = queryInput(
+      queryAndAssert(element, '#itemRevisionSection')
+    );
+    itemRevisionSection.value = 'HEAD';
+    itemRevisionSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
 
     await promise;
 
@@ -91,10 +145,21 @@
     element.itemName = 'test-tag' as BranchName;
     element.itemDetail = 'tags' as RepoDetailView.TAGS;
 
-    ironInput(queryAndAssert(element, '#itemNameSection')).bindValue =
-      'test-tag2';
-    ironInput(queryAndAssert(element, '#itemRevisionSection')).bindValue =
-      'HEAD';
+    const itemNameSection = queryInput(
+      queryAndAssert(element, '#itemNameSection')
+    );
+    itemNameSection.value = 'test-tag2';
+    itemNameSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
+
+    const itemRevisionSection = queryInput(
+      queryAndAssert(element, '#itemRevisionSection')
+    );
+    itemRevisionSection.value = 'HEAD';
+    itemRevisionSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
 
     await promise;
 
@@ -114,12 +179,29 @@
     element.itemAnnotation = 'test-message';
     element.itemDetail = 'tags' as RepoDetailView.TAGS;
 
-    ironInput(queryAndAssert(element, '#itemNameSection')).bindValue =
-      'test-tag2';
-    ironInput(queryAndAssert(element, '#itemAnnotationSection')).bindValue =
-      'test-message2';
-    ironInput(queryAndAssert(element, '#itemRevisionSection')).bindValue =
-      'HEAD';
+    const itemNameSection = queryInput(
+      queryAndAssert(element, '#itemNameSection')
+    );
+    itemNameSection.value = 'test-tag2';
+    itemNameSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
+
+    const itemAnnotationSection = queryInput(
+      queryAndAssert(element, '#itemAnnotationSection')
+    );
+    itemAnnotationSection.value = 'test-message2';
+    itemAnnotationSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
+
+    const itemRevisionSection = queryInput(
+      queryAndAssert(element, '#itemRevisionSection')
+    );
+    itemRevisionSection.value = 'HEAD';
+    itemRevisionSection.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
 
     await promise;
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index 077ac94..e8d1c70 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -3,10 +3,8 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-select/gr-select';
 import {
   BranchName,
   GroupId,
@@ -18,8 +16,8 @@
 import {convertToString} from '../../../utils/string-util';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
-import {customElement, query, property, state} from 'lit/decorators.js';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {createRepoUrl} from '../../../models/views/repo';
@@ -29,6 +27,10 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
 import {branchName} from '../../../utils/patch-set-util';
+import '@material/web/textfield/outlined-text-field';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -42,8 +44,8 @@
 
 @customElement('gr-create-repo-dialog')
 export class GrCreateRepoDialog extends LitElement {
-  @query('input')
-  input?: HTMLInputElement;
+  @query('#repoNameInput')
+  input?: MdOutlinedTextField;
 
   @property({type: Boolean})
   nameChanged = false;
@@ -97,6 +99,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       sharedStyles,
       css`
@@ -109,10 +112,6 @@
           flex-direction: column;
           justify-content: center;
         }
-        input {
-          width: 20em;
-          box-sizing: border-box;
-        }
         div.gr-form-styles section {
           margin: var(--spacing-m) 0;
         }
@@ -126,6 +125,9 @@
           display: block;
           width: 20em;
         }
+        md-outlined-text-field {
+          width: 20em;
+        }
       `,
     ];
   }
@@ -138,26 +140,62 @@
             <div class="title-flex">
               <span class="title">Repository Name</span>
             </div>
-            <iron-input
-              .bindValue=${convertToString(this.repoConfig.name)}
-              @bind-value-changed=${this.handleNameBindValueChanged}
-            >
-              <input id="repoNameInput" autocomplete="on" />
-            </iron-input>
+            <div class="value-flex">
+              <span class="value">
+                <md-outlined-text-field
+                  id="repoNameInput"
+                  class="showBlueFocusBorder"
+                  autocomplete="on"
+                  .value=${convertToString(this.repoConfig.name)}
+                  @input=${this.handleNameInput}
+                >
+                </md-outlined-text-field>
+              </span>
+            </div>
+          </section>
+          <section>
+            <div class="title-flex">
+              <span class="title">
+                <gr-tooltip-content
+                  has-tooltip
+                  title="Only serve as a parent repository for other repositories
+                  to inheright access rights and configs.
+                  If 'true', then you cannot push code to this repo.
+                  It will only have a 'refs/meta/config' branch."
+                >
+                  Parent Repo Only <gr-icon icon="info"></gr-icon>
+                </gr-tooltip-content>
+              </span>
+            </div>
+            <div class="value-flex">
+              <span class="value">
+                <md-checkbox
+                  id="parentRepo"
+                  ?checked=${this.repoConfig.permissions_only}
+                  @change=${this.handlePermissionsOnlyChangeChanged}
+                ></md-checkbox>
+              </span>
+            </div>
           </section>
           <section ?hidden=${!!this.repoConfig.permissions_only}>
             <div class="title-flex">
               <span class="title">Default Branch</span>
             </div>
-            <span class="value">
-              <gr-autocomplete
-                id="defaultBranchNameInput"
-                .text=${convertToString(this.selectedDefaultBranch)}
-                .placeholder=${`Optional, defaults to '${this.defaultBranch}'`}
-                @text-changed=${this.handleBranchNameBindValueChanged}
-              >
-              </gr-autocomplete>
-            </span>
+            <div class="value-flex">
+              <span class="value">
+                <md-outlined-text-field
+                  id="defaultBranchNameInput"
+                  class="showBlueFocusBorder"
+                  placeholder=${`Optional, defaults to '${this.defaultBranch}'`}
+                  .value=${convertToString(this.selectedDefaultBranch)}
+                  @input=${(e: InputEvent) => {
+                    const target = e.target as MdOutlinedTextField;
+                    this.selectedDefaultBranch = target.value as BranchName;
+                  }}
+                >
+                </md-outlined-text-field>
+              </span>
+            </div>
           </section>
           <section>
             <div class="title-flex">
@@ -170,16 +208,19 @@
                 </gr-tooltip-content>
               </span>
             </div>
-            <span class="value">
-              <gr-autocomplete
-                id="rightsInheritFromInput"
-                .text=${convertToString(this.repoConfig.parent)}
-                .query=${this.query}
-                .placeholder=${"Optional, defaults to 'All-Projects'"}
-                @text-changed=${this.handleRightsTextChanged}
-              >
-              </gr-autocomplete>
-            </span>
+            <div class="value-flex">
+              <span class="value">
+                <gr-autocomplete
+                  id="rightsInheritFromInput"
+                  .text=${convertToString(this.repoConfig.parent)}
+                  .query=${this.query}
+                  .placeholder=${"Optional, defaults to 'All-Projects'"}
+                  .showBlueFocusBorder=${true}
+                  @text-changed=${this.handleRightsTextChanged}
+                >
+                </gr-autocomplete>
+              </span>
+            </div>
           </section>
           <section>
             <div class="title-flex">
@@ -192,18 +233,21 @@
                 </gr-tooltip-content>
               </span>
             </div>
-            <span class="value">
-              <gr-autocomplete
-                id="ownerInput"
-                .text=${convertToString(this.repoOwner)}
-                .value=${convertToString(this.repoOwnerId)}
-                .query=${this.queryGroups}
-                .placeholder=${'Optional'}
-                @text-changed=${this.handleOwnerTextChanged}
-                @value-changed=${this.handleOwnerValueChanged}
-              >
-              </gr-autocomplete>
-            </span>
+            <div class="value-flex">
+              <span class="value">
+                <gr-autocomplete
+                  id="ownerInput"
+                  .text=${convertToString(this.repoOwner)}
+                  .value=${convertToString(this.repoOwnerId)}
+                  .query=${this.queryGroups}
+                  .placeholder=${'Optional'}
+                  .showBlueFocusBorder=${true}
+                  @text-changed=${this.handleOwnerTextChanged}
+                  @value-changed=${this.handleOwnerValueChanged}
+                >
+                </gr-autocomplete>
+              </span>
+            </div>
           </section>
           <section>
             <div class="title-flex">
@@ -218,47 +262,11 @@
             </div>
             <div class="value-flex">
               <span class="value">
-                <gr-select
+                <md-checkbox
                   id="initialCommit"
-                  .bindValue=${this.repoConfig.create_empty_commit}
-                  @bind-value-changed=${this
-                    .handleCreateEmptyCommitBindValueChanged}
-                >
-                  <select>
-                    <option value="false">False</option>
-                    <option value="true">True</option>
-                  </select>
-                </gr-select>
-              </span>
-            </div>
-          </section>
-          <section>
-            <div class="title-flex">
-              <span class="title">
-                <gr-tooltip-content
-                  has-tooltip
-                  title="Only serve as a parent repository for other repositories
-to inheright access rights and configs.
-If 'true', then you cannot push code to this repo.
-It will only have a 'refs/meta/config' branch."
-                >
-                  Parent Repo Only <gr-icon icon="info"></gr-icon>
-                </gr-tooltip-content>
-              </span>
-            </div>
-            <div class="value-flex">
-              <span class="value">
-                <gr-select
-                  id="parentRepo"
-                  .bindValue=${this.repoConfig.permissions_only}
-                  @bind-value-changed=${this
-                    .handlePermissionsOnlyBindValueChanged}
-                >
-                  <select>
-                    <option value="false">False</option>
-                    <option value="true">True</option>
-                  </select>
-                </gr-select>
+                  ?checked=${this.repoConfig.create_empty_commit}
+                  @change=${this.handleCreateEmptyCommitChangeChanged}
+                ></md-checkbox>
               </span>
             </div>
           </section>
@@ -329,33 +337,28 @@
     this.repoOwnerId = e.detail.value as GroupId;
   }
 
-  private handleNameBindValueChanged(e: ValueChangedEvent) {
-    this.repoConfig.name = e.detail.value as RepoName;
+  private handleNameInput(e: InputEvent) {
+    const target = e.target as MdOutlinedTextField;
+    this.repoConfig.name = target.value as RepoName;
     // nameChanged needs to be set before the event is fired,
     // because when the event is fired, gr-repo-list gets
     // the nameChanged value.
-    this.nameChanged = !!e.detail.value;
+    this.nameChanged = !!target.value;
     fire(this, 'new-repo-name', {});
     this.requestUpdate();
   }
 
-  private handleBranchNameBindValueChanged(e: ValueChangedEvent) {
-    this.selectedDefaultBranch = e.detail.value as BranchName;
-  }
-
-  private handleCreateEmptyCommitBindValueChanged(
-    e: ValueChangedEvent<string>
-  ) {
+  private handleCreateEmptyCommitChangeChanged() {
     this.repoConfig = {
       ...this.repoConfig,
-      create_empty_commit: e.detail.value === 'true',
+      create_empty_commit: !this.repoConfig.create_empty_commit,
     };
   }
 
-  private handlePermissionsOnlyBindValueChanged(e: ValueChangedEvent<string>) {
+  private handlePermissionsOnlyChangeChanged() {
     this.repoConfig = {
       ...this.repoConfig,
-      permissions_only: e.detail.value === 'true',
+      permissions_only: !this.repoConfig.permissions_only,
     };
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
index 289cd03..aeb0d32 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
@@ -13,8 +13,8 @@
 } from '../../../test/test-utils';
 import {BranchName, GroupId, RepoName} from '../../../types/common';
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {GrSelect} from '../../shared/gr-select/gr-select';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
 
 suite('gr-create-repo-dialog tests', () => {
   let element: GrCreateRepoDialog;
@@ -35,17 +35,57 @@
               <div class="title-flex">
                 <span class="title"> Repository Name </span>
               </div>
-              <iron-input>
-                <input autocomplete="on" id="repoNameInput" />
-              </iron-input>
+              <div class="value-flex">
+                <span class="value">
+                  <md-outlined-text-field
+                    autocomplete="on"
+                    class="showBlueFocusBorder"
+                    id="repoNameInput"
+                    inputmode=""
+                    type="text"
+                  >
+                  </md-outlined-text-field>
+                </span>
+              </div>
+            </section>
+            <section>
+              <div class="title-flex">
+                <span class="title">
+                  <gr-tooltip-content
+                    has-tooltip=""
+                    title="Only serve as a parent repository for other repositories
+                  to inheright access rights and configs.
+                  If 'true', then you cannot push code to this repo.
+                  It will only have a 'refs/meta/config' branch."
+                  >
+                    Parent Repo Only
+                    <gr-icon icon="info"> </gr-icon>
+                  </gr-tooltip-content>
+                </span>
+              </div>
+              <div class="value-flex">
+                <span class="value">
+                  <md-checkbox id="parentRepo"> </md-checkbox>
+                </span>
+              </div>
             </section>
             <section>
               <div class="title-flex">
                 <span class="title"> Default Branch </span>
               </div>
-              <span class="value">
-                <gr-autocomplete id="defaultBranchNameInput"> </gr-autocomplete>
-              </span>
+              <div class="value-flex">
+                <span class="value">
+                  <md-outlined-text-field
+                    autocomplete=""
+                    class="showBlueFocusBorder"
+                    id="defaultBranchNameInput"
+                    inputmode=""
+                    placeholder="Optional, defaults to 'master'"
+                    type="text"
+                  >
+                  </md-outlined-text-field>
+                </span>
+              </div>
             </section>
             <section>
               <div class="title-flex">
@@ -59,9 +99,12 @@
                   </gr-tooltip-content>
                 </span>
               </div>
-              <span class="value">
-                <gr-autocomplete id="rightsInheritFromInput"> </gr-autocomplete>
-              </span>
+              <div class="value-flex">
+                <span class="value">
+                  <gr-autocomplete id="rightsInheritFromInput">
+                  </gr-autocomplete>
+                </span>
+              </div>
             </section>
             <section>
               <div class="title-flex">
@@ -75,9 +118,11 @@
                   </gr-tooltip-content>
                 </span>
               </div>
-              <span class="value">
-                <gr-autocomplete id="ownerInput"> </gr-autocomplete>
-              </span>
+              <div class="value-flex">
+                <span class="value">
+                  <gr-autocomplete id="ownerInput"> </gr-autocomplete>
+                </span>
+              </div>
             </section>
             <section>
               <div class="title-flex">
@@ -93,38 +138,7 @@
               </div>
               <div class="value-flex">
                 <span class="value">
-                  <gr-select id="initialCommit">
-                    <select>
-                      <option value="false">False</option>
-                      <option value="true">True</option>
-                    </select>
-                  </gr-select>
-                </span>
-              </div>
-            </section>
-            <section>
-              <div class="title-flex">
-                <span class="title">
-                  <gr-tooltip-content
-                    has-tooltip=""
-                    title="Only serve as a parent repository for other repositories
-to inheright access rights and configs.
-If 'true', then you cannot push code to this repo.
-It will only have a 'refs/meta/config' branch."
-                  >
-                    Parent Repo Only
-                    <gr-icon icon="info"> </gr-icon>
-                  </gr-tooltip-content>
-                </span>
-              </div>
-              <div class="value-flex">
-                <span class="value">
-                  <gr-select id="parentRepo">
-                    <select>
-                      <option value="false">False</option>
-                      <option value="true">True</option>
-                    </select>
-                  </gr-select>
+                  <md-checkbox checked="" id="initialCommit"> </md-checkbox>
                 </span>
               </div>
             </section>
@@ -136,9 +150,9 @@
 
   test('default values are populated', () => {
     assert.isTrue(
-      queryAndAssert<GrSelect>(element, '#initialCommit').bindValue
+      queryAndAssert<MdCheckbox>(element, '#initialCommit').checked
     );
-    assert.isFalse(queryAndAssert<GrSelect>(element, '#parentRepo').bindValue);
+    assert.isFalse(queryAndAssert<MdCheckbox>(element, '#parentRepo').checked);
   });
 
   test('repo created', async () => {
@@ -179,9 +193,9 @@
     );
     queryAndAssert<GrAutocomplete>(element, '#rightsInheritFromInput').value =
       configInputObj.parent;
-    queryAndAssert<GrSelect>(element, '#initialCommit').bindValue =
+    queryAndAssert<MdCheckbox>(element, '#initialCommit').checked =
       configInputObj.create_empty_commit;
-    queryAndAssert<GrSelect>(element, '#parentRepo').bindValue =
+    queryAndAssert<MdCheckbox>(element, '#parentRepo').checked =
       configInputObj.permissions_only;
 
     assert.deepEqual(element.repoConfig, configInputObj);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
index f4a7bf3..37df2d8 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
@@ -5,11 +5,11 @@
  */
 import '../../shared/gr-account-label/gr-account-label';
 import {
-  GroupInfo,
   AccountInfo,
-  EncodedGroupId,
   GroupAuditEventInfo,
   GroupAuditGroupEventInfo,
+  GroupId,
+  GroupInfo,
   isGroupAuditGroupEventInfo,
 } from '../../../types/common';
 import {firePageError, fireTitleChange} from '../../../utils/event-util';
@@ -17,7 +17,7 @@
 import {ErrorCallback} from '../../../api/rest';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {createGroupUrl} from '../../../models/views/group';
 
@@ -30,7 +30,7 @@
 @customElement('gr-group-audit-log')
 export class GrGroupAuditLog extends LitElement {
   @property({type: String})
-  groupId?: EncodedGroupId;
+  groupId?: GroupId;
 
   @state() private auditLog?: GroupAuditEventInfo[];
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts
index 828a3c6..17d2376 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts
@@ -12,8 +12,8 @@
 } from '../../../test/test-utils';
 import {GrGroupAuditLog} from './gr-group-audit-log';
 import {
-  EncodedGroupId,
   GroupAuditEventType,
+  GroupId,
   GroupInfo,
   GroupName,
 } from '../../../types/common';
@@ -23,7 +23,7 @@
   createGroupInfo,
 } from '../../../test/test-data-generators';
 import {PageErrorEvent} from '../../../types/events';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-group-audit-log tests', () => {
   let element: GrGroupAuditLog;
@@ -111,11 +111,11 @@
 
   suite('404', () => {
     test('fires page-error', async () => {
-      element.groupId = '1' as EncodedGroupId;
+      element.groupId = '1' as GroupId;
       await element.updateComplete;
 
       const response = {...new Response(), status: 404};
-      stubRestApi('getGroupAuditLog').callsFake((_group, errFn) => {
+      stubRestApi('getGroupAuditLog').callsFake((_group: any, errFn: any) => {
         if (errFn) errFn(response);
         return Promise.resolve(undefined);
       });
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
index cb71624..e1762c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -9,18 +9,16 @@
 import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog';
 import {getBaseUrl} from '../../../utils/url-util';
 import {
-  GroupId,
   AccountId,
   AccountInfo,
+  GroupId,
   GroupInfo,
   GroupName,
-  ServerInfo,
   NumericChangeId,
+  ServerInfo,
 } from '../../../types/common';
-import {
-  AutocompleteQuery,
-  AutocompleteSuggestion,
-} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {
   fireAlert,
   firePageError,
@@ -35,7 +33,7 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {subscribe} from '../../lit/subscription-controller';
@@ -191,9 +189,10 @@
               <span class="value">
                 <gr-autocomplete
                   id="groupMemberSearchInput"
-                  .text=${this.groupMemberSearchName}
-                  .value=${this.groupMemberSearchId}
+                  .text=${this.groupMemberSearchName ?? ''}
+                  .value=${`${this.groupMemberSearchId ?? ''}`}
                   .query=${this.queryMembers}
+                  .showBlueFocusBorder=${true}
                   placeholder="Name Or Email"
                   @text-changed=${this.handleGroupMemberTextChanged}
                   @value-changed=${this.handleGroupMemberValueChanged}
@@ -227,9 +226,10 @@
               <span class="value">
                 <gr-autocomplete
                   id="includedGroupSearchInput"
-                  .text=${this.includedGroupSearchName}
-                  .value=${this.includedGroupSearchId}
+                  .text=${this.includedGroupSearchName ?? ''}
+                  .value=${this.includedGroupSearchId ?? ''}
                   .query=${this.queryIncludedGroup}
+                  .showBlueFocusBorder=${true}
                   placeholder="Group Name"
                   @text-changed=${this.handleIncludedGroupTextChanged}
                   @value-changed=${this.handleIncludedGroupValueChanged}
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
index 5dc4e42..f7cab0e 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
@@ -27,7 +27,7 @@
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {PageErrorEvent} from '../../../types/events';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {createServerInfo} from '../../../test/test-data-generators';
 
 suite('gr-group-members tests', () => {
@@ -94,7 +94,7 @@
       },
     ];
 
-    stubRestApi('queryAccounts').callsFake(input => {
+    stubRestApi('queryAccounts').callsFake((input: string) => {
       if (input.startsWith('test')) {
         return Promise.resolve([
           {
@@ -120,7 +120,7 @@
         return Promise.resolve([]);
       }
     });
-    stubRestApi('getSuggestedGroups').callsFake(input => {
+    stubRestApi('getSuggestedGroups').callsFake((input: string) => {
       if (input.startsWith('test')) {
         return Promise.resolve({
           'test-admin': {
@@ -447,14 +447,16 @@
     const alertStub = sinon.stub();
     element.addEventListener('show-alert', alertStub);
     const errorResponse = {...new Response(), status: 404, ok: false};
-    stubRestApi('saveIncludedGroup').callsFake((_, _non, errFn) => {
-      if (errFn !== undefined) {
-        errFn(errorResponse);
-      } else {
-        assert.fail('errFn is undefined');
+    stubRestApi('saveIncludedGroup').callsFake(
+      (_: any, _non: any, errFn: any) => {
+        if (errFn !== undefined) {
+          errFn(errorResponse);
+        } else {
+          assert.fail('errFn is undefined');
+        }
+        return Promise.resolve(undefined);
       }
-      return Promise.resolve(undefined);
-    });
+    );
 
     const groupMemberSearchInput = queryAndAssert<GrAutocomplete>(
       element,
@@ -590,7 +592,7 @@
     element.groupId = 'testId1' as GroupId;
 
     const response = {...new Response(), status: 404};
-    stubRestApi('getGroupConfig').callsFake((_, errFn) => {
+    stubRestApi('getGroupConfig').callsFake((_: any, errFn: any) => {
       if (errFn !== undefined) {
         errFn(response);
       }
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 6087ae71..970a6de 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -6,39 +6,36 @@
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import '../../shared/gr-select/gr-select';
-import '../../shared/gr-suggestion-textarea/gr-suggestion-textarea';
-import {
-  AutocompleteSuggestion,
-  AutocompleteQuery,
-} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {GroupId, GroupInfo, GroupName} from '../../../types/common';
 import {fire, firePageError, fireTitleChange} from '../../../utils/event-util';
+import {resolve} from '../../../models/dependency';
 import {getAppContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
-import {convertToString} from '../../../utils/string-util';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {ValueChangedEvent} from '../../../types/events';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {formStyles} from '../../../styles/form-styles';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {configModelToken} from '../../../models/config/config-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {when} from 'lit/directives/when.js';
+import {AdminChildView, createAdminUrl} from '../../../models/views/admin';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
-const OPTIONS = {
-  submitFalse: {
-    value: false,
-    label: 'False',
-  },
-  submitTrue: {
-    value: true,
-    label: 'True',
-  },
-};
-
 export interface GroupNameChangedDetail {
   name: GroupName;
   external: boolean;
@@ -57,17 +54,21 @@
 export class GrGroup extends LitElement {
   private readonly query: AutocompleteQuery;
 
+  @query('#deleteGroupModal')
+  deleteGroupModal?: HTMLDialogElement;
+
   @property({type: String})
   groupId?: GroupId;
 
+  @property({type: Boolean})
+  canDelete?: boolean;
+
   @state() private originalOwnerName?: string;
 
   @state() private originalDescriptionName?: string;
 
   @state() private originalOptionsVisibleToAll?: boolean;
 
-  @state() private submitTypes = Object.values(OPTIONS);
-
   // private but used in test
   @state() isAdmin = false;
 
@@ -89,10 +90,23 @@
   // private but used in test
   @state() originalName?: GroupName;
 
+  @state() enableDeleteGroup?: boolean;
+
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
+  private readonly getConfigModel = resolve(this, configModelToken);
+
   constructor() {
     super();
+    subscribe(
+      this,
+      () => this.getConfigModel().serverConfig$,
+      config => {
+        this.enableDeleteGroup = !!config?.groups.enable_delete_group;
+      }
+    );
     this.query = (input: string) => this.getGroupSuggestions(input);
   }
 
@@ -102,15 +116,25 @@
 
   static override get styles() {
     return [
+      modalStyles,
       fontStyles,
       grFormStyles,
+      formStyles,
       sharedStyles,
       subpageStyles,
+      materialStyles,
       css`
         h3.edited:after {
           color: var(--deemphasized-text-color);
           content: ' *';
         }
+        gr-autogrow-textarea {
+          background-color: var(--view-background-color);
+          width: 100%;
+        }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
       `,
     ];
   }
@@ -126,7 +150,7 @@
             <fieldset>
               ${this.renderGroupUUID()} ${this.renderGroupName()}
               ${this.renderGroupOwner()} ${this.renderGroupDescription()}
-              ${this.renderGroupOptions()}
+              ${this.renderGroupOptions()}${this.renderGroupDeleteButton()}
             </fieldset>
           </div>
         </div>
@@ -159,7 +183,8 @@
         <span class="value">
           <gr-autocomplete
             id="groupNameInput"
-            .text=${this.groupConfig?.name}
+            .text=${this.groupConfig?.name ?? ''}
+            .showBlueFocusBorder=${true}
             ?disabled=${this.computeGroupDisabled()}
             @text-changed=${this.handleNameTextChanged}
           ></gr-autocomplete>
@@ -191,9 +216,10 @@
         <span class="value">
           <gr-autocomplete
             id="groupOwnerInput"
-            .text=${this.groupConfig?.owner}
-            .value=${this.groupConfigOwner}
+            .text=${this.groupConfig?.owner ?? ''}
+            .value=${this.groupConfigOwner ?? ''}
             .query=${this.query}
+            .showBlueFocusBorder=${true}
             ?disabled=${this.computeGroupDisabled()}
             @text-changed=${this.handleOwnerTextChanged}
             @value-changed=${this.handleOwnerValueChanged}
@@ -222,15 +248,17 @@
       </h3>
       <fieldset>
         <div>
-          <gr-suggestion-textarea
+          <gr-autogrow-textarea
+            id="descriptionInput"
             class="description"
             autocomplete="on"
-            rows="4"
-            monospace
+            placeholder="&lt;Insert group description here&gt;"
+            .rows=${4}
+            .maxRows=${10}
+            .value=${this.groupConfig?.description ?? ''}
             ?disabled=${this.computeGroupDisabled()}
-            .text=${this.groupConfig?.description ?? ''}
-            @text-changed=${this.handleDescriptionTextChanged}
-          ></gr-suggestion-textarea>
+            @input=${this.handleDescriptionInput}
+          ></gr-autogrow-textarea>
         </div>
         <span class="value">
           <gr-button
@@ -267,21 +295,12 @@
             Make group visible to all registered users
           </span>
           <span class="value">
-            <gr-select
+            <md-checkbox
               id="visibleToAll"
-              .bindValue=${convertToString(
-                Boolean(this.groupConfig?.options?.visible_to_all)
-              )}
-              @bind-value-changed=${this.handleOptionsBindValueChanged}
-            >
-              <select ?disabled=${this.computeGroupDisabled()}>
-                ${this.submitTypes.map(
-                  item => html`
-                    <option value=${item.value}>${item.label}</option>
-                  `
-                )}
-              </select>
-            </gr-select>
+              ?checked=${Boolean(this.groupConfig?.options?.visible_to_all)}
+              ?disabled=${this.computeGroupDisabled()}
+              @change=${this.handleOptionsChange}
+            ></md-checkbox>
           </span>
         </section>
         <span class="value">
@@ -296,6 +315,46 @@
     `;
   }
 
+  private renderGroupDeleteButton() {
+    return when(
+      !this.computeGroupDisabled() && this.canDelete && this.enableDeleteGroup,
+      () => html`
+        <div id="deleteGroupContainer">
+          <h3
+            id="deleteGroup"
+            class="heading-3 ${this.computeHeaderClass(false)}"
+          >
+            Delete Group
+          </h3>
+          <fieldset>
+            <span class="value">
+              <gr-button
+                @click=${() => {
+                  this.deleteGroupModal?.showModal();
+                }}
+              >
+                Delete Group
+              </gr-button>
+            </span>
+          </fieldset>
+          <dialog id="deleteGroupModal">
+            <gr-dialog
+              id="deleteGroupDialog"
+              confirm-label="Delete"
+              @confirm=${this.handleDeleteGroup}
+              @cancel=${() => this.deleteGroupModal?.close()}
+            >
+              <div class="header" slot="header">
+                Are you really sure you want to delete the group
+                "${this.originalName}"?
+              </div>
+            </gr-dialog>
+          </dialog>
+        </div>
+      `
+    );
+  }
+
   override willUpdate(changedProperties: PropertyValues) {
     if (changedProperties.has('groupId')) {
       this.loadGroup();
@@ -417,6 +476,16 @@
     this.originalOptionsVisibleToAll = visible;
   }
 
+  async handleDeleteGroup() {
+    if (!this.originalName) return;
+    await this.restApiService.deleteGroup(this.originalName);
+    this.restApiService.invalidateGroupsCache();
+    this.deleteGroupModal?.close();
+    this.getNavigation().setUrl(
+      createAdminUrl({adminView: AdminChildView.GROUPS})
+    );
+  }
+
   private computeHeaderClass(configChanged: boolean) {
     return configChanged ? 'edited' : '';
   }
@@ -467,19 +536,16 @@
     this.requestUpdate();
   }
 
-  private handleDescriptionTextChanged(e: ValueChangedEvent) {
+  private handleDescriptionInput(e: InputEvent) {
     if (!this.groupConfig || this.loading) return;
-    this.groupConfig.description = e.detail.value;
+    const value = (e.target as GrAutogrowTextarea).value ?? '';
+    this.groupConfig.description = value;
     this.requestUpdate();
   }
 
-  private handleOptionsBindValueChanged(e: BindValueChangeEvent) {
+  private handleOptionsChange(e: Event) {
     if (!this.groupConfig || this.loading) return;
-
-    // Because the value for e.detail.value is a string
-    // we convert the value to a boolean.
-    const value = e.detail.value === 'true' ? true : false;
-    this.groupConfig.options!.visible_to_all = value;
+    this.groupConfig.options!.visible_to_all = (e.target as MdCheckbox).checked;
     this.requestUpdate();
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
index 0fb85bc..ddf1e2f 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
@@ -19,8 +19,8 @@
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {GrSelect} from '../../shared/gr-select/gr-select';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
 
 suite('gr-group tests', () => {
   let element: GrGroup;
@@ -98,14 +98,14 @@
                 <h3 class="heading-3">Description</h3>
                 <fieldset>
                   <div>
-                    <gr-suggestion-textarea
+                    <gr-autogrow-textarea
                       autocomplete="on"
-                      class="description monospace"
+                      class="description"
                       disabled=""
-                      monospace=""
-                      rows="4"
+                      id="descriptionInput"
+                      placeholder="<Insert group description here>"
                     >
-                    </gr-suggestion-textarea>
+                    </gr-autogrow-textarea>
                   </div>
                   <span class="value">
                     <gr-button
@@ -125,12 +125,7 @@
                       Make group visible to all registered users
                     </span>
                     <span class="value">
-                      <gr-select id="visibleToAll">
-                        <select disabled="">
-                          <option value="false">False</option>
-                          <option value="true">True</option>
-                        </select>
-                      </gr-select>
+                      <md-checkbox disabled="" id="visibleToAll"> </md-checkbox>
                     </span>
                   </section>
                   <span class="value">
@@ -180,11 +175,8 @@
     element.groupId = '1' as GroupId;
     await element.loadGroup();
     assert.isTrue(element.groupIsInternal);
-    // The value returned is a boolean in a string
-    // thus we have to check with the string.
-    assert.equal(
-      queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue,
-      'false'
+    assert.isFalse(
+      queryAndAssert<MdCheckbox>(element, '#visibleToAll').checked
     );
   });
 
@@ -199,11 +191,8 @@
     element.groupId = '1' as GroupId;
     await element.loadGroup();
     assert.isFalse(element.groupIsInternal);
-    // The value returned is a boolean in a string
-    // thus we have to check with the string.
-    assert.equal(
-      queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue,
-      'false'
+    assert.isFalse(
+      queryAndAssert<MdCheckbox>(element, '#visibleToAll').checked
     );
   });
 
@@ -369,7 +358,7 @@
     element.groupId = '1' as GroupId;
 
     const response = {...new Response(), status: 404};
-    stubRestApi('getGroupConfig').callsFake((_, errFn) => {
+    stubRestApi('getGroupConfig').callsFake((_: any, errFn: any) => {
       if (errFn !== undefined) {
         errFn(response);
       } else {
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index f8fdeda..8179f92 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -3,30 +3,30 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-toggle-button/paper-toggle-button';
+import '@material/web/switch/switch';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import '../gr-rule-editor/gr-rule-editor';
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {
-  toSortedPermissionsArray,
-  PermissionArrayItem,
-  PermissionArray,
   AccessPermissionId,
+  PermissionArray,
+  PermissionArrayItem,
+  toSortedPermissionsArray,
 } from '../../../utils/access-util';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
+  GitRef,
+  GroupInfo,
   LabelNameToLabelTypeInfoMap,
   LabelTypeInfoValues,
-  GroupInfo,
-  GitRef,
   RepoName,
 } from '../../../types/common';
 import {
   AutocompleteQuery,
   GrAutocomplete,
-  AutocompleteSuggestion,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {
   EditablePermissionInfo,
   EditablePermissionRuleInfo,
@@ -35,7 +35,7 @@
 import {getAppContext} from '../../../services/app-context';
 import {fire} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {paperStyles} from '../../../styles/gr-paper-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
 import {when} from 'lit/directives/when.js';
@@ -44,6 +44,7 @@
   ValueChangedEvent,
 } from '../../../types/events';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {MdSwitch} from '@material/web/switch/switch';
 import {repeat} from 'lit/directives/repeat.js';
 
 const MAX_AUTOCOMPLETE_RESULTS = 20;
@@ -146,7 +147,7 @@
   static override get styles() {
     return [
       sharedStyles,
-      paperStyles,
+      materialStyles,
       grFormStyles,
       menuPageStyles,
       css`
@@ -227,14 +228,12 @@
                   this.section
                 ),
                 () => html`
-                  <paper-toggle-button
+                  <md-switch
                     id="exclusiveToggle"
-                    ?checked=${this.permission?.value.exclusive}
+                    ?selected=${!!this.permission?.value.exclusive}
                     ?disabled=${!this.editing}
                     @change=${this.handleValueChange}
-                    @click=${this.onTapExclusiveToggle}
-                    @tap=${this.onTapExclusiveToggle}
-                  ></paper-toggle-button
+                  ></md-switch
                   >${this.computeExclusiveLabel(this.permission?.value)}
                 `
               )}
@@ -358,7 +357,7 @@
       return;
     }
     this.permission.value.modified = true;
-    this.permission.value.exclusive = (e.target as HTMLInputElement).checked;
+    this.permission.value.exclusive = (e.target as MdSwitch).selected;
     // Allows overall access page to know a change has been made.
     fire(this, 'access-modified', {});
   }
@@ -557,13 +556,6 @@
     return permission?.exclusive ? 'Exclusive' : 'Not Exclusive';
   }
 
-  /**
-   * Work around a issue on iOS when clicking turns into double tap
-   */
-  private onTapExclusiveToggle(e: Event) {
-    e.preventDefault();
-  }
-
   // TODO: Do not use generic `CustomEvent`.
   // There is something fishy going on here though.
   // `e.detail.value` is of type `Rule`, but `splice()` expects a `number`.
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
index 4f7ce4f..3092fa1 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
@@ -14,8 +14,8 @@
 import {queryAndAssert} from '../../../test/test-utils';
 import {GrRuleEditor} from '../gr-rule-editor/gr-rule-editor';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
-import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdSwitch} from '@material/web/switch/switch';
 import {AutocompleteCommitEventDetail} from '../../../types/events';
 
 suite('gr-permission tests', () => {
@@ -328,17 +328,7 @@
               <div class="header">
                 <span class="title"> Priority </span>
                 <div class="right">
-                  <paper-toggle-button
-                    aria-disabled="true"
-                    aria-pressed="false"
-                    disabled=""
-                    id="exclusiveToggle"
-                    role="button"
-                    style="pointer-events: none; touch-action: none;"
-                    tabindex="-1"
-                    toggles=""
-                  >
-                  </paper-toggle-button>
+                  <md-switch disabled="" id="exclusiveToggle"> </md-switch>
                   Not Exclusive
                   <gr-button
                     aria-disabled="false"
@@ -376,13 +366,7 @@
               </gr-button>
             </div>
           </section>
-        `,
-        // touch-action varies on paper-toggle-button between local and CI
-        {
-          ignoreAttributes: [
-            {tags: ['paper-toggle-button'], attributes: ['style']},
-          ],
-        }
+        `
       );
     });
 
@@ -495,10 +479,7 @@
 
       assert.isFalse(element.originalExclusiveValue);
       assert.isNotOk(element.permission!.value.modified);
-      queryAndAssert<PaperToggleButtonElement>(
-        element,
-        '#exclusiveToggle'
-      ).click();
+      queryAndAssert<MdSwitch>(element, '#exclusiveToggle').click();
       await element.updateComplete;
       assert.isTrue(element.permission!.value.exclusive);
       assert.isTrue(element.permission!.value.modified);
@@ -517,10 +498,7 @@
       element.addEventListener('access-modified', modifiedHandler);
       await element.updateComplete;
       assert.isNotOk(element.permission.value.modified);
-      queryAndAssert<PaperToggleButtonElement>(
-        element,
-        '#exclusiveToggle'
-      ).click();
+      queryAndAssert<MdSwitch>(element, '#exclusiveToggle').click();
       await element.updateComplete;
       assert.isTrue(element.permission.value.modified);
       assert.isTrue(modifiedHandler.called);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
index cccc55a..ef433a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.ts
@@ -3,19 +3,18 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
-import '@polymer/paper-toggle-button/paper-toggle-button';
 import '../../shared/gr-button/gr-button';
 import {
-  PluginConfigOptionsChangedEventDetail,
   ArrayPluginOption,
+  PluginConfigOptionsChangedEventDetail,
 } from '../gr-repo-plugin-config/gr-repo-plugin-config-types';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
 import {fireNoBubbleNoCompose} from '../../../utils/event-util';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -41,6 +40,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       sharedStyles,
       grFormStyles,
       css`
@@ -80,6 +80,9 @@
           color: var(--deemphasized-text-color);
           padding-top: var(--spacing-m);
         }
+        md-outlined-text-field {
+          flex-grow: 1;
+        }
       `,
     ];
   }
@@ -89,16 +92,18 @@
       <div class="wrapper gr-form-styles">
         ${this.renderPluginOptions()}
         <div class="row ${this.disabled ? 'hide' : ''}">
-          <iron-input
-            .bindValue=${this.newValue}
-            @bind-value-changed=${this.handleBindValueChangedNewValue}
+          <md-outlined-text-field
+            id="input"
+            class="showBlueFocusBorder"
+            .value=${this.newValue ?? ''}
+            ?disabled=${this.disabled}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
+              this.newValue = target.value;
+            }}
+            @keydown=${this.handleInputKeydown}
           >
-            <input
-              id="input"
-              @keydown=${this.handleInputKeydown}
-              ?disabled=${this.disabled}
-            />
-          </iron-input>
+          </md-outlined-text-field>
           <gr-button
             id="addButton"
             ?disabled=${!this.newValue.length}
@@ -175,8 +180,4 @@
     };
     fireNoBubbleNoCompose(this, 'plugin-config-option-changed', detail);
   }
-
-  private handleBindValueChangedNewValue(e: BindValueChangeEvent) {
-    this.newValue = e.detail.value ?? '';
-  }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts
index fea0d58..e710ef5 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.ts
@@ -8,9 +8,9 @@
 import '../../../test/common-test-setup';
 import './gr-plugin-config-array-editor';
 import {GrPluginConfigArrayEditor} from './gr-plugin-config-array-editor';
-import {queryAll, queryAndAssert, pressKey} from '../../../test/test-utils';
+import {pressKey, queryAll, queryAndAssert} from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {Key} from '../../../utils/dom-util';
 
 suite('gr-plugin-config-array-editor tests', () => {
@@ -38,9 +38,14 @@
         <div class="gr-form-styles wrapper">
           <div class="placeholder row">None configured.</div>
           <div class="row">
-            <iron-input>
-              <input id="input" />
-            </iron-input>
+            <md-outlined-text-field
+              autocomplete=""
+              class="showBlueFocusBorder"
+              id="input"
+              inputmode=""
+              type="text"
+            >
+            </md-outlined-text-field>
             <gr-button
               aria-disabled="true"
               disabled=""
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index 852f907..028d75f 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -11,7 +11,7 @@
 import {encodeURL, getBaseUrl} from '../../../utils/url-util';
 import {tableStyles} from '../../../styles/gr-table-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {
   AdminChildView,
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
index 7c41120..74f9464 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
@@ -17,7 +17,7 @@
 import {PluginInfo} from '../../../types/common';
 import {GerritView} from '../../../services/router/router-model';
 import {PageErrorEvent} from '../../../types/events';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {AdminChildView, AdminViewState} from '../../../models/views/admin';
 
 function pluginGenerator(counter: number) {
@@ -390,7 +390,7 @@
     test('fires page-error', async () => {
       const response = {status: 404} as Response;
       stubRestApi('getPlugins').callsFake(
-        (_filter, _pluginsPerPage, _offset, errFn) => {
+        (_filter: any, _pluginsPerPage: any, _offset: any, errFn: any) => {
           if (errFn !== undefined) {
             errFn(response);
           }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 17e24b5..2632ff5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -8,35 +8,33 @@
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {toSortedPermissionsArray} from '../../../utils/access-util';
 import {
-  RepoName,
-  ProjectInfo,
   CapabilityInfoMap,
+  GitRef,
   LabelNameToLabelTypeInfoMap,
   ProjectAccessInput,
-  GitRef,
-  UrlEncodedRepoName,
+  ProjectInfo,
   RepoAccessGroups,
+  RepoName,
+  UrlEncodedRepoName,
 } from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrAccessSection} from '../gr-access-section/gr-access-section';
-import {
-  AutocompleteQuery,
-  AutocompleteSuggestion,
-} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {
   EditableLocalAccessSectionInfo,
   PermissionAccessSection,
-  PropertyTreeNode,
   PrimitiveValue,
+  PropertyTreeNode,
 } from './gr-repo-access-interfaces';
-import {firePageError, fireAlert} from '../../../utils/event-util';
+import {fireAlert, firePageError} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
 import {WebLinkInfo} from '../../../types/diff';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {
@@ -157,6 +155,7 @@
         .weblinks.show,
         .referenceContainer {
           display: block;
+          align-content: center;
         }
         .rightsText {
           margin-right: var(--spacing-s);
@@ -165,11 +164,15 @@
         .editing gr-button,
         .admin #editBtn {
           display: inline-block;
-          margin: var(--spacing-l) 0;
+          margin: var(--spacing-l);
         }
         .editing #editInheritFromInput {
           display: inline-block;
         }
+
+        .topLevelButtons {
+          display: flex;
+        }
       `,
     ];
   }
@@ -197,24 +200,52 @@
             >
             <gr-autocomplete
               id="editInheritFromInput"
-              .text=${this.inheritFromFilter}
+              .text=${this.inheritFromFilter ?? ''}
               .query=${this.query}
+              .showBlueFocusBorder=${true}
               @commit=${(e: AutocompleteCommitEvent) => {
                 this.handleUpdateInheritFrom(e);
               }}
-              @bind-value-changed=${(e: ValueChangedEvent) => {
-                this.handleUpdateInheritFrom(e);
-              }}
               @text-changed=${(e: ValueChangedEvent) => {
                 this.handleEditInheritFromTextChanged(e);
+                this.handleUpdateInheritFrom(e);
               }}
             ></gr-autocomplete>
           </h3>
-          <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
-            History:
-            ${this.weblinks?.map(
-              info => html`<gr-weblink .info=${info}></gr-weblink>`
-            )}
+          <div class="topLevelButtons">
+            <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
+              History:
+              ${this.weblinks?.map(
+                info => html`<gr-weblink .info=${info}></gr-weblink>`
+              )}
+            </div>
+            <div>
+              <gr-button
+                id="editBtn"
+                @click=${() => {
+                  this.handleEdit();
+                }}
+                >${this.editing ? 'Cancel' : 'Edit'}</gr-button
+              >
+              <gr-button
+                id="saveBtn"
+                class=${this.ownerOf && this.ownerOf.length === 0
+                  ? 'invisible'
+                  : ''}
+                primary
+                ?disabled=${!this.modified || this.disableSaveWithoutReview}
+                @click=${this.handleSave}
+                >Save</gr-button
+              >
+              <gr-button
+                id="saveReviewBtn"
+                class=${!this.canUpload ? 'invisible' : ''}
+                primary
+                ?disabled=${!this.modified}
+                @click=${this.handleSaveForReview}
+                >Save For Review</gr-button
+              >
+            </div>
           </div>
           ${this.sections?.map((section, index) =>
             this.renderPermissionSections(section, index)
@@ -226,33 +257,6 @@
               >Add Reference</gr-button
             >
           </div>
-          <div>
-            <gr-button
-              id="editBtn"
-              @click=${() => {
-                this.handleEdit();
-              }}
-              >${this.editing ? 'Cancel' : 'Edit'}</gr-button
-            >
-            <gr-button
-              id="saveBtn"
-              class=${this.ownerOf && this.ownerOf.length === 0
-                ? 'invisible'
-                : ''}
-              primary
-              ?disabled=${!this.modified || this.disableSaveWithoutReview}
-              @click=${this.handleSave}
-              >Save</gr-button
-            >
-            <gr-button
-              id="saveReviewBtn"
-              class=${!this.canUpload ? 'invisible' : ''}
-              primary
-              ?disabled=${!this.modified}
-              @click=${this.handleSaveForReview}
-              >Save For Review</gr-button
-            >
-          </div>
         </div>
       </div>
     `;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
index feea45e..1bb5b1d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
@@ -29,7 +29,7 @@
 import {GrAccessSection} from '../gr-access-section/gr-access-section';
 import {GrPermission} from '../gr-permission/gr-permission';
 import {createChange} from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-repo-access tests', () => {
@@ -137,7 +137,40 @@
               <a href="" id="inheritFromName" rel="noopener"> </a>
               <gr-autocomplete id="editInheritFromInput"> </gr-autocomplete>
             </h3>
-            <div class="weblinks">History:</div>
+            <div class="topLevelButtons">
+              <div class="weblinks">History:</div>
+              <div>
+                <gr-button
+                  aria-disabled="false"
+                  id="editBtn"
+                  role="button"
+                  tabindex="0"
+                >
+                  Edit
+                </gr-button>
+                <gr-button
+                  aria-disabled="true"
+                  disabled=""
+                  class="invisible"
+                  id="saveBtn"
+                  primary=""
+                  role="button"
+                  tabindex="-1"
+                >
+                  Save
+                </gr-button>
+                <gr-button
+                  aria-disabled="false"
+                  class="invisible"
+                  id="saveReviewBtn"
+                  primary=""
+                  role="button"
+                  tabindex="0"
+                >
+                  Save For Review
+                </gr-button>
+              </div>
+            </div>
             <div class="referenceContainer">
               <gr-button
                 aria-disabled="false"
@@ -148,37 +181,6 @@
                 Add Reference
               </gr-button>
             </div>
-            <div>
-              <gr-button
-                aria-disabled="false"
-                id="editBtn"
-                role="button"
-                tabindex="0"
-              >
-                Edit
-              </gr-button>
-              <gr-button
-                aria-disabled="true"
-                disabled=""
-                class="invisible"
-                id="saveBtn"
-                primary=""
-                role="button"
-                tabindex="-1"
-              >
-                Save
-              </gr-button>
-              <gr-button
-                aria-disabled="false"
-                class="invisible"
-                id="saveReviewBtn"
-                primary=""
-                role="button"
-                tabindex="0"
-              >
-                Save For Review
-              </gr-button>
-            </div>
           </div>
         </div>
       `
@@ -353,19 +355,21 @@
     } as CustomEvent);
     await element.updateComplete;
     assert.isOk(element.inheritsFrom);
-    assert.equal(element.inheritsFrom!.id, 'abc+123');
-    assert.equal(element.inheritsFrom!.name, 'foo bar baz' as RepoName);
+    assert.equal(element.inheritsFrom.id, 'abc+123');
+    assert.equal(element.inheritsFrom.name, 'foo bar baz' as RepoName);
   });
 
   test('fires page-error', async () => {
     const response = {status: 404} as Response;
 
-    stubRestApi('getRepoAccessRights').callsFake((_repoName, errFn) => {
-      if (errFn !== undefined) {
-        errFn(response);
+    stubRestApi('getRepoAccessRights').callsFake(
+      (_repoName: any, errFn: any) => {
+        if (errFn !== undefined) {
+          errFn(response);
+        }
+        return Promise.resolve(undefined);
       }
-      return Promise.resolve(undefined);
-    });
+    );
 
     const promise = mockPromise();
     addListenerForTest(document, 'page-error', e => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index 94424b7..d549782 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -13,8 +13,8 @@
 import {
   BranchName,
   ConfigInfo,
-  RevisionPatchSetNum,
   RepoName,
+  RevisionPatchSetNum,
 } from '../../../types/common';
 import {GrCreateChangeDialog} from '../gr-create-change-dialog/gr-create-change-dialog';
 import {
@@ -28,8 +28,8 @@
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
-import {customElement, query, property, state} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {createEditUrl} from '../../../models/views/change';
 import {resolve} from '../../../models/dependency';
@@ -186,7 +186,6 @@
             <gr-create-change-dialog
               id="createNewChangeModal"
               .repoName=${this.repo}
-              .privateByDefault=${this.repoConfig?.private_by_default}
               @can-create-change=${() => {
                 this.handleCanCreateChange();
               }}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
index 414266d..e92d286 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
@@ -17,7 +17,7 @@
 import {PageErrorEvent} from '../../../types/events';
 import {RepoName} from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-repo-commands tests', () => {
   let element: GrRepoCommands;
@@ -202,7 +202,7 @@
       element.repo = 'test' as RepoName;
 
       const response = {status: 404} as Response;
-      stubRestApi('getProjectConfig').callsFake((_repo, errFn) => {
+      stubRestApi('getProjectConfig').callsFake((_repo: any, errFn: any) => {
         if (errFn !== undefined) {
           errFn(response);
         }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index e099184..f46af38 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -3,18 +3,25 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {RepoName, DashboardId, DashboardInfo} from '../../../types/common';
+import {DashboardId, DashboardInfo, RepoName} from '../../../types/common';
 import {firePageError} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {LitElement, css, html, PropertyValues} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
 import {
-  DashboardType,
   createDashboardUrl,
+  DashboardType,
 } from '../../../models/views/dashboard';
+import {when} from 'lit/directives/when.js';
+import '../../shared/gr-list-view/gr-list-view';
+import {
+  createRepoUrl,
+  RepoDetailView,
+  RepoViewState,
+} from '../../../models/views/repo';
 
 interface DashboardRef {
   section: string;
@@ -26,11 +33,20 @@
   @property({type: String})
   repo?: RepoName;
 
-  @property({type: Boolean})
-  _loading = true;
+  @property({type: Object})
+  params?: RepoViewState;
 
-  @property({type: Array})
-  _dashboards?: DashboardRef[];
+  @state()
+  loading = true;
+
+  @state()
+  dashboards?: DashboardRef[];
+
+  @state() offset = 0;
+
+  @state() filter = '';
+
+  @state() itemsPerPage = 25;
 
   private readonly restApiService = getAppContext().restApiService;
 
@@ -43,66 +59,71 @@
           display: block;
           margin-bottom: var(--spacing-xxl);
         }
-        .loading #dashboards,
-        #loadingContainer {
-          display: none;
-        }
-        .loading #loadingContainer {
-          display: block;
-        }
       `,
     ];
   }
 
   override render() {
-    return html` <table
-      id="list"
-      class="genericList ${this._computeLoadingClass(this._loading)}"
+    return html` <gr-list-view
+      .filter=${this.filter}
+      .itemsPerPage=${this.itemsPerPage}
+      .items=${this.dashboards}
+      .loading=${this.loading}
+      .offset=${this.offset}
+      .path=${createRepoUrl({
+        repo: this.repo,
+        detail: RepoDetailView.DASHBOARDS,
+      })}
     >
-      <tbody>
-        <tr class="headerRow">
-          <th class="topHeader">Dashboard name</th>
-          <th class="topHeader">Dashboard title</th>
-          <th class="topHeader">Dashboard description</th>
-          <th class="topHeader">Inherited from</th>
-          <th class="topHeader">Default</th>
-        </tr>
-        <tr id="loadingContainer">
-          <td>Loading...</td>
-        </tr>
-      </tbody>
-      <tbody id="dashboards">
-        ${(this._dashboards ?? []).map(
-          item => html`
-            <tr class="groupHeader">
-              <td colspan="5">${item.section}</td>
-            </tr>
-            ${(item.dashboards ?? []).map(
-              info => html`
-                <tr class="table">
-                  <td class="name">
-                    <a href=${this._getUrl(info.project, info.id)}
-                      >${info.path}</a
-                    >
-                  </td>
-                  <td class="title">${info.title}</td>
-                  <td class="desc">${info.description}</td>
-                  <td class="inherited">
-                    ${this._computeInheritedFrom(
-                      info.project,
-                      info.defining_project
-                    )}
-                  </td>
-                  <td class="default">
-                    ${this._computeIsDefault(info.is_default)}
-                  </td>
-                </tr>
-              `
-            )}
-          `
-        )}
-      </tbody>
-    </table>`;
+      <table id="list" class="genericList">
+        <tbody id="dashboards">
+          <tr class="headerRow">
+            <th class="topHeader">Dashboard name</th>
+            <th class="topHeader">Dashboard title</th>
+            <th class="topHeader">Dashboard description</th>
+            <th class="topHeader">Inherited from</th>
+            <th class="topHeader">Default</th>
+          </tr>
+          ${when(
+            this.loading,
+            () => html`<tr id="loadingContainer">
+              <td>Loading...</td>
+            </tr>`,
+            () => html`
+              ${(this.dashboards ?? []).map(
+                item => html`
+                  <tr class="groupHeader">
+                    <td colspan="5">${item.section}</td>
+                  </tr>
+                  ${(item.dashboards ?? []).map(
+                    info => html`
+                      <tr class="table">
+                        <td class="name">
+                          <a href=${this.getUrl(info.project, info.id)}
+                            >${info.path}</a
+                          >
+                        </td>
+                        <td class="title">${info.title}</td>
+                        <td class="desc">${info.description}</td>
+                        <td class="inherited">
+                          ${this.computeInheritedFrom(
+                            info.project,
+                            info.defining_project
+                          )}
+                        </td>
+                        <td class="default">
+                          ${this.computeIsDefault(info.is_default)}
+                        </td>
+                      </tr>
+                    `
+                  )}
+                `
+              )}
+            `
+          )}
+        </tbody>
+      </table>
+    </gr-list-view>`;
   }
 
   override updated(changedProperties: PropertyValues) {
@@ -111,9 +132,25 @@
     }
   }
 
-  private repoChanged() {
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this.paramsChanged();
+    }
+  }
+
+  async paramsChanged() {
+    const params = this.params;
+    this.loading = true;
+    this.filter = params?.filter ?? '';
+    this.offset = Number(params?.offset ?? 0);
+
+    await this.repoChanged(this.filter, this.offset);
+  }
+
+  private repoChanged(filter?: string, offset?: number) {
     const repo = this.repo;
-    this._loading = true;
+    this.loading = true;
+
     if (!repo) {
       return Promise.resolve();
     }
@@ -129,10 +166,18 @@
           return;
         }
 
-        // Group by ref and sort by id.
-        const dashboards = res.concat
+        let dashboards = res.concat
           .apply([], res)
           .sort((a, b) => (a.id < b.id ? -1 : 1));
+
+        dashboards = dashboards
+          .filter(item =>
+            filter === undefined
+              ? true
+              : item.path.toLowerCase().includes(filter.toLowerCase())
+          )
+          .slice(offset ?? 0, (offset ?? 0) + this.itemsPerPage);
+
         const dashboardsByRef: Record<string, DashboardInfo[]> = {};
         dashboards.forEach(d => {
           if (!dashboardsByRef[d.ref]) {
@@ -151,26 +196,22 @@
             });
           });
 
-        this._dashboards = dashboardBuilder;
-        this._loading = false;
+        this.dashboards = dashboardBuilder;
+        this.loading = false;
       });
   }
 
-  _getUrl(project?: RepoName, dashboard?: DashboardId) {
+  private getUrl(project?: RepoName, dashboard?: DashboardId) {
     if (!project || !dashboard) return '';
 
     return createDashboardUrl({project, type: DashboardType.REPO, dashboard});
   }
 
-  _computeLoadingClass(loading: boolean) {
-    return loading ? 'loading' : '';
-  }
-
-  _computeInheritedFrom(project: RepoName, definingProject: RepoName) {
+  private computeInheritedFrom(project: RepoName, definingProject: RepoName) {
     return project === definingProject ? '' : definingProject;
   }
 
-  _computeIsDefault(isDefault?: boolean) {
+  private computeIsDefault(isDefault?: boolean) {
     return isDefault ? '✓' : '';
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
index 2bbc28b..b0acf64 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
@@ -9,13 +9,13 @@
 import {
   addListenerForTest,
   mockPromise,
-  queryAndAssert,
+  query,
   stubRestApi,
   waitEventLoop,
 } from '../../../test/test-utils';
 import {DashboardInfo, RepoName} from '../../../types/common';
 import {PageErrorEvent} from '../../../types/events';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-repo-dashboards tests', () => {
   let element: GrRepoDashboards;
@@ -83,47 +83,35 @@
       assert.shadowDom.equal(
         element,
         /* HTML */ `
-          <table class="genericList loading" id="list">
-            <tbody>
-              <tr class="headerRow">
-                <th class="topHeader">Dashboard name</th>
-                <th class="topHeader">Dashboard title</th>
-                <th class="topHeader">Dashboard description</th>
-                <th class="topHeader">Inherited from</th>
-                <th class="topHeader">Default</th>
-              </tr>
-              <tr id="loadingContainer">
-                <td>Loading...</td>
-              </tr>
-            </tbody>
-            <tbody id="dashboards"></tbody>
-          </table>
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody id="dashboards">
+                <tr class="headerRow">
+                  <th class="topHeader">Dashboard name</th>
+                  <th class="topHeader">Dashboard title</th>
+                  <th class="topHeader">Dashboard description</th>
+                  <th class="topHeader">Inherited from</th>
+                  <th class="topHeader">Default</th>
+                </tr>
+                <tr id="loadingContainer">
+                  <td>Loading...</td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
         `
       );
     });
 
     test('loading, sections, and ordering', async () => {
-      assert.isTrue(element._loading);
-      assert.notEqual(
-        getComputedStyle(queryAndAssert(element, '#loadingContainer')).display,
-        'none'
-      );
-      assert.equal(
-        getComputedStyle(queryAndAssert(element, '#dashboards')).display,
-        'none'
-      );
+      assert.isTrue(element.loading);
+      assert.isOk(query(element, '#loadingContainer'));
+
       element.repo = 'test' as RepoName;
       await waitEventLoop();
-      assert.equal(
-        getComputedStyle(queryAndAssert(element, '#loadingContainer')).display,
-        'none'
-      );
-      assert.notEqual(
-        getComputedStyle(queryAndAssert(element, '#dashboards')).display,
-        'none'
-      );
+      assert.isNotOk(query(element, '#loadingContainer'));
 
-      const dashboard = element._dashboards!;
+      const dashboard = element.dashboards!;
       assert.equal(dashboard.length, 2);
       assert.equal(dashboard[0].section, 'custom');
       assert.equal(dashboard[1].section, 'default');
@@ -138,8 +126,7 @@
   suite('404', () => {
     test('fires page-error', async () => {
       const response = {status: 404} as Response;
-      stubRestApi('getRepoDashboards').callsFake((_repo, errFn) => {
-        // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+      stubRestApi('getRepoDashboards').callsFake((_repo: any, errFn: any) => {
         errFn!(response);
         return Promise.resolve([]);
       });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
index 403130a..3308dab 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
@@ -3,7 +3,6 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-account-label/gr-account-label';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-date-formatter/gr-date-formatter';
@@ -30,9 +29,8 @@
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html, nothing} from 'lit';
-import {customElement, query, property, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {subscribe} from '../../../elements/lit/subscription-controller';
@@ -44,6 +42,8 @@
   RepoViewState,
 } from '../../../models/views/repo';
 import {modalStyles} from '../../../styles/gr-modal-styles';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const PGP_START = '-----BEGIN PGP SIGNATURE-----';
 
@@ -102,6 +102,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       tableStyles,
       sharedStyles,
@@ -113,7 +114,7 @@
         td.name,
         td.revision,
         td.message {
-          word-break: break-word;
+          overflow-wrap: anywhere;
         }
         td.revision.tags {
           width: 27em;
@@ -159,6 +160,9 @@
     return html`
       <gr-list-view
         .createNew=${this.loggedIn}
+        .createNewText=${this.detailType === RepoDetailView.BRANCHES
+          ? 'Create Branch'
+          : ' Create Tag'}
         .filter=${this.filter}
         .itemsPerPage=${this.itemsPerPage}
         .items=${this.items}
@@ -277,13 +281,15 @@
             >
               Edit
             </gr-button>
-            <iron-input
-              class="editItem"
-              .bindValue=${this.revisedRef}
-              @bind-value-changed=${this.handleRevisedRefBindValueChanged}
+            <md-outlined-text-field
+              class="editItem showBlueFocusBorder"
+              .value=${this.revisedRef ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                this.revisedRef = target.value as GitRef;
+              }}
             >
-              <input />
-            </iron-input>
+            </md-outlined-text-field>
             <gr-button
               class="cancelBtn editItem"
               link
@@ -622,10 +628,6 @@
     assertIsDefined(this.createNewModal, 'createNewModal');
     this.newItemName = !!this.createNewModal.itemName;
   }
-
-  private handleRevisedRefBindValueChanged(e: BindValueChangeEvent) {
-    this.revisedRef = e.detail.value as GitRef;
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
index 29aa599..e757566 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
@@ -31,7 +31,7 @@
 import {PageErrorEvent} from '../../../types/events';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {GrListView} from '../../shared/gr-list-view/gr-list-view';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {RepoDetailView} from '../../../models/views/repo';
 import {testResolver} from '../../../test/common-test-setup';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -157,9 +157,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -224,9 +228,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -252,7 +260,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -293,9 +301,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -321,7 +333,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -362,9 +374,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -390,7 +406,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -431,9 +447,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -459,7 +479,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -500,9 +520,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -528,7 +552,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -569,9 +593,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -597,7 +625,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -638,9 +666,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -666,7 +698,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -707,9 +739,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -735,7 +771,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -776,9 +812,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -804,7 +844,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -845,9 +885,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -873,7 +917,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -914,9 +958,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -942,7 +990,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -983,9 +1031,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1011,7 +1063,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1052,9 +1104,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1080,7 +1136,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1121,9 +1177,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1149,7 +1209,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1190,9 +1250,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1218,7 +1282,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1259,9 +1323,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1287,7 +1355,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1328,9 +1396,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1356,7 +1428,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1397,9 +1469,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1425,7 +1501,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1466,9 +1542,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1494,7 +1574,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1535,9 +1615,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1563,7 +1647,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1604,9 +1688,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1632,7 +1720,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1673,9 +1761,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1701,7 +1793,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1742,9 +1834,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1770,7 +1866,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -1811,9 +1907,13 @@
                         >
                           Edit
                         </gr-button>
-                        <iron-input class="editItem">
-                          <input />
-                        </iron-input>
+                        <md-outlined-text-field
+                          autocomplete=""
+                          class="editItem showBlueFocusBorder"
+                          inputmode=""
+                          type="text"
+                        >
+                        </md-outlined-text-field>
                         <gr-button
                           aria-disabled="false"
                           class="cancelBtn editItem"
@@ -1839,7 +1939,7 @@
                     <td class="hideItem message"></td>
                     <td class="hideItem tagger"></td>
                     <td class="repositoryBrowser">
-                      <gr-weblink imageAndText></gr-weblink>
+                      <gr-weblink imageandtext=""> </gr-weblink>
                     </td>
                     <td class="delete">
                       <gr-button
@@ -2134,7 +2234,13 @@
       test('fires page-error', async () => {
         const response = {status: 404} as Response;
         stubRestApi('getRepoBranches').callsFake(
-          (_filter, _repo, _reposBranchesPerPage, _offset, errFn) => {
+          (
+            _filter: any,
+            _repo: any,
+            _reposBranchesPerPage: any,
+            _offset: any,
+            errFn: any
+          ) => {
             if (errFn !== undefined) {
               errFn(response);
             }
@@ -2193,7 +2299,7 @@
       });
 
       test('test for tag message in the list', async () => {
-        assert.equal((element.items as TagInfo[])![2].message, 'Annotated tag');
+        assert.equal((element.items as TagInfo[])[2].message, 'Annotated tag');
       });
 
       test('test for tagger in the tag list', async () => {
@@ -2203,7 +2309,7 @@
           date: '2017-09-19 14:54:00.000000000' as Timestamp,
         };
 
-        assert.deepEqual((element.items as TagInfo[])![2].tagger, tagger);
+        assert.deepEqual((element.items as TagInfo[])[2].tagger, tagger);
       });
 
       test('test for web links in the tags list', async () => {
@@ -2331,7 +2437,13 @@
       test('fires page-error', async () => {
         const response = {status: 404} as Response;
         stubRestApi('getRepoTags').callsFake(
-          (_filter, _repo, _reposTagsPerPage, _offset, errFn) => {
+          (
+            _filter: any,
+            _repo: any,
+            _reposTagsPerPage: any,
+            _offset: any,
+            errFn: any
+          ) => {
             if (errFn !== undefined) {
               errFn(response);
             }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-labels/gr-repo-labels.ts b/polygerrit-ui/app/elements/admin/gr-repo-labels/gr-repo-labels.ts
new file mode 100644
index 0000000..caff018
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-labels/gr-repo-labels.ts
@@ -0,0 +1,961 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {RepoName} from '../../../types/common';
+import {fireError, firePageError} from '../../../utils/event-util';
+import {getAppContext} from '../../../services/app-context';
+import {ErrorCallback} from '../../../api/rest';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
+import {when} from 'lit/directives/when.js';
+import {grFormStyles} from '../../../styles/gr-form-styles';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {assertIsDefined} from '../../../utils/common-util';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import '../../shared/gr-list-view/gr-list-view';
+import {
+  createRepoUrl,
+  RepoDetailView,
+  RepoViewState,
+} from '../../../models/views/repo';
+import {
+  BatchLabelInput,
+  DeleteLabelInput,
+  LabelDefinitionInfo,
+  LabelDefinitionInfoFunction,
+  LabelDefinitionInput,
+  LabelValueToDescriptionMap,
+} from '../../../api/rest-api';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {createChangeUrl} from '../../../models/views/change';
+import {resolve} from '../../../models/dependency';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+
+@customElement('gr-repo-labels')
+export class GrRepoLabels extends LitElement {
+  @property({type: String})
+  repo?: RepoName;
+
+  @property({type: Object})
+  params?: RepoViewState;
+
+  @query('#createDialog')
+  private readonly createDialog?: HTMLDialogElement;
+
+  @query('#deleteDialog')
+  private readonly deleteDialog?: HTMLDialogElement;
+
+  @state()
+  loading = true;
+
+  @state()
+  labels?: LabelDefinitionInfo[];
+
+  @state()
+  showCreateDialog = false;
+
+  @state() isProjectOwner = false;
+
+  @state()
+  disableSaveWithoutReview = true;
+
+  @state()
+  showSaveForReviewButton = false;
+
+  @state()
+  newLabel: LabelDefinitionInput = this.getEmptyLabel();
+
+  @state() offset = 0;
+
+  @state() filter = '';
+
+  @state() itemsPerPage = 25;
+
+  @state() showDeleteDialog = false;
+
+  @state()
+  labelToDelete?: LabelDefinitionInfo;
+
+  @state()
+  labelToEdit?: LabelDefinitionInfo;
+
+  @state()
+  isEditing = false;
+
+  private readonly getNavigation = resolve(this, navigationToken);
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  static override get styles() {
+    return [
+      materialStyles,
+      sharedStyles,
+      tableStyles,
+      grFormStyles,
+      modalStyles,
+      css`
+        :host {
+          display: block;
+          margin-bottom: var(--spacing-xxl);
+        }
+        .actions {
+          display: flex;
+          justify-content: flex-end;
+          margin-bottom: var(--spacing-m);
+          padding: var(--spacing-l);
+        }
+        .createButton {
+          margin-left: var(--spacing-m);
+        }
+        .deleteBtn {
+          --gr-button-padding: var(--spacing-s) var(--spacing-m);
+        }
+        div.title-flex,
+        div.value-flex {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+        }
+        input {
+          width: 20em;
+          box-sizing: border-box;
+        }
+        div.gr-form-styles section {
+          margin: var(--spacing-m) 0;
+        }
+        div.gr-form-styles span.title {
+          width: 13em;
+        }
+        section .title gr-icon {
+          vertical-align: top;
+        }
+        textarea {
+          width: 20em;
+          min-height: 4em;
+          resize: vertical;
+          box-sizing: border-box;
+          background-color: var(--view-background-color);
+          color: var(--primary-text-color);
+        }
+        textarea:focus {
+          outline: none;
+          box-shadow: none;
+          border: 2px solid var(--input-focus-border-color);
+          margin: -1px;
+        }
+        textarea::placeholder {
+          color: var(--deemphasized-text-color);
+        }
+        gr-dialog .main {
+          max-height: 70vh;
+          overflow-y: auto;
+        }
+        gr-dialog .footer {
+          width: 100%;
+          display: flex;
+          justify-content: flex-end;
+        }
+        gr-dialog {
+          width: 36em;
+        }
+        .warning {
+          color: var(--warning-foreground);
+          margin-top: var(--spacing-s);
+        }
+        .warning gr-icon {
+          color: var(--warning-icon-color);
+          vertical-align: bottom;
+          margin-right: var(--spacing-s);
+        }
+        td gr-icon {
+          vertical-align: bottom;
+          margin-left: var(--spacing-s);
+        }
+        md-outlined-text-field {
+          max-width: 25em;
+        }
+        md-checkbox {
+          --md-checkbox-container-size: 15px;
+          --md-checkbox-icon-size: 15px;
+        }
+      `,
+    ];
+  }
+
+  constructor() {
+    super();
+    if (this.repo) {
+      this.checkProjectOwner();
+    }
+  }
+
+  private async checkProjectOwner() {
+    if (!this.repo) return;
+    try {
+      const access = await this.restApiService.getRepoAccessRights(this.repo);
+      this.isProjectOwner = !!access?.is_owner;
+      this.disableSaveWithoutReview =
+        !!access?.require_change_for_config_update;
+      this.showSaveForReviewButton = true;
+    } catch (e) {
+      console.error('Failed to check project owner status:', e);
+      this.isProjectOwner = false;
+    }
+  }
+
+  override render() {
+    return html`
+      <gr-list-view
+        .createNew=${this.isProjectOwner}
+        .filter=${this.filter}
+        .itemsPerPage=${this.itemsPerPage}
+        .items=${this.labels}
+        .loading=${this.loading}
+        .offset=${this.offset}
+        .path=${createRepoUrl({
+          repo: this.repo,
+          detail: RepoDetailView.LABELS,
+        })}
+        @create-clicked=${() => this.handleCreateClick()}
+      >
+        <table id="list" class="genericList">
+          <tbody>
+            <tr class="headerRow">
+              <th class="topHeader">Name</th>
+              <th class="topHeader">Description</th>
+              <th class="topHeader">Function</th>
+              <th class="topHeader">Default Value</th>
+              <th class="topHeader">Copy Condition</th>
+              <th class="topHeader">Allow Post Submit</th>
+              <th class="topHeader">Can Override</th>
+              <th class="topHeader">Ignore Self Approval</th>
+              <th class="topHeader">Branches</th>
+              <th class="topHeader">Values</th>
+              ${when(
+                this.isProjectOwner,
+                () => html`<th class="topHeader"></th>`
+              )}
+            </tr>
+          </tbody>
+          <tbody id="labels">
+            ${when(
+              this.loading,
+              () => html`<tr id="loadingContainer">
+                <td>Loading...</td>
+              </tr>`,
+              () =>
+                html` ${(this.labels ?? []).map(
+                  item => html`
+                    <tr class="table">
+                      <td class="name">${item.name}</td>
+                      <td class="description">${item.description}</td>
+                      <td class="function">
+                        ${item.function}
+                        ${when(
+                          this.isFunctionDeprecated(item.function),
+                          () => html`
+                            <gr-icon
+                              icon="warning"
+                              title="This function is deprecated."
+                            ></gr-icon>
+                          `
+                        )}
+                      </td>
+                      <td class="defaultValue">${item.default_value}</td>
+                      <td class="copyCondition">${item.copy_condition}</td>
+                      <td class="allowPostSubmit">
+                        ${this.renderCheckmark(item.allow_post_submit)}
+                      </td>
+                      <td class="canOverride">
+                        ${this.renderCheckmark(item.can_override)}
+                      </td>
+                      <td class="ignoreSelfApproval">
+                        ${this.renderCheckmark(item.ignore_self_approval)}
+                        ${when(
+                          item.ignore_self_approval,
+                          () => html`
+                            <gr-icon
+                              icon="warning"
+                              title="ignoreSelfApproval is deprecated."
+                            ></gr-icon>
+                          `
+                        )}
+                      </td>
+                      <td class="branches">
+                        ${(item.branches ?? []).join(', ')}
+                      </td>
+                      <td class="values">${JSON.stringify(item.values)}</td>
+                      ${when(
+                        this.isProjectOwner,
+                        () => html`
+                          <td class="actions">
+                            <gr-button
+                              class="editBtn"
+                              link
+                              @click=${() => this.handleEditClick(item)}
+                            >
+                              Edit
+                            </gr-button>
+                            <gr-button
+                              class="deleteBtn"
+                              link
+                              @click=${() => this.handleDeleteClick(item)}
+                            >
+                              Delete
+                            </gr-button>
+                          </td>
+                        `
+                      )}
+                    </tr>
+                  `
+                )}`
+            )}
+          </tbody>
+        </table>
+      </gr-list-view>
+
+      ${this.renderCreateDialog()} ${this.renderDeleteDialog()}
+    `;
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('repo')) {
+      this.getLabels();
+      this.checkProjectOwner();
+    }
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this.paramsChanged();
+    }
+  }
+
+  async paramsChanged() {
+    const params = this.params;
+    this.loading = true;
+    this.filter = params?.filter ?? '';
+    this.offset = Number(params?.offset ?? 0);
+
+    await this.getLabels(this.filter, this.offset);
+  }
+
+  private getLabels(filter?: string, offset?: number) {
+    const repo = this.repo;
+    this.loading = true;
+    if (!repo) {
+      return Promise.resolve();
+    }
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    return this.restApiService
+      .getRepoLabels(repo, errFn)
+      .then((res?: LabelDefinitionInfo[]) => {
+        if (!res) {
+          return;
+        }
+
+        this.labels = res
+          .filter(item =>
+            filter === undefined
+              ? true
+              : item.name.toLowerCase().includes(filter.toLowerCase())
+          )
+          .slice(offset ?? 0, (offset ?? 0) + this.itemsPerPage);
+        this.loading = false;
+      });
+  }
+
+  private isFunctionDeprecated(fun?: LabelDefinitionInfoFunction) {
+    if (!fun) return false;
+    return (
+      fun !== LabelDefinitionInfoFunction.NoBlock &&
+      fun !== LabelDefinitionInfoFunction.NoOp &&
+      fun !== LabelDefinitionInfoFunction.PatchSetLock
+    );
+  }
+
+  private renderCheckmark(check?: boolean) {
+    return check ? '✓' : '';
+  }
+
+  private handleCreateClick() {
+    this.isEditing = false;
+    this.newLabel = this.getEmptyLabel();
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.showModal();
+  }
+
+  private handleEditClick(label: LabelDefinitionInfo) {
+    this.isEditing = true;
+    this.labelToEdit = label;
+    this.newLabel = {
+      name: label.name,
+      description: label.description,
+      function: label.function,
+      default_value: label.default_value,
+      copy_condition: label.copy_condition,
+      allow_post_submit: label.allow_post_submit,
+      can_override: label.can_override,
+      ignore_self_approval: label.ignore_self_approval,
+      values: label.values,
+      branches: label.branches,
+    };
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.showModal();
+  }
+
+  private handleCreateCancel() {
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.close();
+    this.newLabel = this.getEmptyLabel();
+    this.labelToEdit = undefined;
+    this.isEditing = false;
+  }
+
+  private handleCreateConfirm() {
+    if (!this.repo) return;
+    if (!this.newLabel.name) {
+      return;
+    }
+
+    if (
+      this.isEditing &&
+      this.labelToEdit?.copy_condition &&
+      !this.newLabel.copy_condition
+    ) {
+      this.newLabel.unset_copy_condition = true;
+    }
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    const promise =
+      this.isEditing && this.labelToEdit
+        ? this.restApiService.updateRepoLabel(
+            this.repo,
+            this.labelToEdit.name,
+            this.newLabel,
+            errFn
+          )
+        : this.restApiService.createRepoLabel(
+            this.repo,
+            this.newLabel.name,
+            this.newLabel,
+            errFn
+          );
+
+    promise.then(() => {
+      this.createDialog?.close();
+      this.newLabel = this.getEmptyLabel();
+      this.labelToEdit = undefined;
+      this.isEditing = false;
+      this.getLabels(this.filter, this.offset);
+    });
+  }
+
+  private getEmptyLabel(): LabelDefinitionInput {
+    return {
+      name: '',
+      description: '',
+      function: LabelDefinitionInfoFunction.NoBlock,
+      default_value: 0,
+      copy_condition: '',
+      allow_post_submit: false,
+      can_override: true,
+      ignore_self_approval: false,
+      values: {
+        ' 0': 'No score',
+        '-1': 'I would prefer this is not submitted as is',
+        '-2': 'This shall not be submitted',
+        '+1': 'Looks good to me, but someone else must approve',
+        '+2': 'Looks good to me, approved',
+      },
+      branches: [],
+    };
+  }
+
+  private async handleSaveForReview(e: Event) {
+    if (!this.repo) return;
+    if (!this.newLabel.name) {
+      return;
+    }
+    const button = e.target as GrButton;
+    button.loading = true;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    if (
+      this.isEditing &&
+      this.labelToEdit?.copy_condition &&
+      !this.newLabel.copy_condition
+    ) {
+      this.newLabel.unset_copy_condition = true;
+    }
+
+    const input: BatchLabelInput = {};
+    if (this.isEditing && this.labelToEdit) {
+      const updatedLabel: Partial<LabelDefinitionInput> = {};
+      const original = this.labelToEdit;
+      const updated = this.newLabel;
+
+      if (updated.description !== original.description) {
+        updatedLabel.description = updated.description;
+      }
+      if (updated.function !== original.function) {
+        updatedLabel.function = updated.function;
+      }
+      if (JSON.stringify(updated.values) !== JSON.stringify(original.values)) {
+        updatedLabel.values = updated.values;
+      }
+      if (updated.default_value !== original.default_value) {
+        updatedLabel.default_value = updated.default_value;
+      }
+      if (
+        JSON.stringify(updated.branches) !== JSON.stringify(original.branches)
+      ) {
+        updatedLabel.branches = updated.branches;
+      }
+      if (updated.can_override !== original.can_override) {
+        updatedLabel.can_override = updated.can_override;
+      }
+      if (updated.copy_condition !== original.copy_condition) {
+        updatedLabel.copy_condition = updated.copy_condition;
+      }
+      if (updated.allow_post_submit !== original.allow_post_submit) {
+        updatedLabel.allow_post_submit = updated.allow_post_submit;
+      }
+      if (updated.ignore_self_approval !== original.ignore_self_approval) {
+        updatedLabel.ignore_self_approval = updated.ignore_self_approval;
+      }
+      if (updated.unset_copy_condition) {
+        updatedLabel.unset_copy_condition = updated.unset_copy_condition;
+      }
+      input.update = {[this.newLabel.name]: updatedLabel};
+    } else {
+      input.create = [this.newLabel];
+    }
+
+    const promise = this.restApiService.saveRepoLabelsForReview(
+      this.repo,
+      input,
+      errFn
+    );
+
+    try {
+      const change = await promise;
+      if (change) {
+        this.getNavigation().setUrl(createChangeUrl({change}));
+      }
+    } finally {
+      button.loading = false;
+      this.createDialog?.close();
+      this.newLabel = this.getEmptyLabel();
+      this.labelToEdit = undefined;
+      this.isEditing = false;
+      await this.getLabels(this.filter, this.offset);
+    }
+  }
+
+  private renderCreateDialog() {
+    if (!this.isProjectOwner) return nothing;
+
+    return html`
+      <dialog id="createDialog" tabindex="-1">
+        <gr-dialog .cancelLabel=${''} .confirmLabel=${''}>
+          <div class="header" slot="header">
+            ${this.isEditing ? 'Edit' : 'Create'} Label
+          </div>
+          <div class="main" slot="main">
+            <div class="gr-form-styles">
+              <div id="form">
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Name</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="name"
+                        class="showBlueFocusBorder"
+                        ?required=${true}
+                        ?disabled=${this.isEditing}
+                        .value=${this.newLabel.name ?? ''}
+                        @input=${(e: InputEvent) => {
+                          const target = e.target as HTMLInputElement;
+                          this.newLabel = {
+                            ...this.newLabel,
+                            name: target.value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Description</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="description"
+                        class="showBlueFocusBorder"
+                        .value=${this.newLabel.description ?? ''}
+                        @input=${(e: InputEvent) => {
+                          const target = e.target as HTMLInputElement;
+                          this.newLabel = {
+                            ...this.newLabel,
+                            description: target.value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Function</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <gr-dropdown-list
+                        .items=${Object.values(LabelDefinitionInfoFunction).map(
+                          fun => {
+                            return {
+                              text: fun,
+                              value: fun,
+                            };
+                          }
+                        )}
+                        .value=${this.newLabel.function ?? ''}
+                        @value-change=${(e: CustomEvent<{value: string}>) => {
+                          const value = e.detail.value;
+                          this.newLabel = {
+                            ...this.newLabel,
+                            function: value
+                              ? (value as LabelDefinitionInfoFunction)
+                              : undefined,
+                          };
+                        }}
+                      ></gr-dropdown-list>
+                      ${when(
+                        this.isFunctionDeprecated(this.newLabel.function),
+                        () => html`
+                          <div class="warning">
+                            <gr-icon icon="warning"></gr-icon>
+                            This function is deprecated.
+                          </div>
+                        `
+                      )}
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Default Value</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="defaultValue"
+                        class="showBlueFocusBorder"
+                        type="number"
+                        .value=${this.newLabel.default_value?.toString() ?? ''}
+                        @input=${(e: InputEvent) => {
+                          const target = e.target as HTMLInputElement;
+                          this.newLabel = {
+                            ...this.newLabel,
+                            default_value: Number(target.value),
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Copy Condition</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="copyCondition"
+                        class="showBlueFocusBorder"
+                        .value=${this.newLabel.copy_condition ?? ''}
+                        @input=${(e: InputEvent) => {
+                          const target = e.target as HTMLInputElement;
+                          this.newLabel = {
+                            ...this.newLabel,
+                            copy_condition: target.value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Can Override</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-checkbox
+                        id="canOverride"
+                        ?checked=${this.newLabel.can_override ?? false}
+                        @change=${(e: Event) => {
+                          this.newLabel = {
+                            ...this.newLabel,
+                            can_override: (e.target as MdCheckbox).checked,
+                          };
+                        }}
+                      ></md-checkbox>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Allow Post Submit</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-checkbox
+                        id="allowPostSubmit"
+                        ?checked=${this.newLabel.allow_post_submit ?? false}
+                        @change=${(e: Event) => {
+                          this.newLabel = {
+                            ...this.newLabel,
+                            allow_post_submit: (e.target as MdCheckbox).checked,
+                          };
+                        }}
+                      ></md-checkbox>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Ignore Self Approval</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-checkbox
+                        id="ignoreSelfApproval"
+                        ?checked=${this.newLabel.ignore_self_approval ?? false}
+                        @change=${(e: Event) => {
+                          this.newLabel = {
+                            ...this.newLabel,
+                            ignore_self_approval: (e.target as MdCheckbox)
+                              .checked,
+                          };
+                        }}
+                      ></md-checkbox>
+                      ${when(
+                        this.newLabel.ignore_self_approval,
+                        () => html`
+                          <div class="warning">
+                            <gr-icon icon="warning"></gr-icon>
+                            ignoreSelfApproval is deprecated.
+                          </div>
+                        `
+                      )}
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Branches</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <textarea
+                        id="branches"
+                        rows="2"
+                        .value=${(this.newLabel.branches ?? []).join('\n')}
+                        @change=${(e: Event) => {
+                          const target = e.target as HTMLTextAreaElement;
+                          this.newLabel = {
+                            ...this.newLabel,
+                            branches: target.value
+                              .split('\n')
+                              .map(b => b.trim())
+                              .filter(Boolean),
+                          };
+                        }}
+                      ></textarea>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Values</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <textarea
+                        id="values"
+                        .value=${JSON.stringify(this.newLabel.values, null, 2)}
+                        @change=${(e: Event) => {
+                          const target = e.target as HTMLTextAreaElement;
+                          try {
+                            this.newLabel = {
+                              ...this.newLabel,
+                              values: JSON.parse(
+                                target.value
+                              ) as LabelValueToDescriptionMap,
+                            };
+                          } catch (err) {
+                            fireError(this, 'Invalid JSON');
+                          }
+                        }}
+                      ></textarea>
+                    </span>
+                  </div>
+                </section>
+              </div>
+            </div>
+          </div>
+          <div class="footer" slot="footer">
+            <gr-button @click=${this.handleCreateCancel} link>Cancel</gr-button>
+            <gr-button
+              class="action save-button"
+              link
+              primary
+              ?disabled=${!this.newLabel.name || this.disableSaveWithoutReview}
+              @click=${this.handleCreateConfirm}
+            >
+              ${this.isEditing ? 'Save' : 'Create'}
+            </gr-button>
+            <gr-button
+              class="action save-for-review"
+              primary
+              link
+              ?hidden=${!this.showSaveForReviewButton}
+              ?disabled=${!this.newLabel.name}
+              @click=${this.handleSaveForReview}
+            >
+              Save for review
+            </gr-button>
+          </div>
+        </gr-dialog>
+      </dialog>
+    `;
+  }
+
+  private renderDeleteDialog() {
+    if (!this.isProjectOwner) return nothing;
+
+    return html`
+      <dialog id="deleteDialog" tabindex="-1">
+        <gr-dialog .cancelLabel=${''} .confirmLabel=${''}>
+          <div class="header" slot="header">Delete Label</div>
+          <div class="main" slot="main">
+            Are you sure you want to delete the label
+            "${this.labelToDelete?.name}"?
+          </div>
+          <div class="footer" slot="footer">
+            <gr-button link @click=${this.handleDeleteCancel}>Cancel</gr-button>
+            <gr-button
+              class="action"
+              link
+              ?disabled=${this.disableSaveWithoutReview}
+              @click=${this.handleDeleteConfirm}
+            >
+              Delete
+            </gr-button>
+            <gr-button
+              class="action"
+              primary
+              link
+              ?hidden=${!this.showSaveForReviewButton}
+              @click=${this.handleDeleteForReviewConfirm}
+            >
+              Delete for review
+            </gr-button>
+          </div>
+        </gr-dialog>
+      </dialog>
+    `;
+  }
+
+  private handleDeleteClick(label: LabelDefinitionInfo) {
+    this.labelToDelete = label;
+    assertIsDefined(this.deleteDialog, 'deleteDialog');
+    this.deleteDialog.showModal();
+  }
+
+  private handleDeleteCancel() {
+    assertIsDefined(this.deleteDialog, 'deleteDialog');
+    this.deleteDialog.close();
+    this.labelToDelete = undefined;
+  }
+
+  private async handleDeleteForReviewConfirm() {
+    if (!this.repo || !this.labelToDelete) return;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    const input: BatchLabelInput = {
+      delete: [this.labelToDelete.name],
+    };
+
+    const promise = this.restApiService.saveRepoLabelsForReview(
+      this.repo,
+      input,
+      errFn
+    );
+
+    try {
+      const change = await promise;
+      if (change) {
+        this.getNavigation().setUrl(createChangeUrl({change}));
+      }
+    } finally {
+      this.deleteDialog?.close();
+      this.labelToDelete = undefined;
+      await this.getLabels(this.filter, this.offset);
+    }
+  }
+
+  private handleDeleteConfirm() {
+    if (!this.repo || !this.labelToDelete) return;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    const deleteInput: DeleteLabelInput = {};
+
+    this.restApiService
+      .deleteRepoLabel(this.repo, this.labelToDelete.name, deleteInput, errFn)
+      .then(() => {
+        this.deleteDialog?.close();
+        this.labelToDelete = undefined;
+        this.getLabels(this.filter, this.offset);
+      });
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-repo-labels': GrRepoLabels;
+  }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-labels/gr-repo-labels_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-labels/gr-repo-labels_test.ts
new file mode 100644
index 0000000..62647d4
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-labels/gr-repo-labels_test.ts
@@ -0,0 +1,581 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-repo-labels';
+import {GrRepoLabels} from './gr-repo-labels';
+import {
+  queryAndAssert,
+  stubRestApi,
+  waitEventLoop,
+} from '../../../test/test-utils';
+import {RepoName} from '../../../types/common';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {
+  LabelDefinitionInfo,
+  LabelDefinitionInfoFunction,
+} from '../../../api/rest-api';
+import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {GrListView} from '../../shared/gr-list-view/gr-list-view';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+
+suite('gr-repo-labels tests', () => {
+  let element: GrRepoLabels;
+
+  setup(async () => {
+    element = await fixture(html`<gr-repo-labels></gr-repo-labels>`);
+  });
+
+  suite('labels table', () => {
+    setup(() => {
+      stubRestApi('getRepoLabels').returns(
+        Promise.resolve([
+          {
+            name: 'Code-Review',
+            description: 'This is a cool label',
+            function: 'MaxWithBlock',
+            values: undefined,
+            default_value: 0,
+            copy_condition: 'is:MIN',
+            unset_copy_condition: true,
+            allow_post_submit: true,
+            can_override: true,
+            ignore_self_approval: false,
+            branches: ['refs/heads/main'],
+          },
+        ] as unknown as LabelDefinitionInfo[])
+      );
+      element.repo = 'test' as RepoName;
+    });
+
+    test('render loading', () => {
+      element.repo = 'test2' as RepoName;
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `<gr-list-view>
+          <table class="genericList" id="list">
+            <tbody>
+              <tr class="headerRow">
+                <th class="topHeader">Name</th>
+                <th class="topHeader">Description</th>
+                <th class="topHeader">Function</th>
+                <th class="topHeader">Default Value</th>
+                <th class="topHeader">Copy Condition</th>
+                <th class="topHeader">Allow Post Submit</th>
+                <th class="topHeader">Can Override</th>
+                <th class="topHeader">Ignore Self Approval</th>
+                <th class="topHeader">Branches</th>
+                <th class="topHeader">Values</th>
+              </tr>
+            </tbody>
+            <tbody id="labels">
+              <tr id="loadingContainer">
+                <td>Loading...</td>
+              </tr>
+            </tbody>
+          </table>
+        </gr-list-view>`
+      );
+    });
+
+    test('render', async () => {
+      await waitEventLoop();
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody>
+                <tr class="headerRow">
+                  <th class="topHeader">Name</th>
+                  <th class="topHeader">Description</th>
+                  <th class="topHeader">Function</th>
+                  <th class="topHeader">Default Value</th>
+                  <th class="topHeader">Copy Condition</th>
+                  <th class="topHeader">Allow Post Submit</th>
+                  <th class="topHeader">Can Override</th>
+                  <th class="topHeader">Ignore Self Approval</th>
+                  <th class="topHeader">Branches</th>
+                  <th class="topHeader">Values</th>
+                </tr>
+              </tbody>
+              <tbody id="labels">
+                <tr class="table">
+                  <td class="name">Code-Review</td>
+                  <td class="description">This is a cool label</td>
+                  <td class="function">
+                    MaxWithBlock
+                    <gr-icon
+                      icon="warning"
+                      title="This function is deprecated."
+                    ></gr-icon>
+                  </td>
+                  <td class="defaultValue">0</td>
+                  <td class="copyCondition">is:MIN</td>
+                  <td class="allowPostSubmit">✓</td>
+                  <td class="canOverride">✓</td>
+                  <td class="ignoreSelfApproval"></td>
+                  <td class="branches">refs/heads/main</td>
+                  <td class="values"></td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
+        `
+      );
+    });
+
+    test('render as admin', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      await element.updateComplete;
+      assert.shadowDom.equal(
+        element,
+        `
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody>
+                <tr class="headerRow">
+                  <th class="topHeader">Name</th>
+                  <th class="topHeader">Description</th>
+                  <th class="topHeader">Function</th>
+                  <th class="topHeader">Default Value</th>
+                  <th class="topHeader">Copy Condition</th>
+                  <th class="topHeader">Allow Post Submit</th>
+                  <th class="topHeader">Can Override</th>
+                  <th class="topHeader">Ignore Self Approval</th>
+                  <th class="topHeader">Branches</th>
+                  <th class="topHeader">Values</th>
+                  <th class="topHeader"></th>
+                </tr>
+              </tbody>
+              <tbody id="labels">
+                <tr class="table">
+                  <td class="name">Code-Review</td>
+                  <td class="description">This is a cool label</td>
+                  <td class="function">
+                    MaxWithBlock
+                    <gr-icon
+                      icon="warning"
+                      title="This function is deprecated."
+                    ></gr-icon>
+                  </td>
+                  <td class="defaultValue">0</td>
+                  <td class="copyCondition">is:MIN</td>
+                  <td class="allowPostSubmit">✓</td>
+                  <td class="canOverride">✓</td>
+                  <td class="ignoreSelfApproval"></td>
+                  <td class="branches">refs/heads/main</td>
+                  <td class="values"></td>
+                  <td class="actions">
+                    <gr-button
+                      aria-disabled="false"
+                      class="editBtn"
+                      link=""
+                      role="button"
+                      tabindex="0"
+                    >
+                      Edit
+                    </gr-button>
+                    <gr-button
+                      aria-disabled="false"
+                      class="deleteBtn"
+                      link=""
+                      role="button"
+                      tabindex="0"
+                    >
+                      Delete
+                    </gr-button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
+          <dialog id="createDialog" tabindex="-1">
+            <gr-dialog>
+              <div class="header" slot="header">Create Label</div>
+              <div class="main" slot="main">
+                <div class="gr-form-styles">
+                  <div id="form">
+                    <section>
+                      <div class="title-flex">
+                        <span class="title"> Name </span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="name"
+                        inputmode=""
+                        required=""
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title">Description</span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="description"
+                        inputmode=""
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title"> Function </span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                          <gr-dropdown-list> </gr-dropdown-list>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title"> Default Value </span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="defaultValue"
+                        inputmode=""
+                        type="number"
+                      >
+                      </md-outlined-text-field>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title"> Copy Condition </span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="copyCondition"
+                        inputmode=""
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title">Can Override</span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                          <md-checkbox id="canOverride" checked=""></md-checkbox>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title">Allow Post Submit</span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                          <md-checkbox id="allowPostSubmit"></md-checkbox>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title">Ignore Self Approval</span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                          <md-checkbox id="ignoreSelfApproval"></md-checkbox>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title">Branches</span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                          <textarea id="branches" rows="2"></textarea>
+                        </span>
+                      </div>
+                    </section>
+                    <section>
+                      <div class="title-flex">
+                        <span class="title"> Values </span>
+                      </div>
+                      <div class="value-flex">
+                        <span class="value">
+                          <textarea id="values"></textarea>
+                        </span>
+                      </div>
+                    </section>
+                  </div>
+                </div>
+              </div>
+              <div class="footer" slot="footer">
+                <gr-button aria-disabled="false" role="button" link="" 
+                  tabindex="0"
+                  >Cancel</gr-button
+                >
+                <gr-button
+                  aria-disabled="true"
+                  class="action save-button" 
+                  disabled=""
+                  link=""
+                  primary=""
+                  role="button"
+                  tabindex="-1"
+                  >Create</gr-button
+                >
+                <gr-button
+                  aria-disabled="true"
+                  class="action save-for-review"
+                  disabled=""
+                  link=""
+                  primary=""
+                  role="button"
+                  tabindex="-1"
+                  >Save for review</gr-button
+                >
+              </div>
+            </gr-dialog>
+          </dialog>
+          <dialog id="deleteDialog" tabindex="-1">
+            <gr-dialog>
+              <div class="header" slot="header">Delete Label</div>
+              <div class="main" slot="main">
+                Are you sure you want to delete the label
+            ""?
+              </div>
+              <div
+                class="footer"
+                slot="footer"
+              >
+                <gr-button
+                  aria-disabled="false"
+                  role="button"
+                  link=""
+                  tabindex="0"
+                >
+                  Cancel
+                </gr-button>
+                <gr-button
+                  aria-disabled="false"
+                  class="action"
+                  link=""
+                  role="button"
+                  tabindex="0"
+                >
+                  Delete
+                </gr-button>
+                <gr-button
+                  aria-disabled="false"
+                  class="action"
+                  primary=""
+                  link=""
+                  role="button"
+                  tabindex="0"
+                >
+                  Delete for review
+                </gr-button>
+              </div>
+            </gr-dialog>
+          </dialog>
+        `
+      );
+    });
+
+    test('open edit dialog', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      await element.updateComplete;
+
+      const editButton = queryAndAssert<GrButton>(element, '.editBtn');
+      editButton.click();
+      await element.updateComplete;
+
+      // Verify dialog is open and has correct title
+      const dialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      assert.isTrue(dialog.open);
+
+      const dialogTitle = queryAndAssert<HTMLElement>(element, '.header');
+      assert.equal(dialogTitle.textContent?.trim(), 'Edit Label');
+
+      // Verify form is populated with correct data
+      const nameInput = queryAndAssert<HTMLInputElement>(element, '#name');
+      assert.equal(nameInput.value, 'Code-Review');
+      assert.isTrue(nameInput.disabled);
+
+      const descriptionInput = queryAndAssert<HTMLInputElement>(
+        element,
+        '#description'
+      );
+      assert.equal(descriptionInput.value, 'This is a cool label');
+
+      const functionDropdown = queryAndAssert<GrDropdownList>(
+        element,
+        'gr-dropdown-list'
+      );
+      assert.equal(functionDropdown.value, 'MaxWithBlock');
+      assert.deepEqual(
+        functionDropdown.items,
+        Object.values(LabelDefinitionInfoFunction).map(fun => {
+          return {
+            text: fun,
+            value: fun,
+          };
+        })
+      );
+
+      const copyConditionInput = queryAndAssert<HTMLInputElement>(
+        element,
+        '#copyCondition'
+      );
+      assert.equal(copyConditionInput.value, 'is:MIN');
+
+      const canOverrideInput = queryAndAssert<MdCheckbox>(
+        element,
+        '#canOverride'
+      );
+      assert.isTrue(canOverrideInput.checked);
+
+      const allowPostSubmitInput = queryAndAssert<MdCheckbox>(
+        element,
+        '#allowPostSubmit'
+      );
+      assert.isTrue(allowPostSubmitInput.checked);
+
+      const ignoreSelfApprovalInput = queryAndAssert<MdCheckbox>(
+        element,
+        '#ignoreSelfApproval'
+      );
+      assert.isFalse(ignoreSelfApprovalInput.checked);
+
+      const branchesTextarea = queryAndAssert<HTMLTextAreaElement>(
+        element,
+        '#branches'
+      );
+      assert.equal(branchesTextarea.value, 'refs/heads/main');
+
+      // Verify save button is enabled
+      const saveButton = queryAndAssert<HTMLElement>(element, 'gr-dialog');
+      assert.isFalse(saveButton.hasAttribute('disabled'));
+    });
+
+    test('open create dialog', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      await element.updateComplete;
+
+      const listView = queryAndAssert<GrListView>(element, 'gr-list-view');
+      listView.dispatchEvent(
+        new CustomEvent('create-clicked', {
+          bubbles: true,
+          composed: true,
+        })
+      );
+      await element.updateComplete;
+
+      // Verify dialog is open and has correct title
+      const dialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      assert.isTrue(dialog.open);
+
+      const dialogTitle = queryAndAssert<HTMLElement>(element, '.header');
+      assert.equal(dialogTitle.textContent?.trim(), 'Create Label');
+
+      const copyConditionInput = queryAndAssert<HTMLInputElement>(
+        element,
+        '#copyCondition'
+      );
+      assert.equal(copyConditionInput.value, '');
+    });
+
+    test('unset_copy_condition is set when copy_condition is cleared', async () => {
+      const updateLabelStub = stubRestApi('updateRepoLabel').returns(
+        Promise.resolve(undefined)
+      );
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      await element.updateComplete;
+
+      const editButton = queryAndAssert<GrButton>(element, '.editBtn');
+      editButton.click();
+      await element.updateComplete;
+
+      const copyConditionInput = queryAndAssert<HTMLInputElement>(
+        element,
+        '#copyCondition'
+      );
+      copyConditionInput.value = '';
+      copyConditionInput.dispatchEvent(
+        new CustomEvent('input', {
+          bubbles: true,
+          composed: true,
+        })
+      );
+      await element.updateComplete;
+
+      const saveButton = queryAndAssert<GrButton>(
+        element,
+        'gr-dialog gr-button.action.save-button'
+      );
+      saveButton.click();
+      await element.updateComplete;
+
+      assert.isTrue(updateLabelStub.calledOnce);
+      assert.isTrue(updateLabelStub.firstCall.args[2].unset_copy_condition);
+    });
+  });
+
+  suite('admin', () => {
+    setup(async () => {
+      element.isProjectOwner = true;
+      await element.updateComplete;
+    });
+
+    test('save button is disabled when require_change_for_config_update is set', async () => {
+      element.disableSaveWithoutReview = true;
+      element.newLabel.name = 'Test';
+      await element.updateComplete;
+
+      const dialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      dialog.showModal();
+      await element.updateComplete;
+
+      const saveButton = queryAndAssert<GrButton>(
+        element,
+        'gr-dialog gr-button.action.save-button'
+      );
+      assert.isTrue(saveButton.disabled);
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index 5318500..3053c0c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -7,14 +7,20 @@
 import '../../shared/gr-list-view/gr-list-view';
 import '../../shared/gr-weblink/gr-weblink';
 import '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {ProjectInfoWithName, WebLinkInfo} from '../../../types/common';
+import '../gr-create-change-dialog/gr-create-change-dialog';
+import {
+  BranchName,
+  ProjectInfoWithName,
+  RepoName,
+  WebLinkInfo,
+} from '../../../types/common';
 import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog';
 import {RepoState} from '../../../constants/constants';
 import {fireTitleChange} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
 import {tableStyles} from '../../../styles/gr-table-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
   AdminChildView,
@@ -24,6 +30,13 @@
 import {createSearchUrl} from '../../../models/views/search';
 import {modalStyles} from '../../../styles/gr-modal-styles';
 import {createRepoUrl} from '../../../models/views/repo';
+import {userModelToken} from '../../../models/user/user-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {assertIsDefined} from '../../../utils/common-util';
+import {GrCreateChangeDialog} from '../gr-create-change-dialog/gr-create-change-dialog';
+import {BindValueChangeEvent} from '../../../types/events';
+import '../../shared/gr-repo-branch-picker/gr-repo-branch-picker';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -35,8 +48,16 @@
 export class GrRepoList extends LitElement {
   @query('#createModal') private createModal?: HTMLDialogElement;
 
+  @query('#createChangeModal') private createChangeModal?: HTMLDialogElement;
+
   @query('#createNewModal') private createNewModal?: GrCreateRepoDialog;
 
+  @query('#createRepoModal')
+  private readonly createRepoModal?: HTMLDialogElement;
+
+  @query('#createNewChangeModal')
+  private readonly createNewChangeModal?: GrCreateChangeDialog;
+
   @property({type: Object})
   params?: AdminViewState;
 
@@ -54,11 +75,36 @@
 
   @state() filter = '';
 
+  @state() private loggedIn = false;
+
+  @state() private canCreateChange = false;
+
+  // Used to create a change
+  @state() private repo?: RepoName;
+
+  // Used to create a change
+  @state() private branch = '' as BranchName;
+
   private readonly restApiService = getAppContext().restApiService;
 
-  override async connectedCallback() {
+  private readonly getUserModel = resolve(this, userModelToken);
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getUserModel().capabilities$,
+      x => (this.createNewCapability = x?.createProject ?? false)
+    );
+    subscribe(
+      this,
+      () => this.getUserModel().loggedIn$,
+      x => (this.loggedIn = x)
+    );
+  }
+
+  override connectedCallback() {
     super.connectedCallback();
-    await this.getCreateRepoCapability();
     fireTitleChange('Repos');
     this.maybeOpenCreateModal(this.params);
   }
@@ -84,6 +130,11 @@
         .readOnly {
           white-space: nowrap;
         }
+        @media screen and (max-width: 50em) {
+          #repoBranchPicker {
+            height: 90vh;
+          }
+        }
       `,
     ];
   }
@@ -92,6 +143,7 @@
     return html`
       <gr-list-view
         .createNew=${this.createNewCapability}
+        .createNewText=${'Create Repository'}
         .filter=${this.filter}
         .itemsPerPage=${this.reposPerPage}
         .items=${this.repos}
@@ -100,6 +152,16 @@
         .path=${createAdminUrl({adminView: AdminChildView.REPOS})}
         @create-clicked=${() => this.handleCreateClicked()}
       >
+        <gr-button
+          id="createChangeButton"
+          slot="createNewContainerTop"
+          ?hidden=${!this.loggedIn}
+          primary
+          link
+          @click=${() => this.createNewChange()}
+        >
+          Create Change
+        </gr-button>
         <table id="list" class="genericList">
           <tbody>
             <tr class="headerRow">
@@ -121,7 +183,7 @@
           </tbody>
         </table>
       </gr-list-view>
-      <dialog id="createModal" tabindex="-1">
+      <dialog id="createRepoModal" tabindex="-1">
         <gr-dialog
           id="createDialog"
           class="confirmDialog"
@@ -139,6 +201,59 @@
           </div>
         </gr-dialog>
       </dialog>
+      <dialog id="createModal" tabindex="-1">
+        <gr-dialog
+          id="repoBranchPicker"
+          confirm-label="Next"
+          @confirm=${this.pickerConfirm}
+          @cancel=${() => {
+            assertIsDefined(this.createModal, 'createModal');
+            this.createModal.close();
+            this.repo = '' as RepoName;
+            this.branch = '' as BranchName;
+          }}
+          ?disabled=${!(this.repo && this.branch)}
+        >
+          <div class="header" slot="header">Create change</div>
+          <div class="main" slot="main">
+            <gr-repo-branch-picker
+              .repo=${this.repo}
+              .branch=${this.branch}
+              @repo-changed=${(e: BindValueChangeEvent) => {
+                this.repo = e.detail.value as RepoName;
+              }}
+              @branch-changed=${(e: BindValueChangeEvent) => {
+                this.branch = e.detail.value as BranchName;
+              }}
+            ></gr-repo-branch-picker>
+          </div>
+        </gr-dialog>
+      </dialog>
+      <dialog id="createChangeModal" tabindex="-1">
+        <gr-dialog
+          id="createChangeDialog"
+          confirm-label="Create"
+          ?disabled=${!this.canCreateChange}
+          @confirm=${() => {
+            this.handleCreateChange();
+          }}
+          @cancel=${() => {
+            this.handleCloseCreateChange();
+          }}
+        >
+          <div class="header" slot="header">Create Change</div>
+          <div class="main" slot="main">
+            <gr-create-change-dialog
+              id="createNewChangeModal"
+              .repoName=${this.repo}
+              .branch=${this.branch}
+              @can-create-change=${() => {
+                this.handleCanCreateChange();
+              }}
+            ></gr-create-change-dialog>
+          </div>
+        </gr-dialog>
+      </dialog>
     `;
   }
 
@@ -200,20 +315,6 @@
     }
   }
 
-  private async getCreateRepoCapability() {
-    const account = await this.restApiService.getAccount();
-
-    if (!account) return;
-
-    const accountCapabilities =
-      await this.restApiService.getAccountCapabilities(['createProject']);
-    if (accountCapabilities?.createProject) {
-      this.createNewCapability = true;
-    }
-
-    return account;
-  }
-
   // private but used in test
   async getRepos() {
     this.repos = [];
@@ -250,12 +351,12 @@
 
   // private but used in test
   handleCloseCreate() {
-    this.createModal?.close();
+    this.createRepoModal?.close();
   }
 
   // private but used in test
   handleCreateClicked() {
-    this.createModal?.showModal();
+    this.createRepoModal?.showModal();
     this.createNewModal?.focus();
   }
 
@@ -268,4 +369,36 @@
     if (!this.createNewModal) return;
     this.newRepoName = this.createNewModal.nameChanged;
   }
+
+  private createNewChange() {
+    assertIsDefined(this.createModal, 'createModal');
+    assertIsDefined(this.createNewChangeModal, 'createNewChangeModal');
+    this.createModal.showModal();
+  }
+
+  private handleCreateChange() {
+    assertIsDefined(this.createNewChangeModal, 'createNewChangeModal');
+    this.createNewChangeModal.handleCreateChange();
+    this.handleCloseCreateChange();
+  }
+
+  private handleCloseCreateChange() {
+    assertIsDefined(this.createChangeModal, 'createChangeModal');
+    this.createChangeModal.close();
+    this.repo = '' as RepoName;
+    this.branch = '' as BranchName;
+  }
+
+  private handleCanCreateChange() {
+    assertIsDefined(this.createNewChangeModal, 'createNewChangeModal');
+    this.canCreateChange =
+      !!this.createNewChangeModal.branch && !!this.createNewChangeModal.subject;
+  }
+
+  private pickerConfirm = () => {
+    assertIsDefined(this.createModal, 'createModal');
+    assertIsDefined(this.createChangeModal, 'createChangeModal');
+    this.createModal.close();
+    this.createChangeModal.showModal();
+  };
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
index d7377aa..13e1cc7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
@@ -13,15 +13,15 @@
   stubRestApi,
 } from '../../../test/test-utils';
 import {
-  UrlEncodedRepoName,
   ProjectInfoWithName,
   RepoName,
+  UrlEncodedRepoName,
 } from '../../../types/common';
 import {RepoState} from '../../../api/rest-api';
 import {GerritView} from '../../../services/router/router-model';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {GrListView} from '../../shared/gr-list-view/gr-list-view';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {AdminChildView, AdminViewState} from '../../../models/views/admin';
 import {testResolver} from '../../../test/common-test-setup';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -70,6 +70,17 @@
         element,
         /* HTML */ `
           <gr-list-view>
+            <gr-button
+              aria-disabled="false"
+              id="createChangeButton"
+              link=""
+              primary=""
+              role="button"
+              slot="createNewContainerTop"
+              tabindex="0"
+            >
+              Create Change
+            </gr-button>
             <table class="genericList" id="list">
               <tbody>
                 <tr class="headerRow">
@@ -91,7 +102,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -104,7 +115,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -117,7 +128,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -130,7 +141,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -143,7 +154,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -156,7 +167,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -169,7 +180,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -182,7 +193,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -195,7 +206,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -208,7 +219,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -221,7 +232,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -234,7 +245,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -247,7 +258,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -260,7 +271,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -273,7 +284,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -286,7 +297,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -299,7 +310,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -312,7 +323,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -325,7 +336,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -338,7 +349,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -351,7 +362,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -364,7 +375,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -377,7 +388,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -390,7 +401,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -403,7 +414,7 @@
                     <a href="/admin/repos/test"> test </a>
                   </td>
                   <td class="repositoryBrowser">
-                    <gr-weblink imageAndText></gr-weblink>
+                    <gr-weblink imageandtext=""> </gr-weblink>
                   </td>
                   <td class="changesLink">
                     <a href="/q/project:test"> view all </a>
@@ -414,7 +425,7 @@
               </tbody>
             </table>
           </gr-list-view>
-          <dialog id="createModal" tabindex="-1">
+          <dialog id="createRepoModal" tabindex="-1">
             <gr-dialog
               class="confirmDialog"
               confirm-label="Create"
@@ -429,6 +440,33 @@
               </div>
             </gr-dialog>
           </dialog>
+          <dialog id="createModal" tabindex="-1">
+            <gr-dialog
+              confirm-label="Next"
+              disabled=""
+              id="repoBranchPicker"
+              role="dialog"
+            >
+              <div class="header" slot="header">Create change</div>
+              <div class="main" slot="main">
+                <gr-repo-branch-picker> </gr-repo-branch-picker>
+              </div>
+            </gr-dialog>
+          </dialog>
+          <dialog id="createChangeModal" tabindex="-1">
+            <gr-dialog
+              confirm-label="Create"
+              disabled=""
+              id="createChangeDialog"
+              role="dialog"
+            >
+              <div class="header" slot="header">Create Change</div>
+              <div class="main" slot="main">
+                <gr-create-change-dialog id="createNewChangeModal">
+                </gr-create-change-dialog>
+              </div>
+            </gr-dialog>
+          </dialog>
         `
       );
     });
@@ -585,7 +623,7 @@
 
     test('handleCreateClicked opens modal', () => {
       const openStub = sinon.stub(
-        queryAndAssert<HTMLDialogElement>(element, '#createModal'),
+        queryAndAssert<HTMLDialogElement>(element, '#createRepoModal'),
         'showModal'
       );
       element.handleCreateClicked();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index ee43205..49b0ab3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -3,29 +3,28 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
-import '@polymer/paper-toggle-button/paper-toggle-button';
+import '@material/web/switch/switch';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
 import '../../shared/gr-select/gr-select';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
 import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {ConfigParameterInfoType} from '../../../constants/constants';
 import {
   ConfigParameterInfo,
   PluginParameterToConfigParameterInfoMap,
 } from '../../../types/common';
-import {PaperToggleButtonElement} from '@polymer/paper-toggle-button/paper-toggle-button';
-import {IronInputElement} from '@polymer/iron-input/iron-input';
 import {
   PluginConfigOptionsChangedEventDetail,
   PluginOption,
 } from './gr-repo-plugin-config-types';
-import {paperStyles} from '../../../styles/gr-paper-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {fire} from '../../../utils/event-util';
+import {MdSwitch} from '@material/web/switch/switch';
+import '@material/web/textfield/outlined-text-field';
 
 export interface ConfigChangeInfo {
   _key: string; // parameterName of PluginParameterToConfigParameterInfoMap
@@ -58,9 +57,10 @@
 
   static override get styles() {
     return [
+      materialStyles,
       sharedStyles,
       grFormStyles,
-      paperStyles,
+      materialStyles,
       subpageStyles,
       css`
         .inherited {
@@ -81,7 +81,7 @@
   override render() {
     // Render can be called prior to pluginData being updated.
     const pluginConfigOptions = this.pluginData
-      ? this._computePluginConfigOptions(this.pluginData)
+      ? this.computePluginConfigOptions(this.pluginData)
       : [];
 
     return html`
@@ -133,27 +133,25 @@
     if (option.info.type === ConfigParameterInfoType.ARRAY) {
       return html`
         <gr-plugin-config-array-editor
-          @plugin-config-option-changed=${this._handleArrayChange}
+          @plugin-config-option-changed=${this.handleArrayChange}
           .pluginOption=${option}
           ?disabled=${this.disabled || !option.info.editable}
         ></gr-plugin-config-array-editor>
       `;
     } else if (option.info.type === ConfigParameterInfoType.BOOLEAN) {
       return html`
-        <paper-toggle-button
-          ?checked=${this._computeChecked(option.info.value)}
-          @change=${this._handleBooleanChange}
+        <md-switch
+          ?selected=${this.computeChecked(option.info.value)}
+          @change=${this.handleBooleanChange}
           data-option-key=${option._key}
           ?disabled=${this.disabled || !option.info.editable}
-          @click=${this.onTapPluginBoolean}
-          @tap=${this.onTapPluginBoolean}
-        ></paper-toggle-button>
+        ></md-switch>
       `;
     } else if (option.info.type === ConfigParameterInfoType.LIST) {
       return html`
         <gr-select
           .bindValue=${option.info.value}
-          @change=${this._handleListChange}
+          @change=${this.handleListChange}
         >
           <select
             data-option-key=${option._key}
@@ -171,64 +169,61 @@
       option.info.type === ConfigParameterInfoType.LONG
     ) {
       return html`
-        <iron-input
-          .bindValue=${option.info.value ?? ''}
-          @input=${this._handleStringChange}
+        <md-outlined-text-field
+          class="showBlueFocusBorder"
+          .value=${option.info.value ?? ''}
           data-option-key=${option._key}
+          @input=${this.handleStringChange}
+          ?disabled=${this.disabled || !option.info.editable}
         >
-          <input
-            is="iron-input"
-            .value=${option.info.value ?? ''}
-            @input=${this._handleStringChange}
-            data-option-key=${option._key}
-            ?disabled=${this.disabled || !option.info.editable}
-          />
-        </iron-input>
+        </md-outlined-text-field>
       `;
     } else {
       return html``;
     }
   }
 
-  _computePluginConfigOptions(pluginData: PluginData) {
+  // Private but used in test
+  computePluginConfigOptions(pluginData: PluginData) {
     const config = pluginData.config;
     return Object.keys(config).map(_key => {
       return {_key, info: config[_key]};
     });
   }
 
-  _computeChecked(value = 'false') {
+  private computeChecked(value = 'false') {
     return JSON.parse(value) as boolean;
   }
 
-  _handleStringChange(e: Event) {
-    const el = e.target as IronInputElement;
+  private handleStringChange(e: Event) {
+    const el = e.target as HTMLInputElement;
     // In the template, the data-option-key is assigned to each editor
-    const _key = el.getAttribute('data-option-key')!;
-    const configChangeInfo = this._buildConfigChangeInfo(el.value, _key);
-    this._handleChange(configChangeInfo);
+    const key = el.getAttribute('data-option-key')!;
+    const configChangeInfo = this.buildConfigChangeInfo(el.value, key);
+    this.handleChange(configChangeInfo);
   }
 
-  _handleListChange(e: Event) {
+  private handleListChange(e: Event) {
     const el = e.target as HTMLOptionElement;
     // In the template, the data-option-key is assigned to each editor
-    const _key = el.getAttribute('data-option-key')!;
-    const configChangeInfo = this._buildConfigChangeInfo(el.value, _key);
-    this._handleChange(configChangeInfo);
+    const key = el.getAttribute('data-option-key')!;
+    const configChangeInfo = this.buildConfigChangeInfo(el.value, key);
+    this.handleChange(configChangeInfo);
   }
 
-  _handleBooleanChange(e: Event) {
-    const el = e.target as PaperToggleButtonElement;
+  private handleBooleanChange(e: Event) {
+    const el = e.target as MdSwitch;
     // In the template, the data-option-key is assigned to each editor
-    const _key = el.getAttribute('data-option-key')!;
-    const configChangeInfo = this._buildConfigChangeInfo(
-      JSON.stringify(el.checked),
-      _key
+    const key = el.getAttribute('data-option-key')!;
+    const configChangeInfo = this.buildConfigChangeInfo(
+      JSON.stringify(el.selected),
+      key
     );
-    this._handleChange(configChangeInfo);
+    this.handleChange(configChangeInfo);
   }
 
-  _buildConfigChangeInfo(
+  // Private but used in test
+  buildConfigChangeInfo(
     value: string | null | undefined,
     _key: string
   ): ConfigChangeInfo {
@@ -242,11 +237,13 @@
     };
   }
 
-  _handleArrayChange(e: CustomEvent<PluginConfigOptionsChangedEventDetail>) {
-    this._handleChange(e.detail);
+  // Private but used in test
+  handleArrayChange(e: CustomEvent<PluginConfigOptionsChangedEventDetail>) {
+    this.handleChange(e.detail);
   }
 
-  _handleChange({_key, info}: ConfigChangeInfo) {
+  // Private but used in test
+  handleChange({_key, info}: ConfigChangeInfo) {
     // If pluginData is not set, editors are not created and this method
     // can't be called
     const {name, config} = this.pluginData!;
@@ -258,13 +255,6 @@
     };
     fire(this, 'plugin-config-changed', detail);
   }
-
-  /**
-   * Work around a issue on iOS when clicking turns into double tap
-   */
-  private onTapPluginBoolean(e: Event) {
-    e.preventDefault();
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts
index 1691b88..3dcb999 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts
@@ -11,8 +11,9 @@
 import {ConfigParameterInfoType} from '../../../constants/constants';
 import {queryAndAssert} from '../../../test/test-utils';
 import {GrPluginConfigArrayEditor} from '../gr-plugin-config-array-editor/gr-plugin-config-array-editor';
-import {PaperToggleButtonElement} from '@polymer/paper-toggle-button/paper-toggle-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {MdSwitch} from '@material/web/switch/switch';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-repo-plugin-config tests', () => {
   let element: GrRepoPluginConfig;
@@ -43,9 +44,15 @@
                 <span> </span>
               </span>
               <span class="value">
-                <iron-input data-option-key="plugin">
-                  <input data-option-key="plugin" disabled="" is="iron-input" />
-                </iron-input>
+                <md-outlined-text-field
+                  autocomplete=""
+                  class="showBlueFocusBorder"
+                  data-option-key="plugin"
+                  disabled=""
+                  inputmode=""
+                  type="text"
+                >
+                </md-outlined-text-field>
               </span>
             </section>
           </fieldset>
@@ -54,9 +61,9 @@
     );
   });
 
-  test('_computePluginConfigOptions', () => {
+  test('computePluginConfigOptions', () => {
     assert.deepEqual(
-      element._computePluginConfigOptions({
+      element.computePluginConfigOptions({
         name: 'testInfo',
         config: {
           testKey: {display_name: 'testInfo plugin', type: 'STRING'},
@@ -74,7 +81,7 @@
     );
   });
 
-  test('_handleChange', () => {
+  test('handleChange', () => {
     const eventStub = sinon.stub(element, 'dispatchEvent');
     element.pluginData = {
       name: 'testName',
@@ -82,7 +89,7 @@
         plugin: {type: 'STRING' as ConfigParameterInfoType, value: 'test'},
       },
     };
-    element._handleChange({
+    element.handleChange({
       _key: 'plugin',
       info: {type: 'STRING' as ConfigParameterInfoType, value: 'newTest'},
     });
@@ -101,8 +108,8 @@
     let buildStub: sinon.SinonStub;
 
     setup(() => {
-      changeStub = sinon.stub(element, '_handleChange');
-      buildStub = sinon.stub(element, '_buildConfigChangeInfo');
+      changeStub = sinon.stub(element, 'handleChange');
+      buildStub = sinon.stub(element, 'buildConfigChangeInfo');
     });
 
     test('ARRAY type option', async () => {
@@ -123,7 +130,7 @@
         'gr-plugin-config-array-editor'
       );
       assert.ok(editor);
-      element._handleArrayChange({detail: 'test'} as CustomEvent);
+      element.handleArrayChange({detail: 'test'} as CustomEvent);
       assert.isTrue(changeStub.called);
       assert.equal(changeStub.lastCall.args[0], 'test');
     });
@@ -141,10 +148,7 @@
       };
       await element.updateComplete;
 
-      const toggle = queryAndAssert<PaperToggleButtonElement>(
-        element,
-        'paper-toggle-button'
-      );
+      const toggle = queryAndAssert<MdSwitch>(element, 'md-switch');
       assert.ok(toggle);
       toggle.click();
       await element.updateComplete;
@@ -168,7 +172,10 @@
       };
       await element.updateComplete;
 
-      const input = queryAndAssert<HTMLInputElement>(element, 'input');
+      const input = queryAndAssert<MdOutlinedTextField>(
+        element,
+        'md-outlined-text-field'
+      );
       assert.ok(input);
       input.value = 'newTest';
       input.dispatchEvent(new Event('input'));
@@ -210,14 +217,14 @@
     });
   });
 
-  test('_buildConfigChangeInfo', () => {
+  test('buildConfigChangeInfo', () => {
     element.pluginData = {
       name: 'testName',
       config: {
         plugin: {type: 'STRING' as ConfigParameterInfoType, value: 'test'},
       },
     };
-    const detail = element._buildConfigChangeInfo('newTest', 'plugin');
+    const detail = element.buildConfigChangeInfo('newTest', 'plugin');
     assert.equal(detail._key, 'plugin');
     assert.deepEqual(detail.info, {
       type: 'STRING' as ConfigParameterInfoType,
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
new file mode 100644
index 0000000..e40b5b8
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
@@ -0,0 +1,737 @@
+/**
+ * @license
+ * 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 {sharedStyles} from '../../../styles/shared-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
+import {when} from 'lit/directives/when.js';
+import {grFormStyles} from '../../../styles/gr-form-styles';
+import {assertIsDefined} from '../../../utils/common-util';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import '../../shared/gr-list-view/gr-list-view';
+import {
+  createRepoUrl,
+  RepoDetailView,
+  RepoViewState,
+} from '../../../models/views/repo';
+import '@material/web/textfield/outlined-text-field';
+import {resolve} from '../../../models/dependency';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {createChangeUrl} from '../../../models/views/change';
+import {
+  BatchSubmitRequirementInput,
+  SubmitRequirementInput,
+} from '../../../api/rest-api';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+
+@customElement('gr-repo-submit-requirements')
+export class GrRepoSubmitRequirements extends LitElement {
+  @property({type: String})
+  repo?: RepoName;
+
+  @property({type: Object})
+  params?: RepoViewState;
+
+  @query('#createDialog')
+  private readonly createDialog?: HTMLDialogElement;
+
+  @query('#deleteDialog')
+  private readonly deleteDialog?: HTMLDialogElement;
+
+  @state()
+  loading = true;
+
+  @state()
+  submitRequirements?: SubmitRequirementInfo[];
+
+  @state()
+  showCreateDialog = false;
+
+  @state() isProjectOwner = false;
+
+  @state()
+  disableSaveWithoutReview = true;
+
+  @state()
+  showSaveForReviewButton = false;
+
+  @state()
+  newRequirement: SubmitRequirementInput = this.getEmptyRequirement();
+
+  @state() offset = 0;
+
+  @state() filter = '';
+
+  @state() itemsPerPage = 25;
+
+  @state() showDeleteDialog = false;
+
+  @state() requirementToDelete?: SubmitRequirementInfo;
+
+  @state()
+  requirementToEdit?: SubmitRequirementInfo;
+
+  @state()
+  isEditing = false;
+
+  private readonly getNavigation = resolve(this, navigationToken);
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  static override get styles() {
+    return [
+      materialStyles,
+      sharedStyles,
+      tableStyles,
+      grFormStyles,
+      modalStyles,
+      css`
+        :host {
+          display: block;
+          margin-bottom: var(--spacing-xxl);
+        }
+        .actions {
+          display: flex;
+          justify-content: flex-end;
+          margin-bottom: var(--spacing-m);
+          padding: var(--spacing-l);
+        }
+        .createButton {
+          margin-left: var(--spacing-m);
+        }
+        .deleteBtn {
+          --gr-button-padding: var(--spacing-s) var(--spacing-m);
+        }
+        div.title-flex,
+        div.value-flex {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+        }
+        input {
+          width: 20em;
+          box-sizing: border-box;
+        }
+        div.gr-form-styles section {
+          margin: var(--spacing-m) 0;
+        }
+        div.gr-form-styles span.title {
+          width: 13em;
+        }
+        section .title gr-icon {
+          vertical-align: top;
+        }
+        textarea {
+          width: 20em;
+          min-height: 100px;
+          resize: vertical;
+          box-sizing: border-box;
+          background-color: var(--view-background-color);
+          color: var(--primary-text-color);
+        }
+        textarea:focus {
+          outline: none;
+          box-shadow: none;
+          border: 2px solid var(--input-focus-border-color);
+          margin: -1px;
+        }
+        textarea::placeholder {
+          color: var(--deemphasized-text-color);
+        }
+        gr-dialog {
+          width: 36em;
+        }
+        gr-dialog .footer {
+          width: 100%;
+          display: flex;
+          justify-content: flex-end;
+        }
+        md-outlined-text-field {
+          width: 20em;
+        }
+      `,
+    ];
+  }
+
+  constructor() {
+    super();
+    if (this.repo) {
+      this.checkProjectOwner();
+    }
+  }
+
+  private async checkProjectOwner() {
+    if (!this.repo) return;
+    try {
+      const access = await this.restApiService.getRepoAccessRights(this.repo);
+      this.isProjectOwner = !!access?.is_owner;
+      this.disableSaveWithoutReview =
+        !!access?.require_change_for_config_update;
+      this.showSaveForReviewButton = true;
+    } catch (e) {
+      console.error('Failed to check project owner status:', e);
+      this.isProjectOwner = false;
+    }
+  }
+
+  override render() {
+    return html`
+      <gr-list-view
+        .createNew=${this.isProjectOwner}
+        .filter=${this.filter}
+        .itemsPerPage=${this.itemsPerPage}
+        .items=${this.submitRequirements}
+        .loading=${this.loading}
+        .offset=${this.offset}
+        .path=${createRepoUrl({
+          repo: this.repo,
+          detail: RepoDetailView.SUBMIT_REQUIREMENTS,
+        })}
+        @create-clicked=${() => this.handleCreateClick()}
+      >
+        <table id="list" class="genericList">
+          <tbody>
+            <tr class="headerRow">
+              <th class="topHeader">Name</th>
+              <th class="topHeader">Description</th>
+              <th class="topHeader">Applicability Expression</th>
+              <th class="topHeader">Submittability Expression</th>
+              <th class="topHeader">Override Expression</th>
+              <th
+                class="topHeader"
+                title="Whether override is allowed in child projects"
+              >
+                Allow Override
+              </th>
+              ${when(
+                this.isProjectOwner,
+                () => html`<th class="topHeader"></th>`
+              )}
+            </tr>
+          </tbody>
+          <tbody id="submit-requirements">
+            ${when(
+              this.loading,
+              () => html`<tr id="loadingContainer">
+                <td>Loading...</td>
+              </tr>`,
+              () =>
+                html` ${(this.submitRequirements ?? []).map(
+                  item => html`
+                    <tr class="table">
+                      <td class="name">${item.name}</td>
+                      <td class="desc">${item.description}</td>
+                      <td class="applicability">
+                        ${item.applicability_expression}
+                      </td>
+                      <td class="submittability">
+                        ${item.submittability_expression}
+                      </td>
+                      <td class="override">${item.override_expression}</td>
+                      <td class="allowOverride">
+                        ${this.renderCheckmark(
+                          item.allow_override_in_child_projects
+                        )}
+                      </td>
+                      ${when(
+                        this.isProjectOwner,
+                        () => html`
+                          <td class="actions">
+                            <gr-button
+                              class="editBtn"
+                              link
+                              @click=${() => this.handleEditClick(item)}
+                            >
+                              Edit
+                            </gr-button>
+                            <gr-button
+                              class="deleteBtn"
+                              link
+                              @click=${() => this.handleDeleteClick(item)}
+                            >
+                              Delete
+                            </gr-button>
+                          </td>
+                        `
+                      )}
+                    </tr>
+                  `
+                )}`
+            )}
+          </tbody>
+        </table>
+      </gr-list-view>
+
+      ${this.renderCreateDialog()} ${this.renderDeleteDialog()}
+    `;
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('repo')) {
+      this.getSubmitRequirements();
+      this.checkProjectOwner();
+    }
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this._paramsChanged();
+    }
+  }
+
+  async _paramsChanged() {
+    const params = this.params;
+    this.loading = true;
+    this.filter = params?.filter ?? '';
+    this.offset = Number(params?.offset ?? 0);
+
+    await this.getSubmitRequirements(this.filter, this.offset);
+  }
+
+  private getSubmitRequirements(filter?: string, offset?: number) {
+    const repo = this.repo;
+    this.loading = true;
+    if (!repo) {
+      return Promise.resolve();
+    }
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    return this.restApiService
+      .getRepoSubmitRequirements(repo, errFn)
+      .then((res?: SubmitRequirementInfo[]) => {
+        if (!res) {
+          return;
+        }
+
+        this.submitRequirements = res
+          .filter(item =>
+            filter === undefined
+              ? true
+              : item.name.toLowerCase().includes(filter.toLowerCase())
+          )
+          .slice(offset ?? 0, (offset ?? 0) + this.itemsPerPage);
+        this.loading = false;
+      });
+  }
+
+  private renderCheckmark(check?: boolean) {
+    return check ? '✓' : '';
+  }
+
+  private handleCreateClick() {
+    this.isEditing = false;
+    this.newRequirement = this.getEmptyRequirement();
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.showModal();
+  }
+
+  private handleEditClick(requirement: SubmitRequirementInfo) {
+    this.isEditing = true;
+    this.requirementToEdit = requirement;
+    this.newRequirement = {
+      name: requirement.name,
+      description: requirement.description || '',
+      applicability_expression: requirement.applicability_expression || '',
+      submittability_expression: requirement.submittability_expression || '',
+      override_expression: requirement.override_expression || '',
+      allow_override_in_child_projects:
+        requirement.allow_override_in_child_projects || false,
+    };
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.showModal();
+  }
+
+  private handleCreateCancel() {
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.close();
+    this.newRequirement = this.getEmptyRequirement();
+    this.requirementToEdit = undefined;
+    this.isEditing = false;
+  }
+
+  private handleCreateConfirm() {
+    if (!this.repo) return;
+    if (
+      !this.newRequirement.name ||
+      !this.newRequirement.submittability_expression
+    ) {
+      return;
+    }
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    const promise =
+      this.isEditing && this.requirementToEdit
+        ? this.restApiService.updateSubmitRequirement(
+            this.repo,
+            this.requirementToEdit.name,
+            this.newRequirement,
+            errFn
+          )
+        : this.restApiService.createSubmitRequirement(
+            this.repo,
+            this.newRequirement,
+            errFn
+          );
+
+    promise.then(() => {
+      this.createDialog?.close();
+      this.newRequirement = this.getEmptyRequirement();
+      this.requirementToEdit = undefined;
+      this.isEditing = false;
+      this.getSubmitRequirements(this.filter, this.offset);
+    });
+  }
+
+  private getEmptyRequirement(): SubmitRequirementInput {
+    return {
+      name: '',
+      description: '',
+      applicability_expression: '',
+      submittability_expression: '',
+      override_expression: '',
+      allow_override_in_child_projects: false,
+    };
+  }
+
+  private renderCreateDialog() {
+    if (!this.isProjectOwner) return nothing;
+
+    return html`
+      <dialog id="createDialog" tabindex="-1">
+        <gr-dialog .cancelLabel=${''} .confirmLabel=${''}>
+          <div class="header" slot="header">
+            ${this.isEditing ? 'Edit' : 'Create'} Submit Requirement
+          </div>
+          <div class="main" slot="main">
+            <div class="gr-form-styles">
+              <div id="form">
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Name</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="name"
+                        class="showBlueFocusBorder"
+                        ?required=${true}
+                        ?disabled=${this.isEditing}
+                        .value=${this.newRequirement.name ?? ''}
+                        @input=${(e: InputEvent) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            name: (e.target as HTMLInputElement).value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Description</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <textarea
+                        id="description"
+                        .value=${this.newRequirement.description ?? ''}
+                        @input=${(e: InputEvent) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            description: (e.target as HTMLTextAreaElement)
+                              .value,
+                          };
+                        }}
+                        placeholder="Optional"
+                      ></textarea>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Applicability Expression</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="applicability"
+                        class="showBlueFocusBorder"
+                        placeholder="Optional"
+                        .value=${this.newRequirement.applicability_expression ??
+                        ''}
+                        @input=${(e: InputEvent) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            applicability_expression: (
+                              e.target as HTMLInputElement
+                            ).value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Submittability Expression</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="submittability"
+                        class="showBlueFocusBorder"
+                        ?required=${true}
+                        .value=${this.newRequirement
+                          .submittability_expression ?? ''}
+                        @input=${(e: InputEvent) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            submittability_expression: (
+                              e.target as HTMLInputElement
+                            ).value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Override Expression</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        id="override"
+                        class="showBlueFocusBorder"
+                        placeholder="Optional"
+                        .value=${this.newRequirement.override_expression ?? ''}
+                        @input=${(e: InputEvent) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            override_expression: (e.target as HTMLInputElement)
+                              .value,
+                          };
+                        }}
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Allow Override in Child Projects</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-checkbox
+                        id="allowOverride"
+                        ?checked=${this.newRequirement
+                          .allow_override_in_child_projects}
+                        @change=${(e: Event) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            allow_override_in_child_projects: (
+                              e.target as MdCheckbox
+                            ).checked,
+                          };
+                        }}
+                      ></md-checkbox>
+                    </span>
+                  </div>
+                </section>
+              </div>
+            </div>
+          </div>
+          <div class="footer" slot="footer">
+            <gr-button @click=${this.handleCreateCancel} link>Cancel</gr-button>
+            <gr-button
+              class="action save-button"
+              link
+              primary
+              ?disabled=${!this.newRequirement.name ||
+              !this.newRequirement.submittability_expression ||
+              this.disableSaveWithoutReview}
+              @click=${this.handleCreateConfirm}
+            >
+              ${this.isEditing ? 'Save' : 'Create'}
+            </gr-button>
+            <gr-button
+              class="action save-for-review"
+              primary
+              link
+              ?hidden=${!this.showSaveForReviewButton}
+              ?disabled=${!this.newRequirement.name ||
+              !this.newRequirement.submittability_expression}
+              @click=${this.handleSaveForReview}
+            >
+              Save for review
+            </gr-button>
+          </div>
+        </gr-dialog>
+      </dialog>
+    `;
+  }
+
+  private async handleSaveForReview(e: Event) {
+    if (!this.repo) return;
+    if (
+      !this.newRequirement.name ||
+      !this.newRequirement.submittability_expression
+    ) {
+      return;
+    }
+    const button = e.target as GrButton;
+    button.loading = true;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    const input: BatchSubmitRequirementInput = {};
+    if (this.isEditing && this.requirementToEdit) {
+      input.update = {[this.newRequirement.name]: this.newRequirement};
+    } else {
+      input.create = [this.newRequirement];
+    }
+
+    const promise = this.restApiService.saveRepoSubmitRequirementsForReview(
+      this.repo,
+      input,
+      errFn
+    );
+
+    try {
+      const change = await promise;
+      if (change) {
+        this.getNavigation().setUrl(createChangeUrl({change}));
+      }
+    } finally {
+      button.loading = false;
+      this.createDialog?.close();
+      this.newRequirement = this.getEmptyRequirement();
+      this.requirementToEdit = undefined;
+      this.isEditing = false;
+      void this.getSubmitRequirements(this.filter, this.offset);
+    }
+  }
+
+  private renderDeleteDialog() {
+    if (!this.isProjectOwner) return nothing;
+
+    return html`
+      <dialog id="deleteDialog" tabindex="-1">
+        <gr-dialog .cancelLabel=${''} .confirmLabel=${''}>
+          <div class="header" slot="header">Delete Submit Requirement</div>
+          <div class="main" slot="main">
+            Are you sure you want to delete the submit requirement
+            "${this.requirementToDelete?.name}"?
+          </div>
+          <div class="footer" slot="footer">
+            <gr-button link @click=${this.handleDeleteCancel}>Cancel</gr-button>
+            <gr-button
+              class="action"
+              link
+              ?disabled=${this.disableSaveWithoutReview}
+              @click=${this.handleDeleteConfirm}
+            >
+              Delete
+            </gr-button>
+            <gr-button
+              class="action"
+              primary
+              link
+              ?hidden=${!this.showSaveForReviewButton}
+              @click=${this.handleDeleteForReviewConfirm}
+            >
+              Delete for review
+            </gr-button>
+          </div>
+        </gr-dialog>
+      </dialog>
+    `;
+  }
+
+  private handleDeleteClick(requirement: SubmitRequirementInfo) {
+    this.requirementToDelete = requirement;
+    assertIsDefined(this.deleteDialog, 'deleteDialog');
+    this.deleteDialog.showModal();
+  }
+
+  private handleDeleteCancel() {
+    assertIsDefined(this.deleteDialog, 'deleteDialog');
+    this.deleteDialog.close();
+    this.requirementToDelete = undefined;
+  }
+
+  private async handleDeleteForReviewConfirm() {
+    if (!this.repo || !this.requirementToDelete) return;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    const input: BatchSubmitRequirementInput = {
+      delete: [this.requirementToDelete.name],
+    };
+
+    const promise = this.restApiService.saveRepoSubmitRequirementsForReview(
+      this.repo,
+      input,
+      errFn
+    );
+
+    try {
+      const change = await promise;
+      if (change) {
+        this.getNavigation().setUrl(createChangeUrl({change}));
+      }
+    } finally {
+      this.deleteDialog?.close();
+      this.requirementToDelete = undefined;
+      void this.getSubmitRequirements(this.filter, this.offset);
+    }
+  }
+
+  private handleDeleteConfirm() {
+    if (!this.repo || !this.requirementToDelete) return;
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    this.restApiService
+      .deleteSubmitRequirement(this.repo, this.requirementToDelete.name, errFn)
+      .then(() => {
+        this.deleteDialog?.close();
+        this.requirementToDelete = undefined;
+        this.getSubmitRequirements(this.filter, this.offset);
+      });
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-repo-submit-requirements': GrRepoSubmitRequirements;
+  }
+}
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
new file mode 100644
index 0000000..99d0bf2
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
@@ -0,0 +1,459 @@
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-repo-submit-requirements';
+import {GrRepoSubmitRequirements} from './gr-repo-submit-requirements';
+import {
+  queryAndAssert,
+  stubRestApi,
+  waitEventLoop,
+} from '../../../test/test-utils';
+import {RepoName, SubmitRequirementInfo} from '../../../types/common';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrButton} from '../../shared/gr-button/gr-button';
+
+suite('gr-repo-submit-requirements tests', () => {
+  let element: GrRepoSubmitRequirements;
+
+  setup(async () => {
+    element = await fixture(
+      html`<gr-repo-submit-requirements></gr-repo-submit-requirements>`
+    );
+  });
+
+  suite('submit requirements table', () => {
+    setup(() => {
+      stubRestApi('getRepoSubmitRequirements').returns(
+        Promise.resolve([
+          {
+            name: 'Verified',
+            description: 'CI result status for build and tests is passing',
+            submittability_expression:
+              'label:Verified=MAX AND -label:Verified=MIN',
+          },
+        ] as SubmitRequirementInfo[])
+      );
+      element.repo = 'test' as RepoName;
+    });
+
+    test('render loading', () => {
+      element.repo = 'test2' as RepoName;
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `<gr-list-view>
+          <table class="genericList" id="list">
+            <tbody>
+              <tr class="headerRow">
+                <th class="topHeader">Name</th>
+                <th class="topHeader">Description</th>
+                <th class="topHeader">Applicability Expression</th>
+                <th class="topHeader">Submittability Expression</th>
+                <th class="topHeader">Override Expression</th>
+                <th
+                  class="topHeader"
+                  title="Whether override is allowed in child projects"
+                >
+                  Allow Override
+                </th>
+              </tr>
+            </tbody>
+            <tbody id="submit-requirements">
+              <tr id="loadingContainer">
+                <td>Loading...</td>
+              </tr>
+            </tbody>
+          </table>
+        </gr-list-view>`
+      );
+    });
+
+    test('render', async () => {
+      await waitEventLoop();
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody>
+                <tr class="headerRow">
+                  <th class="topHeader">Name</th>
+                  <th class="topHeader">Description</th>
+                  <th class="topHeader">Applicability Expression</th>
+                  <th class="topHeader">Submittability Expression</th>
+                  <th class="topHeader">Override Expression</th>
+                  <th
+                    class="topHeader"
+                    title="Whether override is allowed in child projects"
+                  >
+                    Allow Override
+                  </th>
+                </tr>
+              </tbody>
+              <tbody id="submit-requirements">
+                <tr class="table">
+                  <td class="name">Verified</td>
+                  <td class="desc">
+                    CI result status for build and tests is passing
+                  </td>
+                  <td class="applicability"></td>
+                  <td class="submittability">
+                    label:Verified=MAX AND -label:Verified=MIN
+                  </td>
+                  <td class="override"></td>
+                  <td class="allowOverride"></td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
+        `
+      );
+    });
+
+    test('render as admin', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      await element.updateComplete;
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody>
+                <tr class="headerRow">
+                  <th class="topHeader">Name</th>
+                  <th class="topHeader">Description</th>
+                  <th class="topHeader">Applicability Expression</th>
+                  <th class="topHeader">Submittability Expression</th>
+                  <th class="topHeader">Override Expression</th>
+                  <th
+                    class="topHeader"
+                    title="Whether override is allowed in child projects"
+                  >
+                    Allow Override
+                  </th>
+                  <th class="topHeader"></th>
+                </tr>
+              </tbody>
+              <tbody id="submit-requirements">
+                <tr class="table">
+                  <td class="name">Verified</td>
+                  <td class="desc">
+                    CI result status for build and tests is passing
+                  </td>
+                  <td class="applicability"></td>
+                  <td class="submittability">
+                    label:Verified=MAX AND -label:Verified=MIN
+                  </td>
+                  <td class="override"></td>
+                  <td class="allowOverride"></td>
+                  <td class="actions">
+                  <gr-button
+                      aria-disabled="false"
+                      class="editBtn"
+                      link=""
+                      role="button"
+                      tabindex="0"
+                    >
+                      Edit
+                    </gr-button>
+                    <gr-button
+                      aria-disabled="false"
+                      class="deleteBtn"
+                      link=""
+                      role="button"
+                      tabindex="0"
+                    >
+                      Delete
+                    </gr-button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
+          <dialog id="createDialog" tabindex="-1">
+            <gr-dialog>
+              <div class="header" slot="header">Create Submit Requirement</div>
+              <div class="main" slot="main">
+                <div class="gr-form-styles">
+              <div id="form">
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Name
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="name"
+                        inputmode=""
+                        required=""
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Description
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <textarea
+                        id="description"
+                        placeholder="Optional"
+                      >
+                      </textarea>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Applicability Expression
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="applicability"
+                        inputmode=""
+                        placeholder="Optional"
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Submittability Expression
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="submittability"
+                        inputmode=""
+                        required=""
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Override Expression
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-outlined-text-field
+                        autocomplete=""
+                        class="showBlueFocusBorder"
+                        id="override"
+                        inputmode=""
+                        placeholder="Optional"
+                        type="text"
+                      >
+                      </md-outlined-text-field>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Allow Override in Child Projects
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <md-checkbox id="allowOverride">
+                      </md-checkbox>
+                    </span>
+                  </div>
+                </section>
+              </div>
+            </div>
+          </div>
+          <div class="footer" slot="footer">
+            <gr-button
+              aria-disabled="false"
+              role="button"
+              tabindex="0"
+              link=""
+            >
+              Cancel
+            </gr-button>
+            <gr-button
+              aria-disabled="true"
+              class="action save-button"
+              disabled=""
+              link=""
+              primary=""
+              role="button"
+              tabindex="-1"
+            >
+              Create
+            </gr-button>
+            <gr-button
+              aria-disabled="true"
+              class="action save-for-review"
+              disabled=""
+              link=""
+              primary=""
+              role="button"
+              tabindex="-1"
+            >
+              Save for review
+            </gr-button>
+          </div>
+        </gr-dialog>
+      </dialog>
+      <dialog
+        id="deleteDialog"
+        tabindex="-1"
+      >
+        <gr-dialog>
+          <div
+            class="header"
+            slot="header"
+          >
+            Delete Submit Requirement
+          </div>
+          <div
+            class="main"
+            slot="main"
+          >
+          Are you sure you want to delete the submit requirement
+            ""?
+          </div>
+          <div class="footer" slot="footer">
+            <gr-button
+              aria-disabled="false"
+              role="button"
+              tabindex="0"
+              link=""
+            >
+              Cancel
+            </gr-button>
+            <gr-button
+              aria-disabled="false"
+              class="action"
+              link=""
+              role="button"
+              tabindex="0"
+            >
+              Delete
+            </gr-button>
+            <gr-button
+              aria-disabled="false"
+              class="action"
+              link=""
+              primary=""
+              role="button"
+              tabindex="0"
+            >
+              Delete for review
+            </-button>
+          </div>
+        </gr-dialog>
+      </dialog>
+    `
+      );
+    });
+
+    test('open edit dialog', async () => {
+      await waitEventLoop();
+      element.isProjectOwner = true;
+      await element.updateComplete;
+
+      const editButton = queryAndAssert<GrButton>(element, '.editBtn');
+      editButton.click();
+      await element.updateComplete;
+
+      // Verify dialog is open and has correct title
+      const dialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      assert.isTrue(dialog.open);
+
+      const dialogTitle = queryAndAssert<HTMLElement>(element, '.header');
+      assert.equal(dialogTitle.textContent?.trim(), 'Edit Submit Requirement');
+
+      // Verify form is populated with correct data
+      const nameInput = queryAndAssert<HTMLInputElement>(element, '#name');
+      assert.equal(nameInput.value, 'Verified');
+      assert.isTrue(nameInput.disabled);
+
+      const descriptionTextarea = queryAndAssert<HTMLTextAreaElement>(
+        element,
+        '#description'
+      );
+      assert.equal(
+        descriptionTextarea.value,
+        'CI result status for build and tests is passing'
+      );
+      assert.isFalse(descriptionTextarea.disabled);
+
+      const submittabilityInput = queryAndAssert<HTMLInputElement>(
+        element,
+        '#submittability'
+      );
+      assert.equal(
+        submittabilityInput.value,
+        'label:Verified=MAX AND -label:Verified=MIN'
+      );
+
+      // Verify save button is enabled
+      const saveButton = queryAndAssert<HTMLElement>(
+        element,
+        'gr-button.save-button'
+      );
+      assert.isFalse(saveButton.hasAttribute('disabled'));
+    });
+  });
+
+  suite('admin', () => {
+    setup(async () => {
+      element.isProjectOwner = true;
+      await element.updateComplete;
+    });
+
+    test('save button is disabled when require_change_for_config_update is set', async () => {
+      element.disableSaveWithoutReview = true;
+      element.newRequirement.name = 'Test';
+      element.newRequirement.submittability_expression = 'test';
+      await element.updateComplete;
+
+      const dialog = queryAndAssert<HTMLDialogElement>(
+        element,
+        '#createDialog'
+      );
+      dialog.showModal();
+      await element.updateComplete;
+
+      const saveButton = queryAndAssert<GrButton>(
+        element,
+        'gr-dialog gr-button.action.save-button'
+      );
+      assert.isTrue(saveButton.disabled);
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index 8830aff..bc48aac 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -3,24 +3,21 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-download-commands/gr-download-commands';
-import '../../shared/gr-select/gr-select';
-import '../../shared/gr-suggestion-textarea/gr-suggestion-textarea';
 import '../gr-repo-plugin-config/gr-repo-plugin-config';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   ConfigInfo,
-  RepoName,
-  InheritedBooleanInfo,
-  SchemesInfoMap,
   ConfigInput,
+  DownloadSchemeInfo,
+  InheritedBooleanInfo,
   MaxObjectSizeLimitInfo,
   PluginParameterToConfigParameterInfoMap,
-  DownloadSchemeInfo,
+  RepoName,
+  SchemesInfoMap,
   ServerInfo,
 } from '../../../types/common';
 import {
@@ -39,7 +36,7 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {BindValueChangeEvent} from '../../../types/events';
 import {deepClone} from '../../../utils/deep-util';
-import {LitElement, PropertyValues, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {createChangeUrl} from '../../../models/views/change';
 import {when} from 'lit/directives/when.js';
@@ -49,8 +46,15 @@
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {computeMainCodeBrowserWeblink} from '../../../utils/weblink-util';
+import {Command} from '../../shared/gr-download-commands/gr-download-commands';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {formStyles} from '../../../styles/form-styles';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
 
 const STATES = {
   active: {value: RepoState.ACTIVE, label: 'Active'},
@@ -108,8 +112,6 @@
   // private but used in test
   @state() readOnly = true;
 
-  @state() showSaveForReviewButton = false;
-
   // private but used in test
   @state() disableSaveWithoutReview = true;
 
@@ -128,8 +130,6 @@
 
   @state() private pluginConfigChanged = false;
 
-  private readonly flagsService = getAppContext().flagsService;
-
   private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly restApiService = getAppContext().restApiService;
@@ -165,7 +165,9 @@
 
   static override get styles() {
     return [
+      materialStyles,
       fontStyles,
+      formStyles,
       grFormStyles,
       subpageStyles,
       sharedStyles,
@@ -183,6 +185,13 @@
         #options .repositorySettings.showConfig {
           display: block;
         }
+        gr-autogrow-textarea {
+          background-color: var(--view-background-color);
+          width: 100%;
+        }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
       `,
     ];
   }
@@ -195,7 +204,7 @@
           <h1 id="Title" class="heading-1">${this.repo}</h1>
           <hr />
           <div>
-            <a href=${this.getWebLink()?.url}
+            <a href=${this.getWebLink()?.url ?? ''}
               ><gr-button link ?disabled=${!this.getWebLink()?.url}
                 >Browse</gr-button
               ></a
@@ -230,7 +239,6 @@
                 <gr-button
                   id="saveReviewBtn"
                   ?disabled=${this.readOnly || !configChanged}
-                  ?hidden=${!this.showSaveForReviewButton}
                   @click=${this.handleSaveRepoConfigForReview}
                   >Save For Review</gr-button
                 >
@@ -279,17 +287,17 @@
     return html`
       <h3 id="Description" class="heading-3">Description</h3>
       <fieldset>
-        <gr-suggestion-textarea
+        <gr-autogrow-textarea
           id="descriptionInput"
           class="description"
           autocomplete="on"
           placeholder="&lt;Insert repo description here&gt;"
-          rows="4"
-          monospace
+          .rows=${4}
+          .maxRows=${10}
+          .value=${this.repoConfig.description ?? ''}
           ?disabled=${this.readOnly}
-          .text=${this.repoConfig.description ?? ''}
-          @text-changed=${this.handleDescriptionTextChanged}
-        ></gr-suggestion-textarea>
+          @input=${this.handleDescriptionInput}
+        ></gr-autogrow-textarea>
       </fieldset>
     `;
   }
@@ -319,19 +327,20 @@
       <section>
         <span class="title">State</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="stateSelect"
-            .bindValue=${this.repoConfig?.state}
-            @bind-value-changed=${this.handleStateSelectBindValueChanged}
+            value=${this.repoConfig?.state ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleStateSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.states.map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.states.map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -342,19 +351,20 @@
       <section>
         <span class="title">Submit type</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="submitTypeSelect"
-            .bindValue=${this.repoConfig?.submit_type}
-            @bind-value-changed=${this.handleSubmitTypeSelectBindValueChanged}
+            value=${this.repoConfig?.submit_type ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleSubmitTypeSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatSubmitTypeSelect(this.repoConfig).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatSubmitTypeSelect(this.repoConfig).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -365,21 +375,20 @@
       <section>
         <span class="title">Allow content merges</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="contentMergeSelect"
-            .bindValue=${this.repoConfig?.use_content_merge?.configured_value}
-            @bind-value-changed=${this.handleContentMergeSelectBindValueChanged}
+            value=${this.repoConfig?.use_content_merge?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleContentMergeSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.use_content_merge
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(this.repoConfig?.use_content_merge).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -392,22 +401,23 @@
           Create a new change for every commit not in the target branch
         </span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="newChangeSelect"
-            .bindValue=${this.repoConfig
-              ?.create_new_change_for_all_not_in_target?.configured_value}
-            @bind-value-changed=${this.handleNewChangeSelectBindValueChanged}
+            value=${this.repoConfig?.create_new_change_for_all_not_in_target
+              ?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleNewChangeSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.create_new_change_for_all_not_in_target
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.create_new_change_for_all_not_in_target
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -418,22 +428,20 @@
       <section>
         <span class="title">Require Change-Id in commit message</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="requireChangeIdSelect"
-            .bindValue=${this.repoConfig?.require_change_id?.configured_value}
-            @bind-value-changed=${this
-              .handleRequireChangeIdSelectBindValueChanged}
+            value=${this.repoConfig?.require_change_id?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleRequireChangeIdSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.require_change_id
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(this.repoConfig?.require_change_id).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -449,21 +457,20 @@
       >
         <span class="title">Enable signed push</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="enableSignedPush"
-            .bindValue=${this.repoConfig?.enable_signed_push?.configured_value}
-            @bind-value-changed=${this.handleEnableSignedPushBindValueChanged}
+            value=${this.repoConfig?.enable_signed_push?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleEnableSignedPushChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.enable_signed_push
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(this.repoConfig?.enable_signed_push).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -479,21 +486,23 @@
       >
         <span class="title">Require signed push</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="requireSignedPush"
-            .bindValue=${this.repoConfig?.require_signed_push?.configured_value}
-            @bind-value-changed=${this.handleRequireSignedPushBindValueChanged}
+            value=${this.repoConfig?.require_signed_push?.configured_value ??
+            ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleRequireSignedPushChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.require_signed_push
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.require_signed_push
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -506,23 +515,23 @@
           Reject implicit merges when changes are pushed for review</span
         >
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="rejectImplicitMergesSelect"
-            .bindValue=${this.repoConfig?.reject_implicit_merges
-              ?.configured_value}
-            @bind-value-changed=${this
-              .handleRejectImplicitMergeSelectBindValueChanged}
+            value=${this.repoConfig?.reject_implicit_merges?.configured_value ??
+            ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleRejectImplicitMergeSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.reject_implicit_merges
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.reject_implicit_merges
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -535,23 +544,23 @@
           Enable adding unregistered users as reviewers and CCs on changes</span
         >
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="unRegisteredCcSelect"
-            .bindValue=${this.repoConfig?.enable_reviewer_by_email
-              ?.configured_value}
-            @bind-value-changed=${this
-              .handleUnRegisteredCcSelectBindValueChanged}
+            value=${this.repoConfig?.enable_reviewer_by_email
+              ?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleUnRegisteredCcSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.enable_reviewer_by_email
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.enable_reviewer_by_email
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -562,22 +571,20 @@
       <section>
         <span class="title"> Set all new changes private by default</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="setAllnewChangesPrivateByDefaultSelect"
-            .bindValue=${this.repoConfig?.private_by_default?.configured_value}
-            @bind-value-changed=${this
-              .handleSetAllNewChangesPrivateByDefaultSelectBindValueChanged}
+            value=${this.repoConfig?.private_by_default?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleSetAllNewChangesPrivateByDefaultSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.private_by_default
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(this.repoConfig?.private_by_default).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -590,23 +597,24 @@
           Set new changes to "work in progress" by default</span
         >
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="setAllNewChangesWorkInProgressByDefaultSelect"
-            .bindValue=${this.repoConfig?.work_in_progress_by_default
-              ?.configured_value}
-            @bind-value-changed=${this
-              .handleSetAllNewChangesWorkInProgressByDefaultSelectBindValueChanged}
+            value=${this.repoConfig?.work_in_progress_by_default
+              ?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this
+              .handleSetAllNewChangesWorkInProgressByDefaultSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.work_in_progress_by_default
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.work_in_progress_by_default
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -617,18 +625,17 @@
       <section>
         <span class="title">Maximum Git object size limit</span>
         <span class="value">
-          <iron-input
-            id="maxGitObjSizeIronInput"
-            .bindValue=${this.repoConfig?.max_object_size_limit
-              ?.configured_value}
-            @bind-value-changed=${this.handleMaxGitObjSizeBindValueChanged}
+          <md-outlined-text-field
+            id="maxGitObjSizeInput"
+            class="showBlueFocusBorder"
+            type="number"
+            min="0"
+            ?disabled=${this.readOnly}
+            .value=${this.repoConfig?.max_object_size_limit?.configured_value ??
+            ''}
+            @input=${this.handleMaxGitObjSizeInputChanged}
           >
-            <input
-              id="maxGitObjSizeInput"
-              type="text"
-              ?disabled=${this.readOnly}
-            />
-          </iron-input>
+          </md-outlined-text-field>
           ${this.repoConfig?.max_object_size_limit?.value
             ? `effective: ${this.repoConfig.max_object_size_limit.value} bytes`
             : ''}
@@ -644,23 +651,23 @@
           >Match authored date with committer date upon submit</span
         >
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="matchAuthoredDateWithCommitterDateSelect"
-            .bindValue=${this.repoConfig?.match_author_to_committer_date
-              ?.configured_value}
-            @bind-value-changed=${this
-              .handleMatchAuthoredDateWithCommitterDateSelectBindValueChanged}
+            value=${this.repoConfig?.match_author_to_committer_date
+              ?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleMatchAuthoredDateWithCommitterDateSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.match_author_to_committer_date
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.match_author_to_committer_date
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -671,22 +678,23 @@
       <section>
         <span class="title">Reject empty commit upon submit</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="rejectEmptyCommitSelect"
-            .bindValue=${this.repoConfig?.reject_empty_commit?.configured_value}
-            @bind-value-changed=${this
-              .handleRejectEmptyCommitSelectBindValueChanged}
+            value=${this.repoConfig?.reject_empty_commit?.configured_value ??
+            ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleRejectEmptyCommitSelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.reject_empty_commit
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.reject_empty_commit
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -699,23 +707,23 @@
           Require a valid contributor agreement to upload</span
         >
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="contributorAgreementSelect"
-            .bindValue=${this.repoConfig?.use_contributor_agreements
-              ?.configured_value}
-            @bind-value-changed=${this
-              .handleUseContributorAgreementsBindValueChanged}
+            value=${this.repoConfig?.use_contributor_agreements
+              ?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleUseContributorAgreementsChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.use_contributor_agreements
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(
+              this.repoConfig?.use_contributor_agreements
+            ).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -726,22 +734,20 @@
       <section>
         <span class="title">Require Signed-off-by in commit message</span>
         <span class="value">
-          <gr-select
+          <md-outlined-select
             id="useSignedOffBySelect"
-            .bindValue=${this.repoConfig?.use_signed_off_by?.configured_value}
-            @bind-value-changed=${this
-              .handleUseSignedOffBySelectBindValueChanged}
+            value=${this.repoConfig?.use_signed_off_by?.configured_value ?? ''}
+            ?disabled=${this.readOnly}
+            @change=${this.handleUseSignedOffBySelectChange}
           >
-            <select ?disabled=${this.readOnly}>
-              ${this.formatBooleanSelect(
-                this.repoConfig?.use_signed_off_by
-              ).map(
-                item => html`
-                  <option value=${item.value}>${item.label}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.formatBooleanSelect(this.repoConfig?.use_signed_off_by).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.label}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </span>
       </section>
     `;
@@ -816,9 +822,6 @@
 
             // If the user is not an owner, is_owner is not a property.
             this.readOnly = !access[repo]?.is_owner;
-            this.showSaveForReviewButton = this.flagsService.isEnabled(
-              KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
-            );
             this.disableSaveWithoutReview =
               !!access[repo]?.require_change_for_config_update;
           });
@@ -1154,9 +1157,9 @@
     return schemeInfo?.description;
   }
 
-  private computeCommands() {
+  private computeCommands(): Command[] {
     const schemeInfo = this.getSchemeInfo();
-    if (!this.repo || !schemeInfo) return undefined;
+    if (!this.repo || !schemeInfo) return [];
 
     const commandObj = schemeInfo.clone_commands ?? {};
     const commands = [];
@@ -1195,148 +1198,153 @@
     }
   }
 
-  private handleDescriptionTextChanged(e: BindValueChangeEvent) {
+  private handleDescriptionInput(e: InputEvent) {
     if (!this.repoConfig || this.loading) return;
-    if (this.repoConfig.description === e.detail.value) return;
+    const value = (e.target as GrAutogrowTextarea).value ?? '';
+    if (this.repoConfig.description === value) return;
     this.repoConfig = {
       ...this.repoConfig,
-      description: e.detail.value,
+      description: value,
     };
     this.requestUpdate();
   }
 
-  private handleStateSelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleStateSelectChange(e: Event) {
     if (!this.repoConfig || this.loading) return;
-    if (this.repoConfig.state === e.detail.value) return;
+    const select = e.target as HTMLSelectElement;
+    if (this.repoConfig.state === select.value) return;
     this.repoConfig = {
       ...this.repoConfig,
-      state: e.detail.value as RepoState,
+      state: select.value as RepoState,
     };
     this.requestUpdate();
   }
 
-  private handleSubmitTypeSelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleSubmitTypeSelectChange(e: Event) {
     if (!this.repoConfig || this.loading) return;
-    if (this.repoConfig.submit_type === e.detail.value) return;
+    const select = e.target as HTMLSelectElement;
+    if (this.repoConfig.submit_type === select.value) return;
     this.repoConfig = {
       ...this.repoConfig,
-      submit_type: e.detail.value as SubmitType,
+      submit_type: select.value as SubmitType,
     };
     this.requestUpdate();
   }
 
-  private handleContentMergeSelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleContentMergeSelectChange(e: Event) {
     if (!this.repoConfig?.use_content_merge || this.loading) return;
-    this.repoConfig.use_content_merge.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.use_content_merge.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleNewChangeSelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleNewChangeSelectChange(e: Event) {
     if (
       !this.repoConfig?.create_new_change_for_all_not_in_target ||
       this.loading
     )
       return;
-    this.repoConfig.create_new_change_for_all_not_in_target.configured_value = e
-      .detail.value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.create_new_change_for_all_not_in_target.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleRequireChangeIdSelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleRequireChangeIdSelectChange(e: Event) {
     if (!this.repoConfig?.require_change_id || this.loading) return;
-    this.repoConfig.require_change_id.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.require_change_id.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleEnableSignedPushBindValueChanged(e: BindValueChangeEvent) {
+  private handleEnableSignedPushChange(e: Event) {
     if (!this.repoConfig?.enable_signed_push || this.loading) return;
-    this.repoConfig.enable_signed_push.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.enable_signed_push.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleRequireSignedPushBindValueChanged(e: BindValueChangeEvent) {
+  private handleRequireSignedPushChange(e: Event) {
     if (!this.repoConfig?.require_signed_push || this.loading) return;
-    this.repoConfig.require_signed_push.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.require_signed_push.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleRejectImplicitMergeSelectBindValueChanged(
-    e: BindValueChangeEvent
-  ) {
+  private handleRejectImplicitMergeSelectChange(e: Event) {
     if (!this.repoConfig?.reject_implicit_merges || this.loading) return;
-    this.repoConfig.reject_implicit_merges.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.reject_implicit_merges.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleUnRegisteredCcSelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleUnRegisteredCcSelectChange(e: Event) {
     if (!this.repoConfig?.enable_reviewer_by_email || this.loading) return;
-    this.repoConfig.enable_reviewer_by_email.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.enable_reviewer_by_email.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleSetAllNewChangesPrivateByDefaultSelectBindValueChanged(
-    e: BindValueChangeEvent
-  ) {
+  private handleSetAllNewChangesPrivateByDefaultSelectChange(e: Event) {
     if (!this.repoConfig?.private_by_default || this.loading) return;
-    this.repoConfig.private_by_default.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.private_by_default.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleSetAllNewChangesWorkInProgressByDefaultSelectBindValueChanged(
-    e: BindValueChangeEvent
-  ) {
+  private handleSetAllNewChangesWorkInProgressByDefaultSelectChange(e: Event) {
     if (!this.repoConfig?.work_in_progress_by_default || this.loading) return;
-    this.repoConfig.work_in_progress_by_default.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.work_in_progress_by_default.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleMaxGitObjSizeBindValueChanged(e: BindValueChangeEvent) {
+  private handleMaxGitObjSizeInputChanged(e: InputEvent) {
     if (!this.repoConfig?.max_object_size_limit || this.loading) return;
-    this.repoConfig.max_object_size_limit.value = e.detail.value;
-    this.repoConfig.max_object_size_limit.configured_value = e.detail.value;
+    const target = e.target as HTMLInputElement;
+    this.repoConfig.max_object_size_limit.value = target.value;
+    this.repoConfig.max_object_size_limit.configured_value = target.value;
     this.requestUpdate();
   }
 
-  private handleMatchAuthoredDateWithCommitterDateSelectBindValueChanged(
-    e: BindValueChangeEvent
-  ) {
+  private handleMatchAuthoredDateWithCommitterDateSelectChange(e: Event) {
     if (!this.repoConfig?.match_author_to_committer_date || this.loading)
       return;
-    this.repoConfig.match_author_to_committer_date.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.match_author_to_committer_date.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleRejectEmptyCommitSelectBindValueChanged(
-    e: BindValueChangeEvent
-  ) {
+  private handleRejectEmptyCommitSelectChange(e: Event) {
     if (!this.repoConfig?.reject_empty_commit || this.loading) return;
-    this.repoConfig.reject_empty_commit.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.reject_empty_commit.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleUseContributorAgreementsBindValueChanged(
-    e: BindValueChangeEvent
-  ) {
+  private handleUseContributorAgreementsChange(e: Event) {
     if (!this.repoConfig?.use_contributor_agreements || this.loading) return;
-    this.repoConfig.use_contributor_agreements.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.use_contributor_agreements.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 
-  private handleUseSignedOffBySelectBindValueChanged(e: BindValueChangeEvent) {
+  private handleUseSignedOffBySelectChange(e: Event) {
     if (!this.repoConfig?.use_signed_off_by || this.loading) return;
-    this.repoConfig.use_signed_off_by.configured_value = e.detail
-      .value as InheritedBooleanInfoConfiguredValue;
+    const select = e.target as HTMLSelectElement;
+    this.repoConfig.use_signed_off_by.configured_value =
+      select.value as InheritedBooleanInfoConfiguredValue;
     this.requestUpdate();
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
index b347e86..bb9c1ab 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
@@ -47,13 +47,11 @@
 } from '../../../test/test-data-generators';
 import {PageErrorEvent} from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {GrSelect} from '../../shared/gr-select/gr-select';
-import {GrSuggestionTextarea} from '../../shared/gr-suggestion-textarea/gr-suggestion-textarea';
-import {IronInputElement} from '@polymer/iron-input/iron-input';
-import {fixture, html, assert} from '@open-wc/testing';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
+import {assert, fixture, html} from '@open-wc/testing';
 import {ChangeInfo} from '../../../api/rest-api';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {MdOutlinedSelect} from '@material/web/select/outlined-select';
 
 suite('gr-repo tests', () => {
   let element: GrRepo;
@@ -152,7 +150,7 @@
 
   function getFormFields() {
     const selects = Array.from(queryAll(element, 'select'));
-    const textareas = Array.from(queryAll(element, 'iron-autogrow-textarea'));
+    const textareas = Array.from(queryAll(element, 'gr-autogrow-textarea'));
     const inputs = Array.from(queryAll(element, 'input'));
     return inputs.concat(textareas).concat(selects);
   }
@@ -174,242 +172,767 @@
     assert.shadowDom.equal(
       element,
       /* prettier-ignore */ /* HTML */ `
-      <div class="gr-form-styles main read-only">
-        <div class="info">
-          <h1 class="heading-1" id="Title">test-repo</h1>
-          <hr />
-          <div>
-            <a href="">
-              <gr-button
-                aria-disabled="true"
-                disabled=""
-                link=""
-                role="button"
-                tabindex="-1"
-              >
-                Browse
-              </gr-button>
-            </a>
-            <a href="/q/project:test-repo">
-              <gr-button
-                aria-disabled="false"
-                link=""
-                role="button"
-                tabindex="0"
-              >
-                View Changes
-              </gr-button>
-            </a>
-          </div>
-        </div>
-        <div id="loadedContent">
-          <h2 class="heading-2" id="configurations">Configurations</h2>
-          <div id="form">
-            <fieldset>
-              <h3 class="heading-3" id="Description">Description</h3>
-              <fieldset>
-                <gr-suggestion-textarea
-                  autocomplete="on"
-                  class="description monospace"
-                  disabled=""
-                  id="descriptionInput"
-                  monospace=""
-                  placeholder="<Insert repo description here>"
-                  rows="4"
-                >
-                </gr-suggestion-textarea>
-              </fieldset>
-              <h3 class="heading-3" id="Options">Repository Options</h3>
-              <fieldset id="options">
-                <section>
-                  <span class="title"> State </span>
-                  <span class="value">
-                    <gr-select id="stateSelect">
-                      <select disabled="">
-                        <option value="ACTIVE">Active</option>
-                        <option value="READ_ONLY">Read Only</option>
-                        <option value="HIDDEN">Hidden</option>
-                      </select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title"> Submit type </span>
-                  <span class="value">
-                    <gr-select id="submitTypeSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title"> Allow content merges </span>
-                  <span class="value">
-                    <gr-select id="contentMergeSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Create a new change for every commit not in the target branch
-                  </span>
-                  <span class="value">
-                    <gr-select id="newChangeSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Require Change-Id in commit message
-                  </span>
-                  <span class="value">
-                    <gr-select id="requireChangeIdSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section
-                  class="repositorySettings showConfig"
-                  id="enableSignedPushSettings"
-                >
-                  <span class="title"> Enable signed push </span>
-                  <span class="value">
-                    <gr-select id="enableSignedPush">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section
-                  class="repositorySettings showConfig"
-                  id="requireSignedPushSettings"
-                >
-                  <span class="title"> Require signed push </span>
-                  <span class="value">
-                    <gr-select id="requireSignedPush">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Reject implicit merges when changes are pushed for review
-                  </span>
-                  <span class="value">
-                    <gr-select id="rejectImplicitMergesSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Enable adding unregistered users as reviewers and CCs on changes
-                  </span>
-                  <span class="value">
-                    <gr-select id="unRegisteredCcSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Set all new changes private by default
-                  </span>
-                  <span class="value">
-                    <gr-select id="setAllnewChangesPrivateByDefaultSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Set new changes to "work in progress" by default
-                  </span>
-                  <span class="value">
-                    <gr-select
-                      id="setAllNewChangesWorkInProgressByDefaultSelect"
-                    >
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title"> Maximum Git object size limit </span>
-                  <span class="value">
-                    <iron-input id="maxGitObjSizeIronInput">
-                      <input disabled="" id="maxGitObjSizeInput" type="text" />
-                    </iron-input>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Match authored date with committer date upon submit
-                  </span>
-                  <span class="value">
-                    <gr-select id="matchAuthoredDateWithCommitterDateSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title"> Reject empty commit upon submit </span>
-                  <span class="value">
-                    <gr-select id="rejectEmptyCommitSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-              </fieldset>
-              <h3 class="heading-3" id="Options">Contributor Agreements</h3>
-              <fieldset id="agreements">
-                <section>
-                  <span class="title">
-                    Require a valid contributor agreement to upload
-                  </span>
-                  <span class="value">
-                    <gr-select id="contributorAgreementSelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-                <section>
-                  <span class="title">
-                    Require Signed-off-by in commit message
-                  </span>
-                  <span class="value">
-                    <gr-select id="useSignedOffBySelect">
-                      <select disabled=""></select>
-                    </gr-select>
-                  </span>
-                </section>
-              </fieldset>
-              <gr-button
-                id="saveBtn"
-                aria-disabled="true"
-                disabled=""
-                role="button"
-                tabindex="-1"
-              >
-                Save Changes
-              </gr-button>
-              <gr-button
-                id="saveReviewBtn"
-                aria-disabled="true"
-                disabled=""
-                hidden=""
-                role="button"
-                tabindex="-1"
-              >
-                Save For Review
-              </gr-button>
-            </fieldset>
-            <gr-endpoint-decorator name="repo-config">
-              <gr-endpoint-param name="repoName"> </gr-endpoint-param>
-              <gr-endpoint-param name="readOnly"> </gr-endpoint-param>
-            </gr-endpoint-decorator>
-          </div>
-        </div>
-      </div>
+       <div class="gr-form-styles main read-only">
+         <div class="info">
+           <h1
+             class="heading-1"
+             id="Title"
+           >
+             test-repo
+           </h1>
+           <hr>
+           <div>
+             <a href="">
+               <gr-button
+                 aria-disabled="true"
+                 disabled=""
+                 link=""
+                 role="button"
+                 tabindex="-1"
+               >
+                 Browse
+               </gr-button>
+             </a>
+             <a href="/q/project:test-repo">
+               <gr-button
+                 aria-disabled="false"
+                 link=""
+                 role="button"
+                 tabindex="0"
+               >
+                 View Changes
+               </gr-button>
+             </a>
+           </div>
+         </div>
+         <div id="loadedContent">
+           <h2
+             class="heading-2"
+             id="configurations"
+           >
+             Configurations
+           </h2>
+           <div id="form">
+             <fieldset>
+               <h3
+                 class="heading-3"
+                 id="Description"
+               >
+                 Description
+               </h3>
+               <fieldset>
+                 <gr-autogrow-textarea
+                   autocomplete="on"
+                   class="description"
+                   disabled=""
+                   id="descriptionInput"
+                   placeholder="<Insert repo description here>"
+                 >
+                 </gr-autogrow-textarea>
+               </fieldset>
+               <h3
+                 class="heading-3"
+                 id="Options"
+               >
+                 Repository Options
+               </h3>
+               <fieldset id="options">
+                 <section>
+                   <span class="title">
+                     State
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="stateSelect"
+                       value="ACTIVE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="ACTIVE"
+                       >
+                         <div slot="headline">
+                           Active
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="READ_ONLY"
+                       >
+                         <div slot="headline">
+                           Read Only
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="HIDDEN"
+                       >
+                         <div slot="headline">
+                           Hidden
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Submit type
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="submitTypeSelect"
+                       value="INHERIT"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit (Merge if necessary)
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="MERGE_IF_NECESSARY"
+                       >
+                         <div slot="headline">
+                           Merge if necessary
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FAST_FORWARD_ONLY"
+                       >
+                         <div slot="headline">
+                           Fast forward only
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="REBASE_ALWAYS"
+                       >
+                         <div slot="headline">
+                           Rebase Always
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="REBASE_IF_NECESSARY"
+                       >
+                         <div slot="headline">
+                           Rebase if necessary
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="MERGE_ALWAYS"
+                       >
+                         <div slot="headline">
+                           Merge always
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="CHERRY_PICK"
+                       >
+                         <div slot="headline">
+                           Cherry pick
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Allow content merges
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="contentMergeSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Create a new change for every commit not in the target branch
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="newChangeSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Require Change-Id in commit message
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="requireChangeIdSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section
+                   class="repositorySettings showConfig"
+                   id="enableSignedPushSettings"
+                 >
+                   <span class="title">
+                     Enable signed push
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="enableSignedPush"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section
+                   class="repositorySettings showConfig"
+                   id="requireSignedPushSettings"
+                 >
+                   <span class="title">
+                     Require signed push
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="requireSignedPush"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Reject implicit merges when changes are pushed for review
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="rejectImplicitMergesSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Enable adding unregistered users as reviewers and CCs on changes
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="unRegisteredCcSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Set all new changes private by default
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="setAllnewChangesPrivateByDefaultSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Set new changes to "work in progress" by default
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="setAllNewChangesWorkInProgressByDefaultSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Maximum Git object size limit
+                   </span>
+                   <span class="value">
+                     <md-outlined-text-field
+                       autocomplete=""
+                       class="showBlueFocusBorder"
+                       disabled=""
+                       id="maxGitObjSizeInput"
+                       inputmode=""
+                       min="0"
+                       type="number"
+                     >
+                     </md-outlined-text-field>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Match authored date with committer date upon submit
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="matchAuthoredDateWithCommitterDateSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Reject empty commit upon submit
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="rejectEmptyCommitSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+               </fieldset>
+               <h3
+                 class="heading-3"
+                 id="Options"
+               >
+                 Contributor Agreements
+               </h3>
+               <fieldset id="agreements">
+                 <section>
+                   <span class="title">
+                     Require a valid contributor agreement to upload
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="contributorAgreementSelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+                 <section>
+                   <span class="title">
+                     Require Signed-off-by in commit message
+                   </span>
+                   <span class="value">
+                     <md-outlined-select
+                       disabled=""
+                       id="useSignedOffBySelect"
+                       value="FALSE"
+                     >
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="0"
+                         value="INHERIT"
+                       >
+                         <div slot="headline">
+                           Inherit
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="TRUE"
+                       >
+                         <div slot="headline">
+                           True
+                         </div>
+                       </md-select-option>
+                       <md-select-option
+                         md-menu-item=""
+                         tabindex="-1"
+                         value="FALSE"
+                       >
+                         <div slot="headline">
+                           False
+                         </div>
+                       </md-select-option>
+                     </md-outlined-select>
+                   </span>
+                 </section>
+               </fieldset>
+               <gr-button
+                 aria-disabled="true"
+                 disabled=""
+                 id="saveBtn"
+                 role="button"
+                 tabindex="-1"
+               >
+                 Save Changes
+               </gr-button>
+               <gr-button
+                 aria-disabled="true"
+                 disabled=""
+                 id="saveReviewBtn"
+                 role="button"
+                 tabindex="-1"
+               >
+                 Save For Review
+               </gr-button>
+             </fieldset>
+             <gr-endpoint-decorator name="repo-config">
+               <gr-endpoint-param name="repoName">
+               </gr-endpoint-param>
+               <gr-endpoint-param name="readOnly">
+               </gr-endpoint-param>
+             </gr-endpoint-decorator>
+           </div>
+         </div>
+       </div>
     `,
       {ignoreTags: ['option']}
     );
@@ -637,7 +1160,7 @@
 
     const pageErrorFired = mockPromise();
     const response = {...new Response(), status: 404};
-    stubRestApi('getProjectConfig').callsFake((_, errFn) => {
+    stubRestApi('getProjectConfig').callsFake((_: any, errFn: any) => {
       if (errFn !== undefined) {
         errFn(response);
       }
@@ -698,19 +1221,24 @@
 
     test('state gets set correctly', async () => {
       await element.loadRepo();
+      await element.updateComplete;
       assert.equal(element.repoConfig!.state, RepoState.ACTIVE);
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#stateSelect').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#stateSelect').value,
         RepoState.ACTIVE
       );
     });
 
     test('inherited submit type value is calculated correctly', async () => {
       await element.loadRepo();
-      const sel = queryAndAssert<GrSelect>(element, '#submitTypeSelect');
-      assert.equal(sel.bindValue, 'INHERIT');
+      await element.updateComplete;
+      const sel = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#submitTypeSelect'
+      );
+      assert.equal(sel.value, 'INHERIT');
       assert.equal(
-        sel.nativeSelect.options[0].text,
+        sel.querySelector('md-select-option')!.textContent?.trim(),
         'Inherit (Merge if necessary)'
       );
     });
@@ -752,52 +1280,198 @@
           '#Title'
         ).classList.contains('edited')
       );
-      queryAndAssert<GrSuggestionTextarea>(element, '#descriptionInput').text =
-        configInputObj.description;
-      queryAndAssert<GrSelect>(element, '#stateSelect').bindValue =
-        configInputObj.state;
-      queryAndAssert<GrSelect>(element, '#submitTypeSelect').bindValue =
-        configInputObj.submit_type;
-      queryAndAssert<GrSelect>(element, '#contentMergeSelect').bindValue =
-        configInputObj.use_content_merge;
-      queryAndAssert<GrSelect>(element, '#newChangeSelect').bindValue =
+      const descriptionInput = queryAndAssert<GrAutogrowTextarea>(
+        element,
+        '#descriptionInput'
+      );
+      descriptionInput.value = configInputObj.description;
+      descriptionInput.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
+      const stateSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#stateSelect'
+      );
+      stateSelect.value = configInputObj.state;
+      stateSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const submitTypeSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#submitTypeSelect'
+      );
+      submitTypeSelect.value = configInputObj.submit_type;
+      submitTypeSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const contentMergeSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#contentMergeSelect'
+      );
+      contentMergeSelect.value = configInputObj.use_content_merge;
+      contentMergeSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const newChangeSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#newChangeSelect'
+      );
+      newChangeSelect.value =
         configInputObj.create_new_change_for_all_not_in_target;
-      queryAndAssert<GrSelect>(element, '#requireChangeIdSelect').bindValue =
-        configInputObj.require_change_id;
-      queryAndAssert<GrSelect>(element, '#enableSignedPush').bindValue =
-        configInputObj.enable_signed_push;
-      queryAndAssert<GrSelect>(element, '#requireSignedPush').bindValue =
-        configInputObj.require_signed_push;
-      queryAndAssert<GrSelect>(
+      newChangeSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const requireChangeIdSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#requireChangeIdSelect'
+      );
+      requireChangeIdSelect.value = configInputObj.require_change_id;
+      requireChangeIdSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const enableSignedPush = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#enableSignedPush'
+      );
+      enableSignedPush.value = configInputObj.enable_signed_push;
+      enableSignedPush.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const requireSignedPush = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#requireSignedPush'
+      );
+      requireSignedPush.value = configInputObj.require_signed_push;
+      requireSignedPush.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const rejectImplicitMergesSelect = queryAndAssert<MdOutlinedSelect>(
         element,
         '#rejectImplicitMergesSelect'
-      ).bindValue = configInputObj.reject_implicit_merges;
-      queryAndAssert<GrSelect>(
+      );
+      rejectImplicitMergesSelect.value = configInputObj.reject_implicit_merges;
+      rejectImplicitMergesSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const setAllnewChangesPrivateByDefaultSelect =
+        queryAndAssert<MdOutlinedSelect>(
+          element,
+          '#setAllnewChangesPrivateByDefaultSelect'
+        );
+      setAllnewChangesPrivateByDefaultSelect.value =
+        configInputObj.private_by_default;
+      setAllnewChangesPrivateByDefaultSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const setAllNewChangesWorkInProgressByDefaultSelect =
+        queryAndAssert<MdOutlinedSelect>(
+          element,
+          '#setAllNewChangesWorkInProgressByDefaultSelect'
+        );
+      setAllNewChangesWorkInProgressByDefaultSelect.value =
+        configInputObj.work_in_progress_by_default;
+      setAllNewChangesWorkInProgressByDefaultSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const matchAuthoredDateWithCommitterDateSelect =
+        queryAndAssert<MdOutlinedSelect>(
+          element,
+          '#matchAuthoredDateWithCommitterDateSelect'
+        );
+      matchAuthoredDateWithCommitterDateSelect.value =
+        configInputObj.match_author_to_committer_date;
+      matchAuthoredDateWithCommitterDateSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const maxGitObjSizeInput = queryAndAssert<MdOutlinedTextField>(
         element,
-        '#setAllnewChangesPrivateByDefaultSelect'
-      ).bindValue = configInputObj.private_by_default;
-      queryAndAssert<GrSelect>(
-        element,
-        '#setAllNewChangesWorkInProgressByDefaultSelect'
-      ).bindValue = configInputObj.work_in_progress_by_default;
-      queryAndAssert<GrSelect>(
-        element,
-        '#matchAuthoredDateWithCommitterDateSelect'
-      ).bindValue = configInputObj.match_author_to_committer_date;
-      queryAndAssert<IronInputElement>(
-        element,
-        '#maxGitObjSizeIronInput'
-      ).bindValue = String(configInputObj.max_object_size_limit);
-      queryAndAssert<GrSelect>(
+        '#maxGitObjSizeInput'
+      );
+      maxGitObjSizeInput.value = String(configInputObj.max_object_size_limit);
+      maxGitObjSizeInput.dispatchEvent(
+        new Event('input', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const contributorAgreementSelect = queryAndAssert<MdOutlinedSelect>(
         element,
         '#contributorAgreementSelect'
-      ).bindValue = configInputObj.use_contributor_agreements;
-      queryAndAssert<GrSelect>(element, '#useSignedOffBySelect').bindValue =
-        configInputObj.use_signed_off_by;
-      queryAndAssert<GrSelect>(element, '#rejectEmptyCommitSelect').bindValue =
-        configInputObj.reject_empty_commit;
-      queryAndAssert<GrSelect>(element, '#unRegisteredCcSelect').bindValue =
-        configInputObj.enable_reviewer_by_email;
+      );
+      contributorAgreementSelect.value =
+        configInputObj.use_contributor_agreements;
+      contributorAgreementSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const useSignedOffBySelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#useSignedOffBySelect'
+      );
+      useSignedOffBySelect.value = configInputObj.use_signed_off_by;
+      useSignedOffBySelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const rejectEmptyCommitSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#rejectEmptyCommitSelect'
+      );
+      rejectEmptyCommitSelect.value = configInputObj.reject_empty_commit;
+      rejectEmptyCommitSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
+      const unRegisteredCcSelect = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#unRegisteredCcSelect'
+      );
+      unRegisteredCcSelect.value = configInputObj.enable_reviewer_by_email;
+      unRegisteredCcSelect.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
 
       await element.updateComplete;
 
@@ -813,6 +1487,7 @@
       assert.deepEqual(formattedObj, configInputObj);
 
       await element.handleSaveRepoConfig();
+      await element.updateComplete;
       assert.isTrue(button.hasAttribute('disabled'));
       assert.isFalse(
         queryAndAssert<HTMLHeadingElement>(
@@ -825,13 +1500,7 @@
       );
     });
 
-    test('saveReviewBtn visible when experiment is enabled', async () => {
-      const flagsService = getAppContext().flagsService;
-      sinon
-        .stub(flagsService, 'isEnabled')
-        .callsFake(
-          id => id === KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
-        );
+    test('saveReviewBtn visible', async () => {
       await element.loadRepo();
       await element.updateComplete;
       const button = queryAndAssert<GrButton>(
@@ -841,13 +1510,7 @@
       assert.isFalse(button.hasAttribute('hidden'));
     });
 
-    test('saveBtn remains disabled when require_change_for_config_update is set', async () => {
-      const flagsService = getAppContext().flagsService;
-      sinon
-        .stub(flagsService, 'isEnabled')
-        .callsFake(
-          id => id === KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
-        );
+    test('saveBtn is disabled', async () => {
       getRepoAccessStub.callsFake(() =>
         Promise.resolve({
           'test-repo': {
@@ -863,12 +1526,6 @@
     });
 
     test('saveReviewBtn', async () => {
-      const flagsService = getAppContext().flagsService;
-      sinon
-        .stub(flagsService, 'isEnabled')
-        .callsFake(
-          id => id === KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
-        );
       let resolver: (value: ChangeInfo | PromiseLike<ChangeInfo>) => void;
       const saveForReviewStub = stubRestApi('saveRepoConfigForReview').returns(
         new Promise(r => (resolver = r))
@@ -878,11 +1535,12 @@
 
       await element.loadRepo();
       await element.updateComplete;
-      const input = queryAndAssert<GrSuggestionTextarea>(
+      const input = queryAndAssert<GrAutogrowTextarea>(
         element,
         '#descriptionInput'
       );
-      input.text = 'New description';
+      input.value = 'New description';
+      input.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
       await input.updateComplete;
       await element.updateComplete;
       const button = queryAndAssert<GrButton>(element, 'gr-button#saveBtn');
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
index a45fa33..d2f2c6e 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
@@ -3,21 +3,25 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-select/gr-select';
 import {encodeURL, getBaseUrl} from '../../../utils/url-util';
 import {AccessPermissionId} from '../../../utils/access-util';
 import {fire} from '../../../utils/event-util';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {ValueChangedEvent} from '../../../types/events';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {EditablePermissionRuleInfo} from '../gr-repo-access/gr-repo-access-interfaces';
 import {PermissionAction} from '../../../constants/constants';
 import {formStyles} from '../../../styles/form-styles';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import {convertToString} from '../../../utils/string-util';
 
 const PRIORITY_OPTIONS = [PermissionAction.BATCH, PermissionAction.INTERACTIVE];
 
@@ -131,6 +135,7 @@
       grFormStyles,
       formStyles,
       sharedStyles,
+      materialStyles,
       css`
         :host {
           border-bottom: 1px solid var(--border-color);
@@ -174,7 +179,7 @@
         .groupPath {
           color: var(--deemphasized-text-color);
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           width: 14em;
         }
       `,
@@ -188,19 +193,20 @@
         class="gr-form-styles ${this.computeSectionClass()}"
       >
         <div id="options">
-          <gr-select
+          <md-outlined-select
             id="action"
-            .bindValue=${this.rule?.value?.action}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
-              this.handleActionBindValueChanged(e);
-            }}
+            value=${convertToString(this.rule?.value?.action)}
+            ?disabled=${!this.editing}
+            @change=${this.handleActionChange}
           >
-            <select ?disabled=${!this.editing}>
-              ${this.computeOptions().map(
-                item => html` <option value=${item}>${item}</option> `
-              )}
-            </select>
-          </gr-select>
+            ${this.computeOptions().map(
+              item => html`
+                <md-select-option value=${item}>
+                  <div slot="headline">${item}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
           ${this.renderMinAndMaxLabel()} ${this.renderMinAndMaxInput()}
           <a
             class="groupPath"
@@ -208,22 +214,21 @@
           >
             ${this.groupName}
           </a>
-          <gr-select
+          <md-outlined-select
             id="force"
             class=${this.computeForce(this.rule?.value?.action) ? 'force' : ''}
-            .bindValue=${this.rule?.value?.force}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
-              this.handleForceBindValueChanged(e);
-            }}
+            value=${convertToString(this.rule?.value?.force)}
+            ?disabled=${!this.editing}
+            @change=${this.handleForceChange}
           >
-            <select ?disabled=${!this.editing}>
-              ${this.computeForceOptions(this.rule?.value?.action).map(
-                item => html`
-                  <option value=${item.value}>${item.name}</option>
-                `
-              )}
-            </select>
-          </gr-select>
+            ${this.computeForceOptions(this.rule?.value?.action).map(
+              item => html`
+                <md-select-option value=${item.value}>
+                  <div slot="headline">${item.name}</div>
+                </md-select-option>
+              `
+            )}
+          </md-outlined-select>
         </div>
         <gr-button
           link
@@ -255,32 +260,34 @@
     if (!this.label) return;
 
     return html`
-      <gr-select
+      <md-outlined-select
         id="labelMin"
-        .bindValue=${this.rule?.value?.min}
-        @bind-value-changed=${(e: BindValueChangeEvent) => {
-          this.handleMinBindValueChanged(e);
-        }}
+        value=${convertToString(this.rule?.value?.min)}
+        ?disabled=${!this.editing}
+        @change=${this.handleMinChange}
       >
-        <select ?disabled=${!this.editing}>
-          ${this.label.values.map(
-            item => html` <option value=${item.value}>${item.value}</option> `
-          )}
-        </select>
-      </gr-select>
-      <gr-select
+        ${this.label.values.map(
+          item => html`
+            <md-select-option value=${item.value}>
+              <div slot="headline">${item.value}</div>
+            </md-select-option>
+          `
+        )}
+      </md-outlined-select>
+      <md-outlined-select
         id="labelMax"
-        .bindValue=${this.rule?.value?.max}
-        @bind-value-changed=${(e: BindValueChangeEvent) => {
-          this.handleMaxBindValueChanged(e);
-        }}
+        value=${convertToString(this.rule?.value?.max)}
+        ?disabled=${!this.editing}
+        @change=${this.handleMaxChange}
       >
-        <select ?disabled=${!this.editing}>
-          ${this.label.values.map(
-            item => html` <option value=${item.value}>${item.value}</option> `
-          )}
-        </select>
-      </gr-select>
+        ${this.label.values.map(
+          item => html`
+            <md-select-option value=${item.value}>
+              <div slot="headline">${item.value}</div>
+            </md-select-option>
+          `
+        )}
+      </md-outlined-select>
     `;
   }
 
@@ -288,28 +295,46 @@
     if (!this.hasRange) return;
 
     return html`
-      <iron-autogrow-textarea
+      <gr-autogrow-textarea
         id="minInput"
         class="min"
         autocomplete="on"
         placeholder="Min value"
-        .bindValue=${this.rule?.value?.min}
+        .value=${this.rule?.value?.min?.toString() ?? ''}
         ?disabled=${!this.editing}
-        @bind-value-changed=${(e: BindValueChangeEvent) => {
-          this.handleMinBindValueChanged(e);
+        @input=${(e: InputEvent) => {
+          const value = (e.target as GrAutogrowTextarea).value ?? '';
+          if (
+            !this.rule?.value ||
+            value === undefined ||
+            this.rule.value.min === Number(value)
+          )
+            return;
+          this.rule.value.min = Number(value);
+
+          this.handleValueChange();
         }}
-      ></iron-autogrow-textarea>
-      <iron-autogrow-textarea
+      ></gr-autogrow-textarea>
+      <gr-autogrow-textarea
         id="maxInput"
         class="max"
         autocomplete="on"
         placeholder="Max value"
-        .bindValue=${this.rule?.value?.max}
+        .value=${this.rule?.value?.max?.toString() ?? ''}
         ?disabled=${!this.editing}
-        @bind-value-changed=${(e: BindValueChangeEvent) => {
-          this.handleMaxBindValueChanged(e);
+        @input=${(e: InputEvent) => {
+          const value = (e.target as GrAutogrowTextarea).value ?? '';
+          if (
+            !this.rule?.value ||
+            value === undefined ||
+            this.rule.value.max === Number(value)
+          )
+            return;
+          this.rule.value.max = Number(value);
+
+          this.handleValueChange();
         }}
-      ></iron-autogrow-textarea>
+      ></gr-autogrow-textarea>
     `;
   }
 
@@ -378,9 +403,8 @@
         return ForcePushOptions.ALLOW;
       } else if (action === Action.BLOCK) {
         return ForcePushOptions.BLOCK;
-      } else {
-        return [];
       }
+      return [];
     } else if (this.permission === AccessPermissionId.EDIT_TOPIC_NAME) {
       return FORCE_EDIT_OPTIONS;
     }
@@ -480,48 +504,52 @@
     this.originalRuleValues = {...this.rule.value};
   }
 
-  private handleActionBindValueChanged(e: BindValueChangeEvent) {
+  private handleActionChange(e: Event) {
+    const select = e.target as HTMLSelectElement;
     if (
       !this.rule?.value ||
-      e.detail.value === undefined ||
-      this.rule.value.action === String(e.detail.value)
+      select.value === undefined ||
+      this.rule.value.action === select.value
     )
       return;
 
-    this.rule.value.action = String(e.detail.value) as PermissionAction;
+    this.rule.value.action = select.value as PermissionAction;
 
     this.handleValueChange();
   }
 
-  private handleMinBindValueChanged(e: BindValueChangeEvent) {
+  private handleMinChange(e: Event) {
+    const select = e.target as HTMLSelectElement;
     if (
       !this.rule?.value ||
-      e.detail.value === undefined ||
-      this.rule.value.min === Number(e.detail.value)
+      select.value === undefined ||
+      this.rule.value.min === Number(select.value)
     )
       return;
-    this.rule.value.min = Number(e.detail.value);
+    this.rule.value.min = Number(select.value);
 
     this.handleValueChange();
   }
 
-  private handleMaxBindValueChanged(e: BindValueChangeEvent) {
+  private handleMaxChange(e: Event) {
+    const select = e.target as HTMLSelectElement;
     if (
       !this.rule?.value ||
-      e.detail.value === undefined ||
-      this.rule.value.max === Number(e.detail.value)
+      select.value === undefined ||
+      this.rule.value.max === Number(select.value)
     )
       return;
-    this.rule.value.max = Number(e.detail.value);
+    this.rule.value.max = Number(select.value);
 
     this.handleValueChange();
   }
 
-  private handleForceBindValueChanged(e: BindValueChangeEvent) {
-    const forceValue = String(e.detail.value) === 'true' ? true : false;
+  private handleForceChange(e: Event) {
+    const select = e.target as HTMLSelectElement;
+    const forceValue = String(select.value) === 'true' ? true : false;
     if (
       !this.rule?.value ||
-      e.detail.value === undefined ||
+      select.value === undefined ||
       this.rule.value.force === forceValue
     )
       return;
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_screenshot_test.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_screenshot_test.ts
new file mode 100644
index 0000000..098f15c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_screenshot_test.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-rule-editor';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrRuleEditor} from './gr-rule-editor';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+import {EditablePermissionRuleInfo} from '../gr-repo-access/gr-repo-access-interfaces';
+import {PermissionAction} from '../../../constants/constants';
+import {AccessPermissionId} from '../../../utils/access-util';
+
+suite('gr-rule-editor screenshot tests', () => {
+  let element: GrRuleEditor;
+
+  setup(async () => {
+    const rule: EditablePermissionRuleInfo = {
+      action: PermissionAction.ALLOW,
+      force: false,
+      min: -1,
+      max: 1,
+    };
+
+    element = await fixture<GrRuleEditor>(
+      html`<gr-rule-editor></gr-rule-editor>`
+    );
+    element.permission = 'label-Code-Review' as AccessPermissionId;
+    element.rule = {value: rule};
+    element.label = {
+      values: [
+        {value: -1, text: '-1 Fails'},
+        {value: 0, text: ' 0 No score'},
+        {value: 1, text: '+1 Passes'},
+      ],
+    };
+    element.hasRange = true;
+    element.editing = true;
+    await element.updateComplete;
+  });
+
+  test('screenshot', async () => {
+    await visualDiff(element, 'gr-rule-editor');
+    await visualDiffDarkTheme(element, 'gr-rule-editor');
+  });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts
index 650876d..a8767c2 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts
@@ -10,10 +10,10 @@
 import {AccessPermissionId} from '../../../utils/access-util';
 import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {GrSelect} from '../../shared/gr-select/gr-select';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {EditablePermissionRuleInfo} from '../gr-repo-access/gr-repo-access-interfaces';
 import {PermissionAction} from '../../../constants/constants';
+import {MdOutlinedSelect} from '@material/web/select/outlined-select';
 
 suite('gr-rule-editor tests', () => {
   let element: GrRuleEditor;
@@ -32,17 +32,20 @@
         /* HTML */ `
           <div class="gr-form-styles" id="mainContainer">
             <div id="options">
-              <gr-select id="action">
-                <select disabled="">
-                  <option value="ALLOW">ALLOW</option>
-                  <option value="DENY">DENY</option>
-                  <option value="BLOCK">BLOCK</option>
-                </select>
-              </gr-select>
+              <md-outlined-select disabled="" id="action" value="">
+                <md-select-option md-menu-item="" tabindex="0" value="ALLOW">
+                  <div slot="headline">ALLOW</div>
+                </md-select-option>
+                <md-select-option md-menu-item="" tabindex="-1" value="DENY">
+                  <div slot="headline">DENY</div>
+                </md-select-option>
+                <md-select-option md-menu-item="" tabindex="-1" value="BLOCK">
+                  <div slot="headline">BLOCK</div>
+                </md-select-option>
+              </md-outlined-select>
               <a class="groupPath"> </a>
-              <gr-select id="force">
-                <select disabled=""></select>
-              </gr-select>
+              <md-outlined-select disabled="" id="force" value="">
+              </md-outlined-select>
             </div>
             <gr-button
               aria-disabled="false"
@@ -87,24 +90,31 @@
         queryAndAssert(element, '#options'),
         /* HTML */ `
           <div id="options">
-            <gr-select id="action">
-              <select>
-                <option value="ALLOW">ALLOW</option>
-                <option value="DENY">DENY</option>
-                <option value="BLOCK">BLOCK</option>
-              </select>
-            </gr-select>
+            <md-outlined-select id="action" value="ALLOW">
+              <md-select-option
+                data-aria-selected="true"
+                md-menu-item=""
+                tabindex="0"
+                value="ALLOW"
+              >
+                <div slot="headline">ALLOW</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="DENY">
+                <div slot="headline">DENY</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="BLOCK">
+                <div slot="headline">BLOCK</div>
+              </md-select-option>
+            </md-outlined-select>
             <a class="groupPath"> </a>
-            <gr-select class="force" id="force">
-              <select>
-                <option value="false">
-                  Allow pushing (but not force pushing)
-                </option>
-                <option value="true">
-                  Allow pushing with or without force
-                </option>
-              </select>
-            </gr-select>
+            <md-outlined-select class="force" id="force" value="">
+              <md-select-option md-menu-item="" tabindex="0" value="false">
+                <div slot="headline">Allow pushing (but not force pushing)</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="true">
+                <div slot="headline">Allow pushing with or without force</div>
+              </md-select-option>
+            </md-outlined-select>
           </div>
         `
       );
@@ -271,6 +281,7 @@
     setup(async () => {
       element.groupName = 'Group Name';
       element.permission = 'submit' as AccessPermissionId;
+      await element.updateComplete;
       element.rule = {
         value: {
           action: PermissionAction.ALLOW,
@@ -289,13 +300,15 @@
 
     test('values are set correctly', () => {
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         element.rule!.value!.action
       );
-      assert.isNotOk(query<GrSelect>(element, '#labelMin'));
-      assert.isNotOk(query<GrSelect>(element, '#labelMax'));
+      assert.isNotOk(query<MdOutlinedSelect>(element, '#labelMin'));
+      assert.isNotOk(query<MdOutlinedSelect>(element, '#labelMax'));
       assert.isFalse(
-        queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+        queryAndAssert<MdOutlinedSelect>(element, '#force').classList.contains(
+          'force'
+        )
       );
     });
 
@@ -310,8 +323,17 @@
         'none'
       );
       assert.isNotOk(element.rule.value!.modified);
-      const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
-      actionBindValue.bindValue = PermissionAction.DENY;
+      const actionBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#action'
+      );
+      actionBindValue.value = PermissionAction.DENY;
+      actionBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule.value!.modified);
       element.editing = false;
@@ -325,15 +347,24 @@
       assert.isNotOk(element.rule.value!.modified);
       assert.equal(element.rule?.value?.action, PermissionAction.ALLOW);
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         PermissionAction.ALLOW
       );
     });
 
     test('modify value', async () => {
       assert.isNotOk(element.rule!.value!.modified);
-      const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
-      actionBindValue.bindValue = PermissionAction.DENY;
+      const actionBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#action'
+      );
+      actionBindValue.value = PermissionAction.DENY;
+      actionBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
 
@@ -443,6 +474,7 @@
     setup(async () => {
       element.groupName = 'Group Name';
       element.permission = 'editTopicName' as AccessPermissionId;
+      await element.updateComplete;
       element.rule = {};
       element.section = 'refs/*';
       element.setupValues();
@@ -465,19 +497,28 @@
 
       // values are set correctly
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         expectedRuleValue.action
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#force').bindValue,
-        expectedRuleValue.force
+        queryAndAssert<MdOutlinedSelect>(element, '#force').value,
+        String(expectedRuleValue.force)
       );
     });
 
     test('modify value', async () => {
       assert.isNotOk(element.rule!.value!.modified);
-      const forceBindValue = queryAndAssert<GrSelect>(element, '#force');
-      forceBindValue.bindValue = 'true';
+      const forceBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#force'
+      );
+      forceBindValue.value = 'true';
+      forceBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
 
@@ -508,6 +549,7 @@
       };
       element.groupName = 'Group Name';
       element.permission = 'label-Code-Review' as AccessPermissionId;
+      await element.updateComplete;
       element.rule = {
         value: {
           action: PermissionAction.ALLOW,
@@ -528,19 +570,21 @@
 
     test('values are set correctly', () => {
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         element.rule!.value!.action
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#labelMin').bindValue,
-        element.rule!.value!.min
+        queryAndAssert<MdOutlinedSelect>(element, '#labelMin').value,
+        String(element.rule!.value!.min)
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#labelMax').bindValue,
-        element.rule!.value!.max
+        queryAndAssert<MdOutlinedSelect>(element, '#labelMax').value,
+        String(element.rule!.value!.max)
       );
       assert.isFalse(
-        queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+        queryAndAssert<MdOutlinedSelect>(element, '#force').classList.contains(
+          'force'
+        )
       );
     });
 
@@ -548,8 +592,17 @@
       const removeStub = sinon.stub();
       element.addEventListener('added-rule-removed', removeStub);
       assert.isNotOk(element.rule!.value!.modified);
-      const labelMinBindValue = queryAndAssert<GrSelect>(element, '#labelMin');
-      labelMinBindValue.bindValue = 1;
+      const labelMinBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#labelMin'
+      );
+      labelMinBindValue.value = '1';
+      labelMinBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
       assert.isFalse(removeStub.called);
@@ -575,6 +628,7 @@
       };
       element.groupName = 'Group Name';
       element.permission = 'label-Code-Review' as AccessPermissionId;
+      await element.updateComplete;
       element.rule = {};
       element.section = 'refs/*';
       element.setupValues();
@@ -600,23 +654,32 @@
 
       // values are set correctly
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         expectedRuleValue.action
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#labelMin').bindValue,
-        expectedRuleValue.min
+        queryAndAssert<MdOutlinedSelect>(element, '#labelMin').value,
+        String(expectedRuleValue.min)
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#labelMax').bindValue,
-        expectedRuleValue.max
+        queryAndAssert<MdOutlinedSelect>(element, '#labelMax').value,
+        String(expectedRuleValue.max)
       );
     });
 
     test('modify value', async () => {
       assert.isNotOk(element.rule!.value!.modified);
-      const labelMinBindValue = queryAndAssert<GrSelect>(element, '#labelMin');
-      labelMinBindValue.bindValue = 1;
+      const labelMinBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#labelMin'
+      );
+      labelMinBindValue.value = '1';
+      labelMinBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
 
@@ -628,7 +691,7 @@
   suite('already existing push rule', () => {
     setup(async () => {
       element.groupName = 'Group Name';
-      element.permission = 'push' as AccessPermissionId;
+      element.permission = AccessPermissionId.PUSH;
       element.rule = {
         value: {
           action: PermissionAction.ALLOW,
@@ -647,24 +710,35 @@
 
     test('values are set correctly', () => {
       assert.isTrue(
-        queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+        queryAndAssert<MdOutlinedSelect>(element, '#force').classList.contains(
+          'force'
+        )
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         element.rule!.value!.action
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#force').bindValue,
-        element.rule!.value!.force
+        queryAndAssert(element, '#force').getAttribute('value'),
+        String(element.rule!.value!.force)
       );
-      assert.isNotOk(query<GrSelect>(element, '#labelMin'));
-      assert.isNotOk(query<GrSelect>(element, '#labelMax'));
+      assert.isNotOk(query<MdOutlinedSelect>(element, '#labelMin'));
+      assert.isNotOk(query<MdOutlinedSelect>(element, '#labelMax'));
     });
 
     test('modify value', async () => {
       assert.isNotOk(element.rule!.value!.modified);
-      const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
-      actionBindValue.bindValue = false;
+      const actionBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#action'
+      );
+      actionBindValue.value = PermissionAction.DENY;
+      actionBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
 
@@ -676,7 +750,8 @@
   suite('new push rule', async () => {
     setup(async () => {
       element.groupName = 'Group Name';
-      element.permission = 'push' as AccessPermissionId;
+      element.permission = AccessPermissionId.PUSH;
+      await element.updateComplete;
       element.rule = {};
       element.section = 'refs/*';
       element.setupValues();
@@ -698,19 +773,28 @@
       assert.deepEqual(element.rule!.value, expectedRuleValue);
       // values are set correctly
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         expectedRuleValue.action
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#force').bindValue,
-        expectedRuleValue.force
+        queryAndAssert(element, '#force').getAttribute('value'),
+        String(expectedRuleValue.force)
       );
     });
 
     test('modify value', async () => {
       assert.isNotOk(element.rule!.value!.modified);
-      const forceBindValue = queryAndAssert<GrSelect>(element, '#force');
-      forceBindValue.bindValue = true;
+      const forceBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#force'
+      );
+      forceBindValue.value = 'true';
+      forceBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
 
@@ -722,7 +806,7 @@
   suite('already existing edit rule', () => {
     setup(async () => {
       element.groupName = 'Group Name';
-      element.permission = 'editTopicName' as AccessPermissionId;
+      element.permission = AccessPermissionId.EDIT_TOPIC_NAME;
       element.rule = {
         value: {
           action: PermissionAction.ALLOW,
@@ -741,24 +825,35 @@
 
     test('values are set correctly', () => {
       assert.isTrue(
-        queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+        queryAndAssert<MdOutlinedSelect>(element, '#force').classList.contains(
+          'force'
+        )
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#action').bindValue,
+        queryAndAssert<MdOutlinedSelect>(element, '#action').value,
         element.rule!.value!.action
       );
       assert.equal(
-        queryAndAssert<GrSelect>(element, '#force').bindValue,
-        element.rule!.value!.force
+        queryAndAssert(element, '#force').getAttribute('value'),
+        String(element.rule!.value!.force)
       );
-      assert.isNotOk(query<GrSelect>(element, '#labelMin'));
-      assert.isNotOk(query<GrSelect>(element, '#labelMax'));
+      assert.isNotOk(query<MdOutlinedSelect>(element, '#labelMin'));
+      assert.isNotOk(query<MdOutlinedSelect>(element, '#labelMax'));
     });
 
     test('modify value', async () => {
       assert.isNotOk(element.rule!.value!.modified);
-      const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
-      actionBindValue.bindValue = false;
+      const actionBindValue = queryAndAssert<MdOutlinedSelect>(
+        element,
+        '#action'
+      );
+      actionBindValue.value = PermissionAction.DENY;
+      actionBindValue.dispatchEvent(
+        new CustomEvent('change', {
+          composed: true,
+          bubbles: true,
+        })
+      );
       await element.updateComplete;
       assert.isTrue(element.rule!.value!.modified);
 
diff --git a/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info_test.ts b/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info_test.ts
new file mode 100644
index 0000000..1351d09
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info_test.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {fixture, html} from '@open-wc/testing';
+import './gr-server-info';
+import {GrServerInfo} from './gr-server-info';
+import {assert} from '@open-wc/testing';
+import {createServerInfo} from '../../../test/test-data-generators';
+
+suite('gr-server-info tests', () => {
+  let element: GrServerInfo;
+
+  setup(async () => {
+    element = await fixture<GrServerInfo>(html`
+      <gr-server-info></gr-server-info>
+    `);
+    element.serverInfo = {
+      ...createServerInfo(),
+      metadata: [
+        {
+          name: 'test.name',
+          value: 'test.value',
+          description: 'test description',
+        },
+      ],
+    };
+    await element.updateComplete;
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(
+      element,
+      `
+        <main class="gr-form-styles read-only">
+          <table id="list" class="genericList">
+            <tbody>
+              <tr class="headerRow">
+                <th class="metadataName topHeader">Name</th>
+                <th class="metadataValue topHeader">Value</th>
+                <th class="metadataWebLinks topHeader">Links</th>
+                <th class="metadataDescription topHeader">Description</th>
+              </tr>
+            </tbody>
+            <tbody>
+              <tr class="table">
+                <td class="metadataName">accounts.visibility</td>
+                <td class="metadataValue">ALL</td>
+                <td class="metadataWebLinks"></td>
+                <td class="metadataDescription">
+                  Controls visibility of other users' dashboard pages and completion suggestions to web users.
+                </td>
+              </tr>
+              <tr class="table">
+                <td class="metadataName">test.name</td>
+                <td class="metadataValue">test.value</td>
+                <td class="metadataWebLinks"></td>
+                <td class="metadataDescription">test description</td>
+              </tr>
+            </tbody>
+          </table>
+        </main>
+      `
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
index a49a95e..260f231 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
@@ -15,6 +15,7 @@
 import '../gr-change-list-topic-flow/gr-change-list-topic-flow';
 import '../gr-change-list-hashtag-flow/gr-change-list-hashtag-flow';
 import '../gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow';
+import '../gr-change-list-copy-link-flow/gr-change-list-copy-link-flow';
 /**
  * An action bar for the top of a <gr-change-list-section> element. Assumes it
  * will be used inside a <tr> element.
@@ -78,6 +79,7 @@
             <gr-change-list-hashtag-flow></gr-change-list-hashtag-flow>
             <gr-change-list-reviewer-flow></gr-change-list-reviewer-flow>
             <gr-change-list-bulk-abandon-flow></gr-change-list-bulk-abandon-flow>
+            <gr-change-list-copy-link-flow></gr-change-list-copy-link-flow>
           </div>
         </div>
       </td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
index 4b7f0a3..9c9229b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
@@ -3,7 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {
   BulkActionsModel,
   bulkActionsModelToken,
@@ -69,6 +69,7 @@
               <gr-change-list-hashtag-flow></gr-change-list-hashtag-flow>
               <gr-change-list-reviewer-flow></gr-change-list-reviewer-flow>
               <gr-change-list-bulk-abandon-flow></gr-change-list-bulk-abandon-flow>
+              <gr-change-list-copy-link-flow></gr-change-list-copy-link-flow>
             </div>
           </div>
         </td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
index 2ca7f36..98781a0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
@@ -3,11 +3,11 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {customElement, state, query} from 'lit/decorators.js';
-import {LitElement, html, css} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
+import {css, html, LitElement} from 'lit';
 import {resolve} from '../../../models/dependency';
 import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
-import {NumericChangeId, ChangeInfo, ChangeStatus} from '../../../api/rest-api';
+import {ChangeInfo, ChangeStatus, NumericChangeId} from '../../../api/rest-api';
 import {subscribe} from '../../lit/subscription-controller';
 import {ProgressStatus} from '../../../constants/constants';
 import '../../shared/gr-dialog/gr-dialog';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts
index b91a0a0..d99a7c4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow_test.ts
@@ -6,10 +6,10 @@
 import * as sinon from 'sinon';
 import {createChange} from '../../../test/test-data-generators';
 import {
-  NumericChangeId,
   ChangeInfo,
   ChangeStatus,
   HttpMethod,
+  NumericChangeId,
   PatchSetNum,
 } from '../../../api/rest-api';
 import {GrChangeListBulkAbandonFlow} from './gr-change-list-bulk-abandon-flow';
@@ -20,16 +20,16 @@
   LoadingState,
 } from '../../../models/bulk-actions/bulk-actions-model';
 import './gr-change-list-bulk-abandon-flow';
-import {fixture, waitUntil, assert} from '@open-wc/testing';
+import {assert, fixture, waitUntil} from '@open-wc/testing';
 import {wrapInProvider} from '../../../models/di-provider-element';
 import {html} from 'lit';
 import {getAppContext} from '../../../services/app-context';
 import {
-  waitUntilObserved,
-  stubRestApi,
-  queryAndAssert,
   mockPromise,
   query,
+  queryAndAssert,
+  stubRestApi,
+  waitUntilObserved,
 } from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {ProgressStatus} from '../../../constants/constants';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index 6d7045a..73af927 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -4,27 +4,27 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {customElement, query, state} from 'lit/decorators.js';
-import {LitElement, html, css, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {resolve} from '../../../models/dependency';
 import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
 import {subscribe} from '../../lit/subscription-controller';
-import {ChangeInfo, AccountInfo, NumericChangeId} from '../../../api/rest-api';
+import {AccountInfo, ChangeInfo, NumericChangeId} from '../../../api/rest-api';
 import {
-  getTriggerVotes,
   computeLabels,
   computeOrderedLabelValues,
+  getTriggerVotes,
+  Label,
   mergeLabelInfoMaps,
   mergeLabelMaps,
-  Label,
   StandardLabels,
 } from '../../../utils/label-util';
 import {getAppContext} from '../../../services/app-context';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {queryAndAssert} from '../../../utils/common-util';
 import {
+  LabelNameToValueMap,
   LabelNameToValuesMap,
   ReviewInput,
-  LabelNameToValueMap,
 } from '../../../types/common';
 import {GrLabelScoreRow} from '../../change/gr-label-score-row/gr-label-score-row';
 import {ProgressStatus} from '../../../constants/constants';
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index df1b297..b8ddfa2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -12,18 +12,18 @@
   LoadingState,
 } from '../../../models/bulk-actions/bulk-actions-model';
 import {
-  waitUntilObserved,
-  stubRestApi,
-  queryAndAssert,
-  query,
   mockPromise,
+  query,
   queryAll,
+  queryAndAssert,
   stubReporting,
+  stubRestApi,
   waitEventLoop,
+  waitUntilObserved,
 } from '../../../test/test-utils';
-import {ChangeInfo, NumericChangeId, LabelInfo} from '../../../api/rest-api';
+import {ChangeInfo, LabelInfo, NumericChangeId} from '../../../api/rest-api';
 import {getAppContext} from '../../../services/app-context';
-import {fixture, waitUntil, assert} from '@open-wc/testing';
+import {assert, fixture, waitUntil} from '@open-wc/testing';
 import {wrapInProvider} from '../../../models/di-provider-element';
 import {html} from 'lit';
 import {SinonStubbedMember} from 'sinon';
@@ -209,7 +209,7 @@
       state => state === LoadingState.LOADED
     );
     stubRestApi('saveChangeReview').callsFake(
-      (_changeNum, _patchNum, _review, errFn) =>
+      (_changeNum: any, _patchNum: any, _review: any, errFn: any) =>
         Promise.resolve(undefined).then(res => {
           errFn && errFn();
           return res;
@@ -446,7 +446,7 @@
       getChangesStub.returns(Promise.resolve(changes));
 
       stubRestApi('saveChangeReview').callsFake(
-        (_changeNum, _patchNum, _review, errFn) =>
+        (_changeNum: any, _patchNum: any, _review: any, errFn: any) =>
           Promise.resolve({}).then(res => {
             errFn && errFn();
             return res;
@@ -504,7 +504,7 @@
       getChangesStub.returns(Promise.resolve(changes));
 
       stubRestApi('saveChangeReview').callsFake(
-        (_changeNum, _patchNum, _review, errFn) =>
+        (_changeNum: any, _patchNum: any, _review: any, errFn: any) =>
           Promise.resolve(undefined).then(res => {
             errFn && errFn();
             return res;
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 3e92658..6ab744a 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
@@ -6,7 +6,7 @@
 import '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
 import '../../shared/gr-change-status/gr-change-status';
 import '../../shared/gr-icon/gr-icon';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {
   ApprovalInfo,
@@ -104,10 +104,10 @@
   }
 
   private renderUnsatisfiedState(requirement: SubmitRequirementResultInfo) {
-    const requirementLabels = extractAssociatedLabels(
-      requirement,
-      'onlySubmittability'
-    );
+    const requirementLabels = extractAssociatedLabels(requirement, {
+      extractFromSubmittability: true,
+      extractFromOverride: 'onlyIfOverridden',
+    });
     const allLabels = this.change?.labels ?? {};
     const associatedLabels = Object.keys(allLabels).filter(label =>
       requirementLabels.includes(label)
@@ -143,7 +143,7 @@
       <gr-icon
         class=${icon.icon}
         icon=${icon.icon}
-        ?filled=${icon.filled}
+        ?filled=${!!icon.filled}
       ></gr-icon>
     `;
   }
@@ -169,10 +169,10 @@
     }
     const requirement = requirements[0];
     if (requirement.status === SubmitRequirementStatus.UNSATISFIED) {
-      const requirementLabels = extractAssociatedLabels(
-        requirement,
-        'onlySubmittability'
-      );
+      const requirementLabels = extractAssociatedLabels(requirement, {
+        extractFromSubmittability: true,
+        extractFromOverride: 'onlyIfOverridden',
+      });
       const allLabels = this.change?.labels ?? {};
       const associatedLabels = Object.keys(allLabels).filter(label =>
         requirementLabels.includes(label)
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 82e8048..88fbbff 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
@@ -4,15 +4,15 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-change-list-column-requirement';
 import {GrChangeListColumnRequirement} from './gr-change-list-column-requirement';
 import {
+  createChange,
+  createNonApplicableSubmitRequirementResultInfo,
   createSubmitRequirementExpressionInfo,
   createSubmitRequirementResultInfo,
-  createNonApplicableSubmitRequirementResultInfo,
-  createChange,
 } from '../../../test/test-data-generators';
 import {
   AccountId,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
index dbde061..9e24d0c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
@@ -6,7 +6,7 @@
 import '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
 import '../../shared/gr-change-status/gr-change-status';
 import '../../shared/gr-icon/gr-icon';
-import {LitElement, css, html, TemplateResult} from 'lit';
+import {css, html, LitElement, TemplateResult} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {ChangeInfo, SubmitRequirementStatus} from '../../../api/rest-api';
 import {changeStatuses} from '../../../utils/change-util';
@@ -102,7 +102,7 @@
       <gr-icon
         class=${icon.icon}
         icon=${icon.icon}
-        ?filled=${icon.filled}
+        ?filled=${!!icon.filled}
         role="img"
       ></gr-icon>
       ${aggregation}</span
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts
index 6da91be2..4eb51ad 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts
@@ -4,17 +4,17 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-change-list-column-requirements-summary';
 import {GrChangeListColumnRequirementsSummary} from './gr-change-list-column-requirements-summary';
 import {
   createApproval,
   createDetailedLabelInfo,
+  createNonApplicableSubmitRequirementResultInfo,
   createParsedChange,
   createSubmitRequirementExpressionInfo,
   createSubmitRequirementResultInfo,
-  createNonApplicableSubmitRequirementResultInfo,
 } from '../../../test/test-data-generators';
 import {
   SubmitRequirementResultInfo,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-copy-link-flow/gr-change-list-copy-link-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-copy-link-flow/gr-change-list-copy-link-flow.ts
new file mode 100644
index 0000000..e8eca8a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-copy-link-flow/gr-change-list-copy-link-flow.ts
@@ -0,0 +1,112 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {css, html, LitElement, nothing} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
+import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
+import {resolve} from '../../../models/dependency';
+import {subscribe} from '../../lit/subscription-controller';
+import '../../shared/gr-button/gr-button';
+import '../../change/gr-copy-links/gr-copy-links';
+import {CopyLink, GrCopyLinks} from '../../change/gr-copy-links/gr-copy-links';
+import {prependOrigin} from '../../../utils/url-util';
+import {ChangeInfo} from '../../../api/rest-api';
+
+@customElement('gr-change-list-copy-link-flow')
+export class GrChangeListCopyLinkFlow extends LitElement {
+  @query('gr-copy-links')
+  private copyLinks?: GrCopyLinks;
+
+  static override get styles() {
+    return css`
+      :host {
+        display: inline-block;
+        position: relative;
+      }
+      gr-button {
+        margin-right: var(--spacing-s);
+      }
+      .copyLinkWrapper {
+        position: relative;
+      }
+    `;
+  }
+
+  @state() private selectedChangeNums: number[] = [];
+
+  @state() private selectedChanges: ChangeInfo[] = [];
+
+  private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getBulkActionsModel().selectedChangeNums$,
+      selectedChangeNums => (this.selectedChangeNums = selectedChangeNums)
+    );
+    subscribe(
+      this,
+      () => this.getBulkActionsModel().selectedChanges$,
+      selectedChanges => (this.selectedChanges = selectedChanges)
+    );
+  }
+
+  // private but used in test
+  getCopyLinks(): CopyLink[] {
+    if (this.selectedChangeNums.length === 0) return [];
+    const changeIds = this.selectedChangeNums
+      .map(num => `change:${num}`)
+      .join('+OR+');
+    const url = prependOrigin(`/q/${changeIds}`);
+    return [
+      {
+        label: 'Change Query URL',
+        shortcut: 'u',
+        value: url,
+      },
+      {
+        label: 'Markdown',
+        shortcut: 'm',
+        value: this.selectedChanges
+          .map(change => {
+            const changeUrl = prependOrigin(`/c/${change._number}`);
+            return `[${change.subject}](${changeUrl})`;
+          })
+          .join('\n'),
+        multiline: true,
+      },
+    ];
+  }
+
+  override render() {
+    if (this.selectedChangeNums.length === 0) return nothing;
+    return html`
+      <div class="copyLinkWrapper">
+        <gr-button
+          id="copyLinkButton"
+          link
+          @click=${(e: Event) => {
+            this.copyLinks?.toggleDropdown(e.target as HTMLElement);
+          }}
+        >
+          Copy Link
+        </gr-button>
+        <gr-copy-links
+          .copyLinks=${this.getCopyLinks()}
+          .horizontalAlign=${'right'}
+          .verticalOffset=${30}
+          .shortcutPrefix=${''}
+        ></gr-copy-links>
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-change-list-copy-link-flow': GrChangeListCopyLinkFlow;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-copy-link-flow/gr-change-list-copy-link-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-copy-link-flow/gr-change-list-copy-link-flow_test.ts
new file mode 100644
index 0000000..9a14afa
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-copy-link-flow/gr-change-list-copy-link-flow_test.ts
@@ -0,0 +1,107 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {assert, fixture, html, waitUntil} from '@open-wc/testing';
+import {
+  BulkActionsModel,
+  bulkActionsModelToken,
+} from '../../../models/bulk-actions/bulk-actions-model';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {getAppContext} from '../../../services/app-context';
+import '../../../test/common-test-setup';
+import {createChange} from '../../../test/test-data-generators';
+import {waitUntilObserved} from '../../../test/test-utils';
+import './gr-change-list-copy-link-flow';
+import {GrChangeListCopyLinkFlow} from './gr-change-list-copy-link-flow';
+import {ChangeInfo, NumericChangeId} from '../../../api/rest-api';
+import {prependOrigin} from '../../../utils/url-util';
+
+const change1: ChangeInfo = {...createChange(), _number: 1 as NumericChangeId};
+const change2: ChangeInfo = {...createChange(), _number: 2 as NumericChangeId};
+
+suite('gr-change-list-copy-link-flow tests', () => {
+  let element: GrChangeListCopyLinkFlow;
+  let model: BulkActionsModel;
+
+  async function selectChange(change: ChangeInfo) {
+    model.addSelectedChangeNum(change._number);
+    await waitUntilObserved(model.selectedChangeNums$, selectedChangeNums =>
+      selectedChangeNums.includes(change._number)
+    );
+    await element.updateComplete;
+  }
+
+  setup(async () => {
+    model = new BulkActionsModel(getAppContext().restApiService);
+    model.sync([change1, change2]);
+    element = (
+      await fixture(
+        wrapInProvider(
+          html`<gr-change-list-copy-link-flow></gr-change-list-copy-link-flow>`,
+          bulkActionsModelToken,
+          model
+        )
+      )
+    ).querySelector('gr-change-list-copy-link-flow')!;
+    await element.updateComplete;
+  });
+
+  test('renders nothing when no changes selected', async () => {
+    assert.shadowDom.equal(element, '');
+  });
+
+  test('renders copy link button when changes are selected', async () => {
+    await selectChange(change1);
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <div class="copyLinkWrapper">
+          <gr-button
+            id="copyLinkButton"
+            link=""
+            role="button"
+            tabindex="0"
+            aria-disabled="false"
+          >
+            Copy Link
+          </gr-button>
+          <gr-copy-links></gr-copy-links>
+        </div>
+      `
+    );
+  });
+
+  test('copy links are generated correctly', async () => {
+    await selectChange(change1);
+    await selectChange(change2);
+    assert.deepEqual(element.getCopyLinks(), [
+      {
+        label: 'Change Query URL',
+        shortcut: 'u',
+        value: prependOrigin('/q/change:1+OR+change:2'),
+      },
+      {
+        label: 'Markdown',
+        shortcut: 'm',
+        value: `[Subject 1](${prependOrigin(
+          '/c/1'
+        )})\n[Subject 2](${prependOrigin('/c/2')})`,
+        multiline: true,
+      },
+    ]);
+  });
+
+  test('clicking copy link button opens dropdown', async () => {
+    await selectChange(change1);
+    const copyLinks = element.shadowRoot?.querySelector('gr-copy-links');
+    assert.exists(copyLinks);
+    const button = element.shadowRoot?.querySelector('gr-button');
+    assert.exists(button);
+    button?.click();
+    await element.updateComplete;
+    await waitUntil(() => element.shadowRoot?.querySelector('gr-copy-links'));
+    assert.isTrue((copyLinks as any).isDropdownOpen);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
index ea61bfc..2d8022b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
@@ -12,12 +12,10 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-autocomplete/gr-autocomplete';
-import '@polymer/iron-dropdown/iron-dropdown';
-import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
 import {getAppContext} from '../../../services/app-context';
 import {isDefined} from '../../../types/types';
 import {unique} from '../../../utils/common-util';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {when} from 'lit/directives/when.js';
 import {ValueChangedEvent} from '../../../types/events';
 import {classMap} from 'lit/directives/class-map.js';
@@ -28,6 +26,8 @@
 import {pluralize} from '../../../utils/string-util';
 import {Interaction} from '../../../constants/reporting';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import '@material/web/menu/menu';
+import {MdMenu} from '@material/web/menu/menu';
 
 @customElement('gr-change-list-hashtag-flow')
 export class GrChangeListHashtagFlow extends LitElement {
@@ -46,7 +46,7 @@
 
   @state() private overallProgress: ProgressStatus = ProgressStatus.NOT_STARTED;
 
-  @query('iron-dropdown') private dropdown?: IronDropdownElement;
+  @query('md-menu') private dropdown?: MdMenu;
 
   private selectedExistingHashtags: Set<Hashtag> = new Set();
 
@@ -60,13 +60,15 @@
     return [
       spinnerStyles,
       css`
-        iron-dropdown {
+        md-menu {
+          --md-menu-container-color: var(--dialog-background-color);
+          --md-menu-top-space: 0px;
+          --md-menu-bottom-space: 0px;
+        }
+        .dropdown-content {
           box-shadow: var(--elevation-level-2);
           width: 400px;
-          background-color: var(--dialog-background-color);
           border-radius: 4px;
-        }
-        [slot='dropdown-content'] {
           padding: var(--spacing-xl) var(--spacing-l) var(--spacing-l);
         }
         gr-autocomplete {
@@ -129,6 +131,10 @@
           position: relative;
           top: 7px;
         }
+        .dropdown {
+          position: relative;
+          display: inline-block;
+        }
       `,
     ];
   }
@@ -146,26 +152,26 @@
 
   override render() {
     const isFlowDisabled = this.selectedChanges.length === 0;
-    return html`
+    return html`<div class="dropdown">
       <gr-button
         id="start-flow"
         flatten
         down-arrow
         @click=${this.toggleDropdown}
-        .disabled=${isFlowDisabled}
+        ?disabled=${isFlowDisabled}
         >Hashtag</gr-button
       >
-      <iron-dropdown
-        .horizontalAlign=${'auto'}
-        .verticalAlign=${'auto'}
-        .verticalOffset=${24}
-        @opened-changed=${(e: ValueChangedEvent<boolean>) =>
-          (this.isDropdownOpen = e.detail.value)}
+      <md-menu
+        anchor="start-flow"
+        tabindex="-1"
+        .quick=${true}
+        @opening=${() => (this.isDropdownOpen = true)}
+        @closing=${() => (this.isDropdownOpen = false)}
       >
         ${when(
           this.isDropdownOpen,
           () => html`
-            <div slot="dropdown-content">
+            <div class="dropdown-content">
               ${this.renderExistingHashtags()}
               <!--
                 The .query function needs to be bound to this because lit's
@@ -209,8 +215,8 @@
             </div>
           `
         )}
-      </iron-dropdown>
-    `;
+      </md-menu>
+    </div> `;
   }
 
   private renderExistingHashtags() {
@@ -292,7 +298,7 @@
   private openDropdown() {
     this.reset();
     this.isDropdownOpen = true;
-    this.dropdown?.open();
+    this.dropdown?.show();
   }
 
   private async getHashtagSuggestions(
@@ -349,9 +355,6 @@
       this.overallProgress = ProgressStatus.SUCCESSFUL;
       fireAlert(this, alert);
       this.reset();
-      // iron-dropdown doesn't automatically expand when the new chip adds more
-      // vertical space.
-      this.dropdown?.notifyResize();
     } else {
       this.overallProgress = ProgressStatus.FAILED;
       this.errorText = errorText;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
index 859a2ba..7c6b559 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
@@ -4,8 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
-import {IronDropdownElement} from '@polymer/iron-dropdown';
+import {assert, fixture, html} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {
   BulkActionsModel,
@@ -29,12 +28,13 @@
   waitUntilCalled,
   waitUntilObserved,
 } from '../../../test/test-utils';
-import {ChangeInfo, NumericChangeId, Hashtag} from '../../../types/common';
+import {ChangeInfo, Hashtag, NumericChangeId} from '../../../types/common';
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import './gr-change-list-hashtag-flow';
 import {GrChangeListHashtagFlow} from './gr-change-list-hashtag-flow';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {MdMenu} from '@material/web/menu/menu';
 
 suite('gr-change-list-hashtag-flow tests', () => {
   let element: GrChangeListHashtagFlow;
@@ -91,33 +91,27 @@
       assert.shadowDom.equal(
         element,
         /* HTML */ `
-          <gr-button
-            id="start-flow"
-            flatten=""
-            down-arrow=""
-            aria-disabled="false"
-            role="button"
-            tabindex="0"
-            >Hashtag</gr-button
-          >
-          <iron-dropdown
-            aria-disabled="false"
-            aria-hidden="true"
-            style="outline: none; display: none;"
-            vertical-align="auto"
-            horizontal-align="auto"
-          >
-          </iron-dropdown>
+          <div class="dropdown">
+            <gr-button
+              aria-disabled="false"
+              down-arrow=""
+              flatten=""
+              id="start-flow"
+              role="button"
+              tabindex="0"
+            >
+              Hashtag
+            </gr-button>
+            <md-menu anchor="start-flow" aria-hidden="true" tabindex="-1">
+            </md-menu>
+          </div>
         `
       );
     });
 
     test('dropdown hidden before flow button clicked', async () => {
-      const dropdown = queryAndAssert<IronDropdownElement>(
-        element,
-        'iron-dropdown'
-      );
-      assert.isFalse(dropdown.opened);
+      const dropdown = queryAndAssert<MdMenu>(element, 'md-menu');
+      assert.isFalse(dropdown.open);
     });
 
     test('flow button click shows dropdown', async () => {
@@ -126,25 +120,17 @@
       button.click();
       await element.updateComplete;
 
-      const dropdown = queryAndAssert<IronDropdownElement>(
-        element,
-        'iron-dropdown'
-      );
-      assert.isTrue(dropdown.opened);
+      const dropdown = queryAndAssert<MdMenu>(element, 'md-menu');
+      assert.isTrue(dropdown.open);
     });
 
     test('flow button click when open hides dropdown', async () => {
       queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
       await waitUntil(() =>
-        Boolean(
-          queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-        )
+        Boolean(queryAndAssert<MdMenu>(element, 'md-menu').open)
       );
       queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
-      await waitUntil(
-        () =>
-          !queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-      );
+      await waitUntil(() => !queryAndAssert<MdMenu>(element, 'md-menu').open);
     });
   });
 
@@ -236,68 +222,71 @@
       assert.shadowDom.equal(
         element,
         /* HTML */ `
-          <gr-button
-            id="start-flow"
-            flatten=""
-            down-arrow=""
-            aria-disabled="false"
-            role="button"
-            tabindex="0"
-            >Hashtag</gr-button
-          >
-          <iron-dropdown
-            aria-disabled="false"
-            vertical-align="auto"
-            horizontal-align="auto"
-          >
-            <div slot="dropdown-content">
-              <div class="chips">
-                <button
-                  role="listbox"
-                  aria-label="hashtag1 selection"
-                  class="chip"
-                >
-                  hashtag1
-                </button>
-                <button
-                  role="listbox"
-                  aria-label="sharedHashtag selection"
-                  class="chip"
-                >
-                  sharedHashtag
-                </button>
-                <button
-                  role="listbox"
-                  aria-label="hashtag2 selection"
-                  class="chip"
-                >
-                  hashtag2
-                </button>
-              </div>
-              <gr-autocomplete
-                placeholder="Type hashtag name to create or filter hashtags"
-                show-blue-focus-border=""
-              ></gr-autocomplete>
-              <div class="footer">
-                <div class="loadingOrError" role="progressbar"></div>
-                <div class="buttons">
-                  <gr-button
-                    id="add-hashtag-button"
-                    flatten=""
-                    aria-disabled="true"
-                    disabled=""
-                    role="button"
-                    tabindex="-1"
-                    >Add Hashtag</gr-button
+          <div class="dropdown">
+            <gr-button
+              aria-disabled="false"
+              down-arrow=""
+              flatten=""
+              id="start-flow"
+              role="button"
+              tabindex="0"
+            >
+              Hashtag
+            </gr-button>
+            <md-menu anchor="start-flow" open="" tabindex="-1">
+              <div class="dropdown-content">
+                <div class="chips">
+                  <button
+                    aria-label="hashtag1 selection"
+                    class="chip"
+                    role="listbox"
                   >
+                    hashtag1
+                  </button>
+                  <button
+                    aria-label="sharedHashtag selection"
+                    class="chip"
+                    role="listbox"
+                  >
+                    sharedHashtag
+                  </button>
+                  <button
+                    aria-label="hashtag2 selection"
+                    class="chip"
+                    role="listbox"
+                  >
+                    hashtag2
+                  </button>
+                </div>
+                <gr-autocomplete
+                  placeholder="Type hashtag name to create or filter hashtags"
+                  show-blue-focus-border=""
+                >
+                </gr-autocomplete>
+                <div class="footer">
+                  <div class="loadingOrError" role="progressbar"></div>
+                  <div class="buttons">
+                    <gr-button
+                      aria-disabled="true"
+                      disabled=""
+                      flatten=""
+                      id="add-hashtag-button"
+                      role="button"
+                      tabindex="-1"
+                    >
+                      Add Hashtag
+                    </gr-button>
+                  </div>
                 </div>
               </div>
-            </div>
-          </iron-dropdown>
+            </md-menu>
+          </div>
         `,
         {
           // iron-dropdown sizing seems to vary between local & CI
-          ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
+          ignoreAttributes: [
+            {tags: ['iron-dropdown'], attributes: ['style', 'focused']},
+          ],
         }
       );
     });
@@ -346,9 +335,7 @@
         selectedChangeCount: 3,
         hashtagsApplied: 1,
       });
-      assert.isTrue(
-        queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-      );
+      assert.isTrue(queryAndAssert<MdMenu>(element, 'md-menu').open);
     });
 
     test('add multiple hashtag from selected change', async () => {
@@ -413,6 +400,7 @@
         element,
         'gr-autocomplete'
       );
+      autocomplete.debounceWait = 10;
 
       autocomplete.setFocus(true);
       autocomplete.text = 'foo';
@@ -456,9 +444,7 @@
         selectedChangeCount: 3,
         hashtagsApplied: 1,
       });
-      assert.isTrue(
-        queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-      );
+      assert.isTrue(queryAndAssert<MdMenu>(element, 'md-menu').open);
     });
 
     test('add new hashtag', async () => {
@@ -472,6 +458,7 @@
         element,
         'gr-autocomplete'
       );
+      autocomplete.debounceWait = 10;
       autocomplete.setFocus(true);
       autocomplete.text = 'foo';
       await element.updateComplete;
@@ -521,9 +508,7 @@
         selectedChangeCount: 3,
         hashtagsApplied: 1,
       });
-      assert.isTrue(
-        queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-      );
+      assert.isTrue(queryAndAssert<MdMenu>(element, 'md-menu').open);
       assert.equal(
         queryAll<HTMLButtonElement>(element, 'button.chip')[2].innerText,
         'foo'
@@ -538,6 +523,7 @@
         element,
         'gr-autocomplete'
       );
+      autocomplete.debounceWait = 10;
       autocomplete.setFocus(true);
       autocomplete.text = 'foo';
       await element.updateComplete;
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 9bcc862..fbd5dc1 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
@@ -20,16 +20,17 @@
 import {truncatePath} from '../../../utils/path-list-util';
 import {isSelf, isServiceUser} from '../../../utils/account-util';
 import {
-  ChangeInfo,
-  ServerInfo,
   AccountInfo,
-  Timestamp,
+  ChangeInfo,
   NumericChangeId,
+  ServerInfo,
+  Timestamp,
+  UserId,
 } from '../../../types/common';
-import {hasOwnProperty, assertIsDefined} from '../../../utils/common-util';
+import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
 import {changeListStyles} from '../../../styles/gr-change-list-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
 import {ifDefined} from 'lit/directives/if-defined.js';
@@ -44,6 +45,8 @@
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {configModelToken} from '../../../models/config/config-model';
 import {formStyles} from '../../../styles/form-styles';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 enum ChangeSize {
   XS = 10,
@@ -82,7 +85,7 @@
    * generated.
    */
   @property({type: String})
-  dashboardUser?: string;
+  dashboardUser?: UserId | 'self';
 
   @property({type: Array})
   visibleChangeTableColumns?: string[];
@@ -203,6 +206,7 @@
       sharedStyles,
       submitRequirementsStyles,
       changeListStyles,
+      materialStyles,
       css`
         :host {
           display: table-row;
@@ -317,10 +321,18 @@
           padding: 0;
           margin: 0;
         }
+        md-checkbox {
+          --md-checkbox-container-size: 15px;
+          --md-checkbox-icon-size: 15px;
+        }
         @media only screen and (max-width: 50em) {
           :host {
             display: flex;
           }
+          .content,
+          .spacer {
+            max-width: calc(100vw - 50px);
+          }
         }
       `,
     ];
@@ -349,18 +361,11 @@
 
     return html`
       <td class="cell selection">
-        <!--
-          The .checked property must be used rather than the attribute because
-          the attribute only controls the default checked state and does not
-          update the current checked state.
-          See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#attr-checked
-        -->
         <label class="selectionLabel">
-          <input
-            type="checkbox"
-            .checked=${this.checked}
-            @click=${this.toggleCheckbox}
-          />
+          <md-checkbox
+            ?checked=${this.checked}
+            @change=${this.toggleCheckbox}
+          ></md-checkbox>
         </label>
       </td>
     `;
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 2722efe..c62d185 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
@@ -4,11 +4,11 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import {
-  SubmitRequirementResultInfo,
   NumericChangeId,
+  SubmitRequirementResultInfo,
   Timestamp,
 } from '../../../api/rest-api';
 import '../../../test/common-test-setup';
@@ -16,10 +16,10 @@
   createAccountWithEmail,
   createAccountWithId,
   createChange,
-  createSubmitRequirementExpressionInfo,
-  createSubmitRequirementResultInfo,
   createNonApplicableSubmitRequirementResultInfo,
   createServerInfo,
+  createSubmitRequirementExpressionInfo,
+  createSubmitRequirementResultInfo,
 } from '../../../test/test-data-generators';
 import {
   query,
@@ -42,8 +42,8 @@
   wrapInProvider,
 } from '../../../models/di-provider-element';
 import {
-  bulkActionsModelToken,
   BulkActionsModel,
+  bulkActionsModelToken,
 } from '../../../models/bulk-actions/bulk-actions-model';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {createTestAppContext} from '../../../test/test-app-context-init';
@@ -114,7 +114,7 @@
 
       const checkbox = queryAndAssert<HTMLInputElement>(
         element,
-        '.selection > .selectionLabel > input'
+        '.selection > .selectionLabel > md-checkbox'
       );
       checkbox.click();
       let selectedChangeNums = await waitUntilObserved(
@@ -147,7 +147,7 @@
 
       const checkbox = queryAndAssert<HTMLInputElement>(
         element,
-        '.selection > .selectionLabel > input'
+        '.selection > .selectionLabel > md-checkbox'
       );
       checkbox.click();
       await element.updateComplete;
@@ -170,7 +170,7 @@
 
       const checkbox = queryAndAssert<HTMLInputElement>(
         element,
-        '.selection > .selectionLabel > input'
+        '.selection > .selectionLabel > md-checkbox'
       );
       assert.isTrue(checkbox.checked);
 
@@ -199,7 +199,7 @@
 
       const checkbox = queryAndAssert<HTMLInputElement>(
         element,
-        '.selection > .selectionLabel > input'
+        '.selection > .selectionLabel > md-checkbox'
       );
       assert.isTrue(checkbox.checked);
 
@@ -396,7 +396,7 @@
       element,
       /* HTML */ `
         <label class="selectionLabel">
-          <input type="checkbox" />
+          <md-checkbox checked=""> </md-checkbox>
         </label>
         <gr-change-star></gr-change-star>
         <a href="/c/test-project/+/42">42</a>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index bd222cb..53bef3c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -245,7 +245,7 @@
         .placeholder=${placeholder}
         .pendingConfirmation=${this.groupPendingConfirmationByReviewerState.get(
           reviewerState
-        )}
+        ) ?? null}
         @accounts-changed=${() => this.onAccountsChanged(reviewerState)}
         @pending-confirmation-changed=${(
           ev: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index 7ccdb0e..fa8ffba 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {
   AccountInfo,
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 8ad6c68..0e9cf7b 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
@@ -3,13 +3,13 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, html, css, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {ChangeListSection} from '../gr-change-list/gr-change-list';
 import '../gr-change-list-action-bar/gr-change-list-action-bar';
 import {CLOSED, YOUR_TURN} from '../../../utils/dashboard-util';
 import {getAppContext} from '../../../services/app-context';
-import {ChangeInfo, AccountInfo} from '../../../api/rest-api';
+import {AccountInfo, ChangeInfo} from '../../../api/rest-api';
 import {changeListStyles} from '../../../styles/gr-change-list-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -17,14 +17,20 @@
 import {WAITING} from '../../../constants/constants';
 import {provide, resolve} from '../../../models/dependency';
 import {
-  bulkActionsModelToken,
   BulkActionsModel,
+  bulkActionsModelToken,
+  LoadingState,
 } from '../../../models/bulk-actions/bulk-actions-model';
 import {createSearchUrl} from '../../../models/views/search';
 import {userModelToken} from '../../../models/user/user-model';
 import {subscribe} from '../../lit/subscription-controller';
 import {classMap} from 'lit/directives/class-map.js';
 import {formStyles} from '../../../styles/form-styles';
+import {UserId} from '../../../types/common';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import {when} from 'lit/directives/when.js';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 
 const NUMBER_FIXED_COLUMNS = 4;
 const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
@@ -83,7 +89,7 @@
    * generated.
    */
   @property({type: String})
-  dashboardUser?: string;
+  dashboardUser?: UserId | 'self';
 
   @property({type: String})
   usp?: string;
@@ -103,6 +109,10 @@
   @state()
   private totalChangeCount = 0;
 
+  // Private but used in test
+  @state()
+  bulkActionsLoaded = false;
+
   bulkActionsModel: BulkActionsModel = new BulkActionsModel(
     getAppContext().restApiService
   );
@@ -117,6 +127,8 @@
       changeListStyles,
       fontStyles,
       formStyles,
+      materialStyles,
+      spinnerStyles,
       css`
         :host {
           display: contents;
@@ -145,6 +157,20 @@
         .showSelectionBorder {
           border-bottom: 2px solid var(--input-focus-border-color);
         }
+        md-checkbox {
+          --md-checkbox-container-size: 15px;
+          --md-checkbox-icon-size: 15px;
+        }
+        /* The basics of .loadingSpin are defined in shared styles. */
+        .loadingSpin {
+          width: 15px;
+          height: 15px;
+          display: inline-block;
+          vertical-align: middle;
+        }
+        .selection:has(.loadingSpin):not(:has(md-checkbox)) {
+          padding-right: 4px !important;
+        }
       `,
     ];
   }
@@ -161,6 +187,13 @@
     );
     subscribe(
       this,
+      () => this.bulkActionsModel.loadingState$,
+      loadingState => {
+        this.bulkActionsLoaded = loadingState === LoadingState.LOADED;
+      }
+    );
+    subscribe(
+      this,
       () => this.bulkActionsModel.totalChangeCount$,
       totalChangeCount => (this.totalChangeCount = totalChangeCount)
     );
@@ -205,7 +238,7 @@
         <td class="leftPadding" aria-hidden="true"></td>
         <td
           class="star"
-          ?aria-hidden=${!this.isLoggedIn}
+          aria-hidden=${!this.isLoggedIn}
           ?hidden=${!this.isLoggedIn}
         ></td>
         <td class="cell" colspan=${colSpan}>
@@ -284,19 +317,21 @@
       this.numSelected > 0 && this.numSelected !== this.totalChangeCount;
     return html`
       <td class="selection" ?hidden=${!this.isLoggedIn}>
-        <!--
-          The .checked property must be used rather than the attribute because
-          the attribute only controls the default checked state and does not
-          update the current checked state.
-          See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#attr-checked
-        -->
-        <input
-          class="selection-checkbox"
-          type="checkbox"
-          .checked=${checked}
-          .indeterminate=${indeterminate}
-          @click=${this.handleSelectAllCheckboxClicked}
-        />
+        ${when(
+          this.bulkActionsLoaded,
+          () => html`
+            <md-checkbox
+              class="selection-checkbox"
+              ?checked=${checked}
+              .indeterminate=${indeterminate}
+              @change=${this.handleSelectAllCheckboxClicked}
+            ></md-checkbox>
+          `,
+          () => html` <span
+            class="loadingSpin"
+            title="Bulk actions is loading"
+          ></span>`
+        )}
       </td>
     `;
   }
@@ -337,7 +372,7 @@
         .change=${change}
         .sectionName=${this.changeSection.name}
         .visibleChangeTableColumns=${columns}
-        .showNumber=${this.showNumber}
+        .showNumber=${!!this.showNumber}
         .usp=${this.usp}
         .labelNames=${this.labelNames}
         .globalIndex=${this.startIndex + index}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
index 75a5b5c8..16fc638 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
@@ -5,29 +5,29 @@
  */
 import * as sinon from 'sinon';
 import {
-  GrChangeListSection,
   computeLabelShortcut,
+  GrChangeListSection,
 } from './gr-change-list-section';
 import '../../../test/common-test-setup';
 import './gr-change-list-section';
 import '../gr-change-list-item/gr-change-list-item';
 import {
-  createChange,
   createAccountDetailWithId,
   createAccountWithEmail,
+  createChange,
   createServerInfo,
 } from '../../../test/test-data-generators';
 import {ChangeInfoId, NumericChangeId, Timestamp} from '../../../api/rest-api';
 import {
-  queryAll,
   query,
+  queryAll,
   queryAndAssert,
   stubFlags,
   waitUntilObserved,
 } from '../../../test/test-utils';
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
 import {ChangeListSection} from '../gr-change-list/gr-change-list';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {ColumnNames} from '../../../constants/constants';
 import {testResolver} from '../../../test/common-test-setup';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
@@ -73,7 +73,8 @@
       element,
       /* prettier-ignore */ /* HTML */ `
       <td class="selection">
-        <input class="selection-checkbox" type="checkbox"/>
+        <md-checkbox class="selection-checkbox">
+        </md-checkbox>
       </td>
       #
               SubjectOwnerReviewersRepoBranchUpdatedSizeStatus
@@ -110,7 +111,11 @@
       element,
       /* prettier-ignore */ /* HTML */ `
         <td class="selection">
-          <input class="selection-checkbox" type="checkbox" />
+      <md-checkbox
+        checked=""
+         class="selection-checkbox"
+       >
+      </md-checkbox>
         </td>
         <gr-change-list-action-bar></gr-change-list-action-bar>
         <gr-change-list-item
@@ -203,17 +208,19 @@
         ...createAccountWithEmail('abc@def.com'),
         registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
       });
+      element.bulkActionsLoaded = true;
+      await element.updateComplete;
       await element.updateComplete;
       let rows = queryAll(element, 'gr-change-list-item');
       assert.lengthOf(rows, 2);
       assert.isFalse(
-        queryAndAssert<HTMLInputElement>(rows[0], 'input').checked
+        queryAndAssert<HTMLInputElement>(rows[0], 'md-checkbox').checked
       );
       assert.isFalse(
-        queryAndAssert<HTMLInputElement>(rows[1], 'input').checked
+        queryAndAssert<HTMLInputElement>(rows[1], 'md-checkbox').checked
       );
 
-      const checkbox = queryAndAssert<HTMLInputElement>(element, 'input');
+      const checkbox = queryAndAssert<HTMLInputElement>(element, 'md-checkbox');
       checkbox.click();
       await waitUntilObserved(
         element.bulkActionsModel.selectedChangeNums$,
@@ -223,8 +230,12 @@
 
       rows = queryAll(element, 'gr-change-list-item');
       assert.lengthOf(rows, 2);
-      assert.isTrue(queryAndAssert<HTMLInputElement>(rows[0], 'input').checked);
-      assert.isTrue(queryAndAssert<HTMLInputElement>(rows[1], 'input').checked);
+      assert.isTrue(
+        queryAndAssert<HTMLInputElement>(rows[0], 'md-checkbox').checked
+      );
+      assert.isTrue(
+        queryAndAssert<HTMLInputElement>(rows[1], 'md-checkbox').checked
+      );
     });
 
     test('checkbox matches partial and fully selected state', async () => {
@@ -249,40 +260,42 @@
         ...createAccountWithEmail('abc@def.com'),
         registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
       });
+      element.bulkActionsLoaded = true;
+      await element.updateComplete;
       await element.updateComplete;
       const rows = queryAll(element, 'gr-change-list-item');
 
       // zero case
-      let checkbox = queryAndAssert<HTMLInputElement>(element, 'input');
+      let checkbox = queryAndAssert<HTMLInputElement>(element, 'md-checkbox');
       assert.isFalse(checkbox.checked);
       assert.isFalse(checkbox.indeterminate);
 
       // partial case
-      queryAndAssert<HTMLInputElement>(rows[0], 'input').click();
+      queryAndAssert<HTMLInputElement>(rows[0], 'md-checkbox').click();
       await element.updateComplete;
 
-      checkbox = queryAndAssert<HTMLInputElement>(element, 'input');
+      checkbox = queryAndAssert<HTMLInputElement>(element, 'md-checkbox');
       assert.isTrue(checkbox.indeterminate);
 
       // plural case
-      queryAndAssert<HTMLInputElement>(rows[1], 'input').click();
+      queryAndAssert<HTMLInputElement>(rows[1], 'md-checkbox').click();
       await element.updateComplete;
 
-      checkbox = queryAndAssert<HTMLInputElement>(element, 'input');
+      checkbox = queryAndAssert<HTMLInputElement>(element, 'md-checkbox');
       assert.isFalse(checkbox.indeterminate);
       assert.isTrue(checkbox.checked);
 
       // Clicking Check All checkbox when all checkboxes selected unselects
       // all checkboxes
-      queryAndAssert<HTMLInputElement>(element, 'input');
+      queryAndAssert<HTMLInputElement>(element, 'md-checkbox');
       checkbox.click();
       await element.updateComplete;
 
       assert.isFalse(
-        queryAndAssert<HTMLInputElement>(rows[0], 'input').checked
+        queryAndAssert<HTMLInputElement>(rows[0], 'md-checkbox').checked
       );
       assert.isFalse(
-        queryAndAssert<HTMLInputElement>(rows[1], 'input').checked
+        queryAndAssert<HTMLInputElement>(rows[1], 'md-checkbox').checked
       );
     });
   });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
index 08d3218..2a5d5fe 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
@@ -12,12 +12,10 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-autocomplete/gr-autocomplete';
-import '@polymer/iron-dropdown/iron-dropdown';
-import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
 import {getAppContext} from '../../../services/app-context';
 import {isDefined} from '../../../types/types';
 import {unique} from '../../../utils/common-util';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {when} from 'lit/directives/when.js';
 import {ValueChangedEvent} from '../../../types/events';
 import {classMap} from 'lit/directives/class-map.js';
@@ -29,6 +27,8 @@
 import {pluralize} from '../../../utils/string-util';
 import {Interaction} from '../../../constants/reporting';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import '@material/web/menu/menu';
+import {MdMenu} from '@material/web/menu/menu';
 
 @customElement('gr-change-list-topic-flow')
 export class GrChangeListTopicFlow extends LitElement {
@@ -47,7 +47,7 @@
 
   @state() private overallProgress: ProgressStatus = ProgressStatus.NOT_STARTED;
 
-  @query('iron-dropdown') private dropdown?: IronDropdownElement;
+  @query('md-menu') private dropdown?: MdMenu;
 
   private selectedExistingTopics: Set<TopicName> = new Set();
 
@@ -61,13 +61,15 @@
     return [
       spinnerStyles,
       css`
-        iron-dropdown {
+        md-menu {
+          --md-menu-container-color: var(--dialog-background-color);
+          --md-menu-top-space: 0px;
+          --md-menu-bottom-space: 0px;
+        }
+        .dropdown-content {
           box-shadow: var(--elevation-level-2);
           width: 400px;
-          background-color: var(--dialog-background-color);
           border-radius: 4px;
-        }
-        [slot='dropdown-content'] {
           padding: var(--spacing-xl) var(--spacing-l) var(--spacing-l);
         }
         gr-autocomplete {
@@ -129,6 +131,10 @@
           position: relative;
           top: 7px;
         }
+        .dropdown {
+          position: relative;
+          display: inline-block;
+        }
       `,
     ];
   }
@@ -146,26 +152,26 @@
 
   override render() {
     const isFlowDisabled = this.selectedChanges.length === 0;
-    return html`
+    return html`<div class="dropdown">
       <gr-button
         id="start-flow"
         flatten
         down-arrow
         @click=${this.toggleDropdown}
-        .disabled=${isFlowDisabled}
+        ?disabled=${isFlowDisabled}
         >Topic</gr-button
       >
-      <iron-dropdown
-        .horizontalAlign=${'auto'}
-        .verticalAlign=${'auto'}
-        .verticalOffset=${24}
-        @opened-changed=${(e: ValueChangedEvent<boolean>) =>
-          (this.isDropdownOpen = e.detail.value)}
+      <md-menu
+        anchor="start-flow"
+        tabindex="-1"
+        .quick=${true}
+        @opening=${() => (this.isDropdownOpen = true)}
+        @closing=${() => (this.isDropdownOpen = false)}
       >
         ${when(
           this.isDropdownOpen,
           () => html`
-            <div slot="dropdown-content">
+            <div class="dropdown-content">
               ${when(
                 this.selectedChanges.some(change => change.topic),
                 () => this.renderExistingTopicsMode(),
@@ -174,8 +180,8 @@
             </div>
           `
         )}
-      </iron-dropdown>
-    `;
+      </md-menu>
+    </div> `;
   }
 
   private disableApplyToAllButton() {
@@ -337,7 +343,7 @@
   private openDropdown() {
     this.reset();
     this.isDropdownOpen = true;
-    this.dropdown?.open();
+    this.dropdown?.show();
   }
 
   private async getTopicSuggestions(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
index 087b8a8..23af59c2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
@@ -4,8 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
-import {IronDropdownElement} from '@polymer/iron-dropdown';
+import {assert, fixture, html} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {
   BulkActionsModel,
@@ -35,6 +34,7 @@
 import './gr-change-list-topic-flow';
 import {GrChangeListTopicFlow} from './gr-change-list-topic-flow';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {MdMenu} from '@material/web/menu/menu';
 
 suite('gr-change-list-topic-flow tests', () => {
   let element: GrChangeListTopicFlow;
@@ -100,33 +100,27 @@
       assert.shadowDom.equal(
         element,
         /* HTML */ `
-          <gr-button
-            id="start-flow"
-            flatten=""
-            down-arrow=""
-            aria-disabled="false"
-            role="button"
-            tabindex="0"
-            >Topic</gr-button
-          >
-          <iron-dropdown
-            aria-disabled="false"
-            aria-hidden="true"
-            style="outline: none; display: none;"
-            vertical-align="auto"
-            horizontal-align="auto"
-          >
-          </iron-dropdown>
+          <div class="dropdown">
+            <gr-button
+              aria-disabled="false"
+              down-arrow=""
+              flatten=""
+              id="start-flow"
+              role="button"
+              tabindex="0"
+            >
+              Topic
+            </gr-button>
+            <md-menu anchor="start-flow" aria-hidden="true" tabindex="-1">
+            </md-menu>
+          </div>
         `
       );
     });
 
     test('dropdown hidden before flow button clicked', async () => {
-      const dropdown = queryAndAssert<IronDropdownElement>(
-        element,
-        'iron-dropdown'
-      );
-      assert.isFalse(dropdown.opened);
+      const dropdown = queryAndAssert<MdMenu>(element, 'md-menu');
+      assert.isFalse(dropdown.open);
     });
 
     test('flow button click shows dropdown', async () => {
@@ -135,25 +129,17 @@
       button.click();
       await element.updateComplete;
 
-      const dropdown = queryAndAssert<IronDropdownElement>(
-        element,
-        'iron-dropdown'
-      );
-      assert.isTrue(dropdown.opened);
+      const dropdown = queryAndAssert<MdMenu>(element, 'md-menu');
+      assert.isTrue(dropdown.open);
     });
 
     test('flow button click when open hides dropdown', async () => {
       queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
       await waitUntil(() =>
-        Boolean(
-          queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-        )
+        Boolean(queryAndAssert<MdMenu>(element, 'md-menu').open)
       );
       queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
-      await waitUntil(
-        () =>
-          !queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
-      );
+      await waitUntil(() => !queryAndAssert<MdMenu>(element, 'md-menu').open);
     });
   });
 
@@ -231,66 +217,69 @@
       assert.shadowDom.equal(
         element,
         /* HTML */ `
-          <gr-button
-            id="start-flow"
-            flatten=""
-            down-arrow=""
-            aria-disabled="false"
-            role="button"
-            tabindex="0"
-            >Topic</gr-button
-          >
-          <iron-dropdown
-            aria-disabled="false"
-            vertical-align="auto"
-            horizontal-align="auto"
-          >
-            <div slot="dropdown-content">
-              <div class="chips">
-                <button
-                  role="listbox"
-                  aria-label="topic1 selection"
-                  class="chip"
-                >
-                  topic1
-                </button>
-                <button
-                  role="listbox"
-                  aria-label="topic2 selection"
-                  class="chip"
-                >
-                  topic2
-                </button>
-              </div>
-              <div class="footer">
-                <div class="loadingOrError" role="progressbar"></div>
-                <div class="buttons">
-                  <gr-button
-                    id="apply-to-all-button"
-                    flatten=""
-                    aria-disabled="true"
-                    disabled=""
-                    role="button"
-                    tabindex="-1"
-                    >Apply to all</gr-button
+          <div class="dropdown">
+            <gr-button
+              aria-disabled="false"
+              down-arrow=""
+              flatten=""
+              id="start-flow"
+              role="button"
+              tabindex="0"
+            >
+              Topic
+            </gr-button>
+            <md-menu anchor="start-flow" open="" tabindex="-1">
+              <div class="dropdown-content">
+                <div class="chips">
+                  <button
+                    aria-label="topic1 selection"
+                    class="chip"
+                    role="listbox"
                   >
-                  <gr-button
-                    id="remove-topics-button"
-                    flatten=""
-                    aria-disabled="true"
-                    disabled=""
-                    role="button"
-                    tabindex="-1"
-                    >Remove</gr-button
+                    topic1
+                  </button>
+                  <button
+                    aria-label="topic2 selection"
+                    class="chip"
+                    role="listbox"
                   >
+                    topic2
+                  </button>
+                </div>
+                <div class="footer">
+                  <div class="loadingOrError" role="progressbar"></div>
+                  <div class="buttons">
+                    <gr-button
+                      aria-disabled="true"
+                      disabled=""
+                      flatten=""
+                      id="apply-to-all-button"
+                      role="button"
+                      tabindex="-1"
+                    >
+                      Apply to all
+                    </gr-button>
+                    <gr-button
+                      aria-disabled="true"
+                      disabled=""
+                      flatten=""
+                      id="remove-topics-button"
+                      role="button"
+                      tabindex="-1"
+                    >
+                      Remove
+                    </gr-button>
+                  </div>
                 </div>
               </div>
-            </div>
-          </iron-dropdown>
+            </md-menu>
+          </div>
         `,
         {
           // iron-dropdown sizing seems to vary between local & CI
-          ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
+          ignoreAttributes: [
+            {tags: ['iron-dropdown'], attributes: ['style', 'focused']},
+          ],
         }
       );
     });
@@ -560,45 +549,48 @@
       assert.shadowDom.equal(
         element,
         /* HTML */ `
-          <gr-button
-            id="start-flow"
-            flatten=""
-            down-arrow=""
-            aria-disabled="false"
-            role="button"
-            tabindex="0"
-            >Topic</gr-button
-          >
-          <iron-dropdown
-            aria-disabled="false"
-            vertical-align="auto"
-            horizontal-align="auto"
-          >
-            <div slot="dropdown-content">
-              <gr-autocomplete
-                placeholder="Type topic name to create or filter topics"
-                show-blue-focus-border=""
-              ></gr-autocomplete>
-              <div class="footer">
-                <div class="loadingOrError" role="progressbar"></div>
-                <div class="buttons">
-                  <gr-button
-                    id="set-topic-button"
-                    flatten=""
-                    aria-disabled="true"
-                    disabled=""
-                    role="button"
-                    tabindex="-1"
-                    >Set Topic</gr-button
-                  >
+          <div class="dropdown">
+            <gr-button
+              aria-disabled="false"
+              down-arrow=""
+              flatten=""
+              id="start-flow"
+              role="button"
+              tabindex="0"
+            >
+              Topic
+            </gr-button>
+            <md-menu anchor="start-flow" open="" tabindex="-1">
+              <div class="dropdown-content">
+                <gr-autocomplete
+                  placeholder="Type topic name to create or filter topics"
+                  show-blue-focus-border=""
+                >
+                </gr-autocomplete>
+                <div class="footer">
+                  <div class="loadingOrError" role="progressbar"></div>
+                  <div class="buttons">
+                    <gr-button
+                      aria-disabled="true"
+                      disabled=""
+                      flatten=""
+                      id="set-topic-button"
+                      role="button"
+                      tabindex="-1"
+                    >
+                      Set Topic
+                    </gr-button>
+                  </div>
                 </div>
               </div>
-            </div>
-          </iron-dropdown>
+            </md-menu>
+          </div>
         `,
         {
           // iron-dropdown sizing seems to vary between local & CI
-          ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
+          ignoreAttributes: [
+            {tags: ['iron-dropdown'], attributes: ['style', 'focused']},
+          ],
         }
       );
     });
@@ -613,6 +605,7 @@
         element,
         'gr-autocomplete'
       );
+      autocomplete.debounceWait = 10;
       autocomplete.setFocus(true);
       autocomplete.text = 'foo';
       await element.updateComplete;
@@ -665,6 +658,7 @@
         element,
         'gr-autocomplete'
       );
+      autocomplete.debounceWait = 10;
       autocomplete.setFocus(true);
       autocomplete.text = 'foo';
       await element.updateComplete;
@@ -707,7 +701,7 @@
         element,
         'gr-autocomplete'
       );
-
+      autocomplete.debounceWait = 10;
       autocomplete.setFocus(true);
       autocomplete.text = 'foo';
       await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 81710e8..933d933 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -8,17 +8,16 @@
 import '../gr-user-header/gr-user-header';
 import {
   AccountDetailInfo,
-  AccountId,
   ChangeInfo,
-  EmailAddress,
   RepoName,
+  UserId,
 } from '../../../types/common';
 import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
-import {fireAlert, fire, fireTitleChange} from '../../../utils/event-util';
+import {fire, fireAlert, fireTitleChange} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css, nothing} from 'lit';
-import {customElement, state, query} from 'lit/decorators.js';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
 import {
   createSearchUrl,
   searchViewModelToken,
@@ -62,7 +61,7 @@
   @state() loading = true;
 
   // private but used in test
-  @state() userId?: AccountId | EmailAddress;
+  @state() userId?: UserId;
 
   // private but used in test
   @state() repo?: RepoName;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
index d69eac8..6f7c2a1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -10,7 +10,7 @@
 import {query, queryAndAssert} from '../../../test/test-utils';
 import {createChange} from '../../../test/test-data-generators';
 import {ChangeInfo} from '../../../api/rest-api';
-import {fixture, html, waitUntil, assert} from '@open-wc/testing';
+import {assert, fixture, html, waitUntil} from '@open-wc/testing';
 import {GrChangeList} from '../gr-change-list/gr-change-list';
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
@@ -90,7 +90,7 @@
       await changeListItemEl.updateComplete;
       let checkbox = queryAndAssert<HTMLInputElement>(
         changeListItemEl,
-        '.selection > .selectionLabel > input'
+        '.selection > .selectionLabel > md-checkbox'
       );
       checkbox.click();
       await waitUntil(() => checkbox.checked);
@@ -103,7 +103,7 @@
           query(query(element, 'gr-change-list'), 'gr-change-list-section'),
           'gr-change-list-item'
         ),
-        '.selection > .selectionLabel > input'
+        '.selection > .selectionLabel > md-checkbox'
       );
       assert.isTrue(checkbox.checked);
     });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index b6a834e..061bd3d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -13,8 +13,9 @@
 import {
   AccountInfo,
   ChangeInfo,
-  ServerInfo,
   PreferencesInput,
+  ServerInfo,
+  UserId,
 } from '../../../types/common';
 import {fire, fireReload} from '../../../utils/event-util';
 import {ColumnNames, ScrollMode} from '../../../constants/constants';
@@ -27,7 +28,7 @@
 import {changeListStyles} from '../../../styles/gr-change-list-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {Shortcut, ShortcutController} from '../../lit/shortcut-controller';
 import {queryAll} from '../../../utils/common-util';
@@ -95,7 +96,7 @@
    * generated.
    */
   @property({type: String})
-  dashboardUser?: string;
+  dashboardUser?: UserId | 'self';
 
   @property({type: Array})
   changes?: ChangeInfo[];
@@ -368,12 +369,16 @@
       return this.config?.submit_requirement_dashboard_columns;
     }
     const changes = sections.map(section => section.results).flat();
-    const labels = (changes ?? [])
-      .map(change => getRequirements(change))
-      .flat()
-      .map(requirement => requirement.name)
-      .filter(unique);
-    return orderSubmitRequirementNames(labels);
+    let labels: string[] = [];
+    if (this.config?.dashboard_show_all_labels) {
+      labels = changes.map(change => Object.keys(change.labels ?? {})).flat();
+    } else {
+      labels = changes
+        .map(change => getRequirements(change))
+        .flat()
+        .map(requirement => requirement.name);
+    }
+    return orderSubmitRequirementNames(labels.filter(unique));
   }
 
   private changesChanged() {
@@ -457,7 +462,10 @@
 
   // Private but used in tests
   async sectionsChanged() {
-    this.cursor.stops = await this.getListItems();
+    // Ensure we have a valid array for cursor.stops even if getListItems() returns undefined
+    // This prevents TypeError when forEach is called on cursor.stops
+    const items = await this.getListItems();
+    this.cursor.stops = items ?? [];
     this.cursor.moveToStart();
     if (this.selectedIndex) this.cursor.setCursorAtIndex(this.selectedIndex);
   }
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
new file mode 100644
index 0000000..dee4356
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_screenshot_test.ts
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-change-list';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrChangeList} from './gr-change-list';
+import {createChange} from '../../../test/test-data-generators';
+import {ChangeInfo, NumericChangeId, Timestamp} from '../../../types/common';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+
+suite('gr-change-list screenshot tests', () => {
+  let element: GrChangeList;
+
+  function createChanges(count: number): ChangeInfo[] {
+    return Array.from(Array(count).keys()).map(index => {
+      return {
+        ...createChange(),
+        _number: (index + 1) as NumericChangeId,
+        subject: `Change subject ${index + 1}`,
+        updated: `2020-01-${String(index + 1).padStart(
+          2,
+          '0'
+        )} 10:00:00.000000000` as Timestamp,
+      };
+    });
+  }
+
+  setup(async () => {
+    element = await fixture(html`<gr-change-list></gr-change-list>`);
+    element.changes = createChanges(5);
+    await element.updateComplete;
+  });
+
+  test('basic list', async () => {
+    await visualDiff(element, 'gr-change-list');
+    await visualDiffDarkTheme(element, 'gr-change-list');
+  });
+});
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 d1fa75e..477e75b 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
@@ -8,8 +8,8 @@
 import './gr-change-list';
 import {
   ChangeListSection,
-  GrChangeList,
   computeRelativeIndex,
+  GrChangeList,
 } from './gr-change-list';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
@@ -34,7 +34,7 @@
 } from '../../../test/test-data-generators';
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import {testResolver} from '../../../test/common-test-setup';
 import {Timestamp} from '../../../api/rest-api';
@@ -228,6 +228,40 @@
     );
   });
 
+  test('computed fields with all labels', () => {
+    const createTestLabels = (labelLists: string[][]): ChangeListSection[] => {
+      let changeNumber = 0;
+      const sections: ChangeListSection[] = [];
+      for (const labelList of labelLists) {
+        sections.push({
+          results: [
+            {
+              ...createChange(),
+              _number: changeNumber++ as NumericChangeId,
+              labels: Object.fromEntries(labelList.map(label => [label, {}])),
+            },
+          ],
+        });
+      }
+      return sections;
+    };
+
+    element.config = {
+      ...createServerInfo(),
+      dashboard_show_all_labels: true,
+    };
+    assert.deepEqual(
+      element.computeLabelNames(
+        createTestLabels([
+          ['Verified', 'A-Label'],
+          ['Verified', 'Code-Review'],
+          ['Library-Compliance'],
+        ])
+      ),
+      ['Code-Review', 'A-Label', 'Library-Compliance', 'Verified']
+    );
+  });
+
   test('keyboard shortcuts', async () => {
     sinon.stub(element, 'computeLabelNames');
     element.sections = [
@@ -302,7 +336,10 @@
     await element.updateComplete;
 
     const getCheckbox = (item: GrChangeListItem) =>
-      queryAndAssert<HTMLInputElement>(query(item, '.selection'), 'input');
+      queryAndAssert<HTMLInputElement>(
+        query(item, '.selection'),
+        'md-checkbox'
+      );
 
     sinon.stub(element, 'computeLabelNames');
     element.sections = [
@@ -433,7 +470,7 @@
         const elementClass = '.' + column.trim().toLowerCase();
         const section = queryAndAssert(element, 'gr-change-list-section');
         assert.isFalse(
-          queryAndAssert<HTMLElement>(section, elementClass)!.hidden
+          queryAndAssert<HTMLElement>(section, elementClass).hidden
         );
       }
     });
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
index e4c413f..52a7d7c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
@@ -7,7 +7,7 @@
 import '../../shared/gr-icon/gr-icon';
 import {fire} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement} from 'lit/decorators.js';
 import {fontStyles} from '../../../styles/gr-font-styles';
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts
index d5ab511..4b46131 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts
@@ -8,7 +8,7 @@
 import {GrCreateChangeHelp} from './gr-create-change-help';
 import {mockPromise, queryAndAssert} from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-create-change-help tests', () => {
   let element: GrCreateChangeHelp;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
index cf5c26d..076c45e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
@@ -6,7 +6,7 @@
 import '../../shared/gr-dialog/gr-dialog';
 import '../../shared/gr-shell-command/gr-shell-command';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query} from 'lit/decorators.js';
 import {modalStyles} from '../../../styles/gr-modal-styles';
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts
index 3252e3d..dd5fd459 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.ts
@@ -3,7 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import './gr-create-commands-dialog';
 import {GrCreateCommandsDialog} from './gr-create-commands-dialog';
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
index 16220ba..0d8eac2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.ts
@@ -5,10 +5,10 @@
  */
 import '../../shared/gr-dialog/gr-dialog';
 import '../../shared/gr-repo-branch-picker/gr-repo-branch-picker';
-import {RepoName, BranchName} from '../../../types/common';
+import {BranchName, RepoName} from '../../../types/common';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html} from 'lit';
-import {customElement, state, query} from 'lit/decorators.js';
+import {html, LitElement} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {BindValueChangeEvent} from '../../../types/events';
 import {modalStyles} from '../../../styles/gr-modal-styles';
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts
index cb27aae..da3fd7a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import './gr-create-destination-dialog';
 import {GrCreateDestinationDialog} from './gr-create-destination-dialog';
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index f7a4082..cb7f42f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -20,6 +20,7 @@
   DashboardId,
   PreferencesInput,
   RepoName,
+  UserId,
 } from '../../../types/common';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {GrCreateCommandsDialog} from '../gr-create-commands-dialog/gr-create-commands-dialog';
@@ -29,8 +30,8 @@
 } from '../gr-create-destination-dialog/gr-create-destination-dialog';
 import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
 import {
-  fireAlert,
   fire,
+  fireAlert,
   firePageError,
   fireTitleChange,
 } from '../../../utils/event-util';
@@ -38,8 +39,8 @@
 import {ChangeListSection} from '../gr-change-list/gr-change-list';
 import {a11yStyles} from '../../../styles/gr-a11y-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css, nothing} from 'lit';
-import {customElement, property, state, query} from 'lit/decorators.js';
+import {css, html, LitElement, nothing} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {Shortcut} from '../../../services/shortcuts/shortcuts-config';
 import {ShortcutController} from '../../lit/shortcut-controller';
@@ -376,7 +377,7 @@
   }
 
   // private but used in test
-  computeTitle(user?: string) {
+  computeTitle(user?: UserId | 'self') {
     if (!user || user === 'self') {
       return 'My Reviews';
     }
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
index adf2cfc..d93bae5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
@@ -13,27 +13,29 @@
 import {
   createAccountDetailWithId,
   createChange,
+  createChangeWithStatus,
 } from '../../../test/test-data-generators';
 import {
   addListenerForTest,
+  mockPromise,
+  query,
+  queryAndAssert,
+  stubFlags,
   stubReporting,
   stubRestApi,
-  mockPromise,
-  queryAndAssert,
-  query,
-  stubFlags,
   waitUntil,
 } from '../../../test/test-utils';
 import {
   ChangeInfoId,
   DashboardId,
+  EmailAddress,
   RepoName,
   Timestamp,
 } from '../../../types/common';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {GrCreateChangeHelp} from '../gr-create-change-help/gr-create-change-help';
 import {PageErrorEvent} from '../../../types/events';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {GrButton} from '../../shared/gr-button/gr-button';
@@ -123,7 +125,7 @@
       element.viewState = {
         view: GerritView.DASHBOARD,
         type: DashboardType.CUSTOM,
-        user: 'user',
+        user: 'user@email.com' as EmailAddress,
         sections: [
           {name: 'test1', query: 'test1', hideIfEmpty: true},
           {name: 'test2', query: 'test2', hideIfEmpty: true},
@@ -143,7 +145,7 @@
           query(query(element, 'gr-change-list'), 'gr-change-list-section'),
           'gr-change-list-item'
         ),
-        '.selection > label > input'
+        '.selection > label > md-checkbox'
       );
       checkbox.click();
       await waitUntil(() => checkbox.checked);
@@ -175,7 +177,7 @@
         element.viewState = {
           view: GerritView.DASHBOARD,
           type: DashboardType.USER,
-          user: 'notself',
+          user: 'notself@email.com' as EmailAddress,
           dashboard: '' as DashboardId,
         };
         element.maybeShowDraftsBanner();
@@ -195,7 +197,7 @@
       });
 
       test('no drafts on open changes', () => {
-        const openChange = {...createChange(), status: ChangeStatus.NEW};
+        const openChange = createChangeWithStatus(ChangeStatus.NEW);
         element.results = [
           {countLabel: '', name: '', query: 'has:draft', results: [openChange]},
         ];
@@ -210,7 +212,7 @@
       });
 
       test('no drafts on not open changes', () => {
-        const notOpenChange = {...createChange(), status: '_' as ChangeStatus};
+        const notOpenChange = createChangeWithStatus('_' as ChangeStatus);
         element.results = [
           {
             name: '',
@@ -299,7 +301,10 @@
 
   test('computeTitle', () => {
     assert.equal(element.computeTitle('self'), 'My Reviews');
-    assert.equal(element.computeTitle('not self'), 'Dashboard for not self');
+    assert.equal(
+      element.computeTitle('notself@email.com' as EmailAddress),
+      'Dashboard for notself@email.com'
+    );
   });
 
   suite('computeSectionCountLabel', () => {
@@ -367,7 +372,7 @@
       element.viewState = {
         view: GerritView.DASHBOARD,
         type: DashboardType.CUSTOM,
-        user: 'user',
+        user: 'user@email.com' as EmailAddress,
         dashboard: '' as DashboardId,
         sections: [
           {name: '', query: '1'},
@@ -591,7 +596,7 @@
       view: GerritView.DASHBOARD,
       type: DashboardType.USER,
       dashboard: '' as DashboardId,
-      user: 'user',
+      user: 'user@email.com' as EmailAddress,
     };
     await element.updateComplete;
     assert.isOk(query(element, 'gr-user-header'));
@@ -601,7 +606,7 @@
       type: DashboardType.REPO,
       dashboard: '' as DashboardId,
       project: 'p' as RepoName,
-      user: 'user',
+      user: 'user@email.com' as EmailAddress,
     };
     await element.updateComplete;
     assert.isNotOk(query(element, 'gr-user-header'));
@@ -610,7 +615,7 @@
   test('404 page', async () => {
     const response = {...new Response(), status: 404};
     stubRestApi('getDashboard').callsFake(
-      async (_project, _dashboard, errFn) => {
+      async (_project: any, _dashboard: any, errFn: any) => {
         if (errFn !== undefined) {
           errFn(response);
         }
@@ -627,7 +632,7 @@
       type: DashboardType.REPO,
       dashboard: 'dashboard' as DashboardId,
       project: 'project' as RepoName,
-      user: '',
+      user: undefined,
     };
     await Promise.all([element.reload(), promise]);
   });
@@ -665,7 +670,7 @@
       type: DashboardType.REPO,
       dashboard: 'dashboard' as DashboardId,
       project: 'project' as RepoName,
-      user: '',
+      user: undefined,
     };
     await element.reload();
     assert.isFalse(dashboardDisplayedStub.calledOnce);
@@ -677,7 +682,7 @@
     element.viewState = {
       view: GerritView.DASHBOARD,
       type: DashboardType.USER,
-      user: 'notself',
+      user: 'notself@email.com' as EmailAddress,
     };
     await element.reload();
     assert.isFalse(dashboardDisplayedStub.calledOnce);
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
index 85372aa..f170c5b1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
@@ -9,8 +9,8 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {dashboardHeaderStyles} from '../../../styles/dashboard-header-styles';
-import {LitElement, css, html, PropertyValues} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
 import {createRepoUrl} from '../../../models/views/repo';
 import '../../shared/gr-weblink/gr-weblink';
 
@@ -19,11 +19,11 @@
   @property({type: String})
   repo?: RepoName;
 
-  @property({type: String})
-  _repoUrl: string | null = null;
+  @state()
+  private repoUrl: string | null = null;
 
-  @property({type: Array})
-  _webLinks: WebLinkInfo[] = [];
+  @state()
+  private webLinks: WebLinkInfo[] = [];
 
   private readonly restApiService = getAppContext().restApiService;
 
@@ -46,7 +46,7 @@
     ];
   }
 
-  _renderLinks(webLinks: WebLinkInfo[]) {
+  private renderLinks(webLinks: WebLinkInfo[]) {
     if (!webLinks) return;
     return html`<div>
       <span class="browse">Browse:</span>
@@ -60,31 +60,34 @@
     return html` <div class="info">
       <h1 class="heading-1">${this.repo}</h1>
       <hr />
-      <div>
-        <span>Detail:</span> <a href=${this._repoUrl!}>Repo settings</a>
-      </div>
-      ${this._renderLinks(this._webLinks)}
+      <div><span>Detail:</span> <a href=${this.repoUrl!}>Repo settings</a></div>
+      ${this.renderLinks(this.webLinks)}
     </div>`;
   }
 
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('repo')) {
+      if (this.repo) {
+        this.repoUrl = createRepoUrl({repo: this.repo});
+      } else {
+        this.repoUrl = null;
+      }
+    }
+  }
+
   override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('repo')) {
-      this._repoChanged();
+      this.repoChanged();
     }
   }
 
-  _repoChanged() {
+  private repoChanged() {
     const repo = this.repo;
-    if (!repo) {
-      this._repoUrl = null;
-      return;
-    }
-
-    this._repoUrl = createRepoUrl({repo});
+    if (!repo) return;
 
     this.restApiService.getRepo(repo).then(repo => {
       if (!repo?.web_links) return;
-      this._webLinks = repo.web_links;
+      this.webLinks = repo.web_links;
     });
   }
 }
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.ts
index 9054474..71b2ecd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.ts
@@ -4,10 +4,14 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import './gr-repo-header';
 import {GrRepoHeader} from './gr-repo-header';
-import {stubRestApi} from '../../../test/test-utils';
+import {
+  queryAndAssert,
+  stubRestApi,
+  waitEventLoop,
+} from '../../../test/test-utils';
 import {RepoName, UrlEncodedRepoName} from '../../../types/common';
 
 suite('gr-repo-header tests', () => {
@@ -41,12 +45,17 @@
   test('repoUrl reset once repo changed', async () => {
     element.repo = undefined;
     await element.updateComplete;
-    assert.equal(element._repoUrl, undefined);
+    const href = queryAndAssert(element, 'div.info div a').getAttribute('href');
+    // Lit may render null as empty string or remove attribute depending on version/binding
+    assert.isTrue(href === null || href === '');
 
     element.repo = 'test' as RepoName;
     await element.updateComplete;
 
-    assert.equal(element._repoUrl, '/admin/repos/test');
+    assert.equal(
+      queryAndAssert(element, 'div.info div a').getAttribute('href'),
+      '/admin/repos/test'
+    );
   });
 
   test('webLinks set', async () => {
@@ -63,11 +72,16 @@
     element.repo = undefined;
     await element.updateComplete;
 
-    assert.deepEqual(element._webLinks, []);
+    assert.isEmpty(element.shadowRoot!.querySelectorAll('gr-weblink'));
 
     element.repo = 'test' as RepoName;
     await element.updateComplete;
+    // Wait for the promise in repoChanged to resolve and trigger update
+    await waitEventLoop();
+    await element.updateComplete;
 
-    assert.deepEqual(element._webLinks, repoRes.web_links);
+    const links = element.shadowRoot!.querySelectorAll('gr-weblink');
+    assert.equal(links.length, 1);
+    assert.deepEqual(links[0].info, repoRes.web_links[0]);
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index d1bba95..8509156 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -7,23 +7,23 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../shared/gr-avatar/gr-avatar';
 import '../../shared/gr-date-formatter/gr-date-formatter';
-import {AccountDetailInfo, AccountId} from '../../../types/common';
+import {AccountDetailInfo, UserId} from '../../../types/common';
 import {getDisplayName} from '../../../utils/display-name-util';
 import {getAppContext} from '../../../services/app-context';
 import {dashboardHeaderStyles} from '../../../styles/dashboard-header-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
-import {LitElement, css, html, PropertyValues} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
 import {
-  DashboardType,
   createDashboardUrl,
+  DashboardType,
 } from '../../../models/views/dashboard';
 
 @customElement('gr-user-header')
 export class GrUserHeader extends LitElement {
   @property({type: String})
-  userId?: AccountId;
+  userId?: UserId;
 
   @property({type: Boolean})
   showDashboardLink = false;
@@ -31,11 +31,11 @@
   @property({type: Boolean})
   loggedIn = false;
 
-  @property({type: Object})
-  _accountDetails: AccountDetailInfo | undefined;
+  @state()
+  private accountDetails: AccountDetailInfo | undefined;
 
-  @property({type: String})
-  _status = '';
+  @state()
+  private status = '';
 
   private readonly restApiService = getAppContext().restApiService;
 
@@ -56,37 +56,34 @@
 
   override render() {
     return html`<gr-avatar
-        .account=${this._accountDetails}
+        .account=${this.accountDetails}
         .imageSize=${100}
         aria-label="Account avatar"
       ></gr-avatar>
       <div class="info">
-        <h1 class="heading-1">${this._computeHeading(this._accountDetails)}</h1>
+        <h1 class="heading-1">${this.computeHeading(this.accountDetails)}</h1>
         <hr />
-        <div class="status ${this._computeStatusClass(this._status)}">
-          <span>Status:</span> ${this._status}
+        <div class="status ${this.computeStatusClass(this.status)}">
+          <span>Status:</span> ${this.status}
         </div>
         <div>
           <span>Email:</span>
-          <a href="mailto:${this._computeDetail(this._accountDetails, 'email')}"
+          <a href="mailto:${this.computeDetail(this.accountDetails, 'email')}"
             ><!--
-          -->${this._computeDetail(this._accountDetails, 'email')}</a
+          -->${this.computeDetail(this.accountDetails, 'email')}</a
           >
         </div>
         <div>
           <span>Joined:</span>
           <gr-date-formatter
-            dateStr=${this._computeDetail(
-              this._accountDetails,
-              'registered_on'
-            )}
+            dateStr=${this.computeDetail(this.accountDetails, 'registered_on')}
           >
           </gr-date-formatter>
         </div>
         <gr-endpoint-decorator name="user-header">
           <gr-endpoint-param
             name="accountDetails"
-            .value=${this._accountDetails}
+            .value=${this.accountDetails}
           >
           </gr-endpoint-param>
           <gr-endpoint-param name="loggedIn" .value=${this.loggedIn}>
@@ -95,61 +92,60 @@
       </div>
       <div class="info">
         <div
-          class=${this._computeDashboardLinkClass(
+          class=${this.computeDashboardLinkClass(
             this.showDashboardLink,
             this.loggedIn
           )}
         >
-          <a href=${this._computeDashboardUrl(this._accountDetails)}
+          <a href=${this.computeDashboardUrl(this.accountDetails)}
             >View dashboard</a
           >
         </div>
       </div>`;
   }
 
-  override updated(changedProperties: PropertyValues) {
+  override willUpdate(changedProperties: PropertyValues) {
     if (changedProperties.has('userId')) {
-      this._accountChanged(this.userId);
+      this.accountChanged(this.userId);
     }
   }
 
-  _accountChanged(userId?: AccountId) {
+  private accountChanged(userId?: UserId) {
     if (!userId) {
-      this._accountDetails = undefined;
-      this._status = '';
+      this.accountDetails = undefined;
+      this.status = '';
       return;
     }
 
     this.restApiService
       .getAccountDetails(userId, () => {})
       .then(details => {
-        this._accountDetails = details ?? undefined;
-        this._status = details?.status ?? '';
+        this.accountDetails = details ?? undefined;
+        this.status = details?.status ?? '';
       });
   }
 
-  _computeDetail(
+  private computeDetail(
     accountDetails: AccountDetailInfo | undefined,
     name: keyof AccountDetailInfo
   ) {
     return accountDetails ? String(accountDetails[name]) : '';
   }
 
-  _computeHeading(accountDetails: AccountDetailInfo | undefined) {
+  private computeHeading(accountDetails: AccountDetailInfo | undefined) {
     if (!accountDetails) return '';
     return getDisplayName(undefined, accountDetails);
   }
 
-  _computeStatusClass(status: string) {
+  private computeStatusClass(status: string) {
     return status ? '' : 'hide';
   }
 
-  _computeDashboardUrl(accountDetails: AccountDetailInfo | undefined) {
+  private computeDashboardUrl(accountDetails: AccountDetailInfo | undefined) {
     if (!accountDetails) return '';
 
     const id = accountDetails._account_id;
-    if (id)
-      return createDashboardUrl({type: DashboardType.USER, user: String(id)});
+    if (id) return createDashboardUrl({type: DashboardType.USER, user: id});
 
     const email = accountDetails.email;
     if (email)
@@ -158,7 +154,10 @@
     return '';
   }
 
-  _computeDashboardLinkClass(showDashboardLink: boolean, loggedIn: boolean) {
+  private computeDashboardLinkClass(
+    showDashboardLink: boolean,
+    loggedIn: boolean
+  ) {
     return showDashboardLink && loggedIn
       ? 'dashboardLink'
       : 'dashboardLink hide';
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.ts
index 7d204d9..8d249d5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.ts
@@ -4,10 +4,14 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import './gr-user-header';
 import {GrUserHeader} from './gr-user-header';
-import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
+import {
+  queryAndAssert,
+  stubRestApi,
+  waitEventLoop,
+} from '../../../test/test-utils';
 import {AccountId, EmailAddress, Timestamp} from '../../../types/common';
 
 suite('gr-user-header tests', () => {
@@ -63,20 +67,43 @@
     element.userId = 10 as AccountId;
     await waitEventLoop();
 
-    assert.isOk(element._accountDetails);
-    assert.isOk(element._status);
+    assert.ok(element.shadowRoot!.querySelector('h1.heading-1'));
+    // We can check if status is visible
+    const statusDiv = queryAndAssert(element, '.status');
+    assert.notInclude(statusDiv.className, 'hide');
+    assert.include(statusDiv.textContent, 'OOO');
 
     element.userId = undefined;
     await waitEventLoop();
 
-    assert.isUndefined(element._accountDetails);
-    assert.equal(element._status, '');
+    // Check if status is hidden or empty
+    const statusDivAfter = queryAndAssert(element, '.status');
+    // It should be hidden or empty
+    assert.include(statusDivAfter.className, 'hide');
   });
 
-  test('_computeDashboardLinkClass', () => {
-    assert.include(element._computeDashboardLinkClass(false, false), 'hide');
-    assert.include(element._computeDashboardLinkClass(true, false), 'hide');
-    assert.include(element._computeDashboardLinkClass(false, true), 'hide');
-    assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
+  test('dashboard link class', async () => {
+    element.showDashboardLink = false;
+    element.loggedIn = false;
+    await element.updateComplete;
+    assert.include(queryAndAssert(element, '.dashboardLink').className, 'hide');
+
+    element.showDashboardLink = true;
+    element.loggedIn = false;
+    await element.updateComplete;
+    assert.include(queryAndAssert(element, '.dashboardLink').className, 'hide');
+
+    element.showDashboardLink = false;
+    element.loggedIn = true;
+    await element.updateComplete;
+    assert.include(queryAndAssert(element, '.dashboardLink').className, 'hide');
+
+    element.showDashboardLink = true;
+    element.loggedIn = true;
+    await element.updateComplete;
+    assert.notInclude(
+      queryAndAssert(element, '.dashboardLink').className,
+      'hide'
+    );
   });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts
new file mode 100644
index 0000000..a2c2e6d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts
@@ -0,0 +1,353 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {fire} from '../../../utils/event-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {getAppContext} from '../../../services/app-context';
+import {fireError} from '../../../utils/event-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {changeModelToken} from '../../../models/change/change-model';
+import {ParsedChangeInfo} from '../../../types/types';
+import {PatchSetNum} from '../../../types/common';
+import {HELP_ME_REVIEW_PROMPT, IMPROVE_COMMIT_MESSAGE} from './prompts';
+import {when} from 'lit/directives/when.js';
+import {copyToClipboard} from '../../../utils/common-util';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/radio/radio';
+
+const PROMPT_TEMPLATES = {
+  HELP_REVIEW: {
+    id: 'help_review',
+    label: 'Help me with review',
+    prompt: HELP_ME_REVIEW_PROMPT,
+  },
+  IMPROVE_COMMIT_MESSAGE: {
+    id: 'improve_commit_message',
+    label: 'Improve commit message',
+    prompt: IMPROVE_COMMIT_MESSAGE,
+  },
+  PATCH_ONLY: {
+    id: 'patch_only',
+    label: 'Just patch content',
+    prompt: '{{patch}}',
+  },
+};
+
+const CONTEXT_OPTIONS = [
+  {label: '3 lines (default)', value: 3},
+  {label: '10 lines', value: 10},
+  {label: '25 lines', value: 25},
+  {label: '50 lines', value: 50},
+  {label: '100 lines', value: 100},
+];
+
+type PromptTemplateId = keyof typeof PROMPT_TEMPLATES;
+
+@customElement('gr-ai-prompt-dialog')
+export class GrAiPromptDialog extends LitElement {
+  /**
+   * Fired when the user presses the close button.
+   *
+   * @event close
+   */
+
+  @query('#closeButton') protected closeButton?: GrButton;
+
+  @state() change?: ParsedChangeInfo;
+
+  @state() patchNum?: PatchSetNum;
+
+  @state() patchContent?: string;
+
+  @state() loading = false;
+
+  @state() selectedTemplate: PromptTemplateId = 'HELP_REVIEW';
+
+  @state() private context = 3;
+
+  @state() private promptContent = '';
+
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getChangeModel().change$,
+      x => (this.change = x)
+    );
+    subscribe(
+      this,
+      () => this.getChangeModel().patchNum$,
+      x => (this.patchNum = x)
+    );
+  }
+
+  static override get styles() {
+    return [
+      fontStyles,
+      sharedStyles,
+      modalStyles,
+      materialStyles,
+      css`
+        :host {
+          display: block;
+          padding: var(--spacing-m) 0;
+        }
+        section {
+          display: flex;
+          padding: var(--spacing-m) var(--spacing-xl);
+        }
+        .flexContainer {
+          display: flex;
+          justify-content: space-between;
+          padding-top: var(--spacing-m);
+        }
+        .footer {
+          justify-content: flex-end;
+        }
+        .info-text {
+          font-size: var(--font-size-small);
+          color: var(--deactivated-text-color);
+          max-width: 420px;
+        }
+        .closeButtonContainer {
+          align-items: flex-end;
+          display: flex;
+          flex: 0;
+          justify-content: flex-end;
+        }
+        .content {
+          width: 100%;
+        }
+        .options-bar {
+          display: flex;
+          justify-content: space-between;
+          align-items: flex-start;
+          margin-bottom: var(--spacing-m);
+        }
+        .template-selector {
+        }
+        .template-options {
+          display: flex;
+          flex-direction: column;
+          gap: var(--spacing-s);
+        }
+        .template-option {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-m);
+          margin-bottom: 0.5em;
+        }
+        textarea {
+          width: 550px;
+          height: 300px;
+          font-family: var(--monospace-font-family);
+          font-size: var(--font-size-mono);
+          line-height: var(--line-height-mono);
+          padding: var(--spacing-s);
+          border: 1px solid var(--border-color);
+          border-radius: var(--border-radius);
+          resize: vertical;
+        }
+        .toolbar {
+          display: flex;
+          gap: var(--spacing-s);
+          margin-top: var(--spacing-s);
+          justify-content: space-between;
+          align-items: center;
+        }
+        .context-selector {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-s);
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <section>
+        <h3 class="heading-3">Copy AI Prompt (experimental)</h3>
+      </section>
+      ${when(
+        this.loading,
+        () => html` <div class="loading">Loading patch ...</div>`,
+        () => html` <section class="flexContainer">
+          <div class="content">
+            ${when(
+              this.getNumParents() === 1,
+              () => html`<div class="options-bar">
+                  <div class="template-selector">
+                    <div class="template-options">
+                      ${Object.entries(PROMPT_TEMPLATES).map(
+                        ([key, template]) => html`
+                          <label class="template-option">
+                            <md-radio
+                              name="template"
+                              .value=${key}
+                              ?checked=${this.selectedTemplate === key}
+                              @change=${(e: Event) => {
+                                const input = e.target as HTMLInputElement;
+                                this.selectedTemplate =
+                                  input.value as PromptTemplateId;
+                              }}
+                            >
+                            </md-radio>
+                            ${template.label}
+                          </label>
+                        `
+                      )}
+                    </div>
+                  </div>
+                  <div class="context-selector">
+                    <md-outlined-select
+                      label="Context"
+                      value=${this.context.toString()}
+                      @change=${(e: Event) => {
+                        const select = e.target as HTMLSelectElement;
+                        this.context = Number(select.value);
+                      }}
+                    >
+                      ${CONTEXT_OPTIONS.map(
+                        option =>
+                          html`<md-select-option
+                            .value=${option.value.toString()}
+                          >
+                            <div slot="headline">${option.label}</div>
+                          </md-select-option>`
+                      )}
+                    </md-outlined-select>
+                  </div>
+                </div>
+                <textarea
+                  .value=${this.promptContent}
+                  readonly
+                  placeholder="Patch content will appear here..."
+                ></textarea>
+                <div class="toolbar">
+                  <div class="info-text">
+                    You can paste this prompt in an AI Model if your project
+                    code can be shared with AI. We recommend a thinking model.
+                    You can also use it for an AI Agent as context (a reference
+                    to a git change).
+                  </div>
+                  <gr-button @click=${this.handleCopyPatch}>
+                    <gr-icon icon="content_copy" small></gr-icon>
+                    Copy Prompt
+                  </gr-button>
+                </div>`,
+              () => html`
+                <div class="info-text">
+                  This change has multiple parents. Currently, the "Create AI
+                  Review Prompt" feature does not support multiple parents.
+                </div>
+              `
+            )}
+          </div>
+        </section>`
+      )}
+      <section class="footer">
+        <span class="closeButtonContainer">
+          <gr-button
+            id="closeButton"
+            link
+            @click=${(e: Event) => {
+              this.handleCloseTap(e);
+            }}
+            >Close</gr-button
+          >
+        </span>
+      </section>
+    `;
+  }
+
+  override firstUpdated(changedProperties: PropertyValues) {
+    super.firstUpdated(changedProperties);
+    if (!this.getAttribute('role')) this.setAttribute('role', 'dialog');
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (
+      changedProperties.has('patchContent') ||
+      changedProperties.has('selectedTemplate')
+    ) {
+      this.updatePromptContent();
+    }
+    if (changedProperties.has('context')) {
+      this.loadPatchContent();
+    }
+  }
+
+  open() {
+    if (this.getNumParents() === 1) {
+      this.loadPatchContent();
+    }
+  }
+
+  private getNumParents() {
+    return this.change?.revisions[this.change.current_revision].commit?.parents
+      .length;
+  }
+
+  private async loadPatchContent() {
+    if (!this.change || !this.patchNum) return;
+    this.loading = true;
+    const content = await this.restApiService.getPatchContent(
+      this.change._number,
+      this.patchNum,
+      this.context
+    );
+    this.loading = false;
+    if (!content) {
+      fireError(this, 'Failed to get patch content');
+      return;
+    }
+    this.patchContent = content;
+  }
+
+  private updatePromptContent() {
+    if (!this.patchContent) {
+      this.promptContent = '';
+      return;
+    }
+    const template = PROMPT_TEMPLATES[this.selectedTemplate];
+    this.promptContent = template.prompt.replace(
+      '{{patch}}',
+      this.patchContent
+    );
+  }
+
+  private async handleCopyPatch(e: Event) {
+    e.preventDefault();
+    e.stopPropagation();
+    if (!this.promptContent) return;
+    await copyToClipboard(this.promptContent, 'AI Prompt');
+  }
+
+  private handleCloseTap(e: Event) {
+    e.preventDefault();
+    e.stopPropagation();
+    fire(this, 'close', {});
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-ai-prompt-dialog': GrAiPromptDialog;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts
new file mode 100644
index 0000000..c72d0c3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-ai-prompt-dialog';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrAiPromptDialog} from './gr-ai-prompt-dialog';
+import {createParsedChange} from '../../../test/test-data-generators';
+import {CommitId, PatchSetNum} from '../../../api/rest-api';
+import {stubRestApi} from '../../../test/test-utils';
+
+suite('gr-ai-prompt-dialog test', () => {
+  let element: GrAiPromptDialog;
+  setup(async () => {
+    stubRestApi('getPatchContent').returns(Promise.resolve('<patch>'));
+    element = await fixture(html`<gr-ai-prompt-dialog></gr-ai-prompt-dialog>`);
+    element.change = createParsedChange();
+    element.change.revisions['abc'].commit!.parents = [
+      {
+        commit: 'def' as CommitId,
+        subject: 'Parent commit subject',
+      },
+    ];
+    element.patchNum = 1 as PatchSetNum;
+    await element.updateComplete;
+  });
+
+  test('renders', async () => {
+    assert.shadowDom.equal(
+      element,
+      ` <section>
+         <h3 class="heading-3">
+           Copy AI Prompt (experimental)
+         </h3>
+       </section>
+       <section class="flexContainer">
+         <div class="content">
+           <div class="options-bar">
+             <div class="template-selector">
+               <div class="template-options">
+                 <label class="template-option">
+                   <md-radio
+                     checked=""
+                     name="template"
+                     tabindex="0"
+                   >
+                   </md-radio>
+                   Help me with review
+                 </label>
+                 <label class="template-option">
+                   <md-radio
+                     name="template"
+                     tabindex="-1"
+                   >
+                   </md-radio>
+                   Improve commit message
+                 </label>
+                 <label class="template-option">
+                   <md-radio
+                     name="template"
+                     tabindex="-1"
+                   >
+                   </md-radio>
+                   Just patch content
+                 </label>
+               </div>
+             </div>
+             <div class="context-selector">
+               <md-outlined-select
+                 label="Context"
+                 value="3"
+               >
+                 <md-select-option md-menu-item="">
+                   <div slot="headline">
+                     3 lines (default)
+                   </div>
+                 </md-select-option>
+                 <md-select-option md-menu-item="">
+                   <div slot="headline">
+                     10 lines
+                   </div>
+                 </md-select-option>
+                 <md-select-option md-menu-item="">
+                   <div slot="headline">
+                     25 lines
+                   </div>
+                 </md-select-option>
+                 <md-select-option md-menu-item="">
+                   <div slot="headline">
+                     50 lines
+                   </div>
+                 </md-select-option>
+                 <md-select-option md-menu-item="">
+                   <div slot="headline">
+                     100 lines
+                   </div>
+                 </md-select-option>
+               </md-outlined-select>
+             </div>
+           </div>
+           <textarea
+             placeholder="Patch content will appear here..."
+             readonly=""
+           >
+           </textarea>
+           <div class="toolbar">
+             <div class="info-text">
+               You can paste this prompt in an AI Model if your project
+                    code can be shared with AI. We recommend a thinking model.
+                    You can also use it for an AI Agent as context (a reference
+                    to a git change).
+             </div>
+             <gr-button>
+               <gr-icon
+                 icon="content_copy"
+                 small=""
+               >
+               </gr-icon>
+               Copy Prompt
+             </gr-button>
+           </div>
+         </div>
+       </section>
+       <section class="footer">
+         <span class="closeButtonContainer">
+           <gr-button
+             id="closeButton"
+             link=""
+           >
+             Close
+           </gr-button>
+         </span>
+       </section>`
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/prompts.ts b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/prompts.ts
new file mode 100644
index 0000000..e44ab1a
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/prompts.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const HELP_ME_REVIEW_PROMPT = `
+You are a highly experienced code reviewer specializing in Git patches. Your
+task is to analyze the provided Git patch (\`patch\`) and provide comprehensive
+feedback.  Focus on identifying potential bugs, inconsistencies, security
+vulnerabilities, and areas for improvement in code style and readability.
+Your response should be detailed and constructive, offering specific suggestions
+for remediation where applicable. Prioritize clarity and conciseness in your
+feedback.
+
+# Step by Step Instructions
+
+1.  Read the provided \`patch\` carefully.  Understand the changes it introduces to the codebase.
+
+2.  Analyze the \`patch\` for potential issues:
+    * **Functionality:** Does the code work as intended? Are there any bugs or unexpected behavior?
+    * **Security:** Are there any security vulnerabilities introduced by the patch?
+    * **Style:** Does the code adhere to the project's coding style guidelines? Is it readable and maintainable?
+    * **Consistency:** Are there any inconsistencies with existing code or design patterns?
+    * **Testing:** Does the patch include sufficient tests to cover the changes?
+
+3.  Formulate concise and constructive feedback for each identified issue.  Provide specific suggestions for remediation where possible.
+
+4.  Summarize your findings in a clear and organized manner.  Prioritize critical issues over minor ones.
+
+5.  Review the feedback written so far. Is the feedback comprehensive and sufficiently detailed?
+If not, go back to step 2, focusing on any areas that require further analysis or clarification.
+ If yes, proceed to step 6.
+
+6.  Output the complete review.
+
+
+Patch: 
+"""
+{{patch}}
+"""
+IMPORTANT NOTE: Start directly with the output, do not output any delimiters.
+Take a Deep Breath, read the instructions again, read the inputs again. Each instruction is crucial and must be executed with utmost care and attention to detail.
+
+Review:
+`;
+
+export const IMPROVE_COMMIT_MESSAGE = `
+You are a Git commit message expert, tasked with improving the quality and clarity of commit messages.
+Your goal is to generate a well-structured and informative commit message based on a provided Git patch. 
+The commit message must adhere to a specific style guide, focusing on conciseness, clarity, and a professional tone.
+You will use the patch's diff to understand the changes, summarizing complex diffs and focusing on the intent and impact of the changes.
+You should paraphrase any provided bug summaries to explain the problem that was fixed.
+Your output must be a single Markdown code block containing only the complete commit message (title and body), 
+formatted according to the provided specifications.
+
+# Step by Step Instructions
+
+1. **Analyze the Patch:** Carefully examine the provided \`patch\` to understand the changes made to the codebase. 
+Identify the key modifications, focusing on their intent and impact. Summarize complex changes concisely.
+
+2. **Review Existing Commit Message:** Read the commit message included in the \`patch\`. Note its strengths and weaknesses.  
+Identify areas for improvement in clarity, conciseness, and adherence to the style guide.
+
+3. **Refine the Title:** Craft a concise and informative commit title (under 60 characters) using sentence case and the imperative mood. 
+The title should accurately reflect the primary change implemented in the patch.
+
+4. **Develop the Body:** Write a detailed body for the commit message, explaining the "what" and "why" of the changes.
+ Use the information gathered in Step 1 to describe the intent and impact of the modifications.  
+ Structure the body using paragraphs, blank lines, and bullet points as needed for clarity. Wrap lines to approximately 72 characters.
+
+5. **Ensure Style Compliance:** Verify that the commit message (title and body) adheres to all requirements outlined in the provided
+"Commit Message Requirements" section.  This includes checking for sentence case, imperative mood, line wrapping, and the exclusion of testing information.
+
+6. **Format the Output:**  Enclose the complete commit message (title and body) within a single Markdown code block.
+ Ensure there is one blank line separating the title and the body.
+
+7. **Review and Iterate (Loop Instruction):** Review the complete commit message. Is it clear, concise, and informative? 
+Does it accurately reflect the changes made in the patch and adhere to the style guide? If not, return to Step 3 or Step 4 to make improvements.\
+  If satisfied, proceed to Step 8.
+
+8. **Output the Commit Message:** Output the final, formatted commit message as a single Markdown code block.
+
+
+Patch: 
+"""
+{{patch}}
+"""
+IMPORTANT NOTE: Start directly with the output, do not output any delimiters.
+Take a Deep Breath, read the instructions again, read the inputs again. Each instruction is crucial and must be executed with utmost care and attention to detail.
+
+Commit Message:
+`;
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 81b7bdf..2a2a132 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
@@ -44,7 +44,6 @@
   CherryPickInput,
   CommentThread,
   CommitId,
-  InheritedBooleanInfo,
   isDetailedLabelInfo,
   isQuickLabelInfo,
   LabelInfo,
@@ -54,6 +53,7 @@
   RequestPayload,
   RevertSubmissionInfo,
   ReviewInput,
+  RevisionInfo,
 } from '../../../types/common';
 import {GrConfirmAbandonDialog} from '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
@@ -99,7 +99,7 @@
 import {resolve} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {assertIsDefined, queryAll, uuid} from '../../../utils/common-util';
@@ -115,7 +115,6 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {userModelToken} from '../../../models/user/user-model';
 import {ParsedChangeInfo} from '../../../types/types';
-import {configModelToken} from '../../../models/config/config-model';
 import {readJSONResponsePayload} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {when} from 'lit/directives/when.js';
@@ -405,8 +404,6 @@
   // The DOWNLOAD action is also added to it in `actionsChanged()`.
   @state() revisionActions?: ActionNameToActionInfoMap;
 
-  @state() privateByDefault?: InheritedBooleanInfo;
-
   @state() actionLoadingMessage = '';
 
   @state() inProgressActionKeys = new Set<string>();
@@ -496,8 +493,6 @@
 
   private readonly getUserModel = resolve(this, userModelToken);
 
-  private readonly getConfigModel = resolve(this, configModelToken);
-
   private readonly getChangeModel = resolve(this, changeModelToken);
 
   private readonly getStorage = resolve(this, storageServiceToken);
@@ -575,11 +570,6 @@
     );
     subscribe(
       this,
-      () => this.getConfigModel().repoConfig$,
-      config => (this.privateByDefault = config?.private_by_default)
-    );
-    subscribe(
-      this,
       () => this.getCommentsModel().threadsWithUnappliedSuggestions$,
       x => (this.threadsWithUnappliedSuggestions = x)
     );
@@ -699,8 +689,6 @@
         <gr-dropdown
           id="moreActions"
           link
-          .verticalOffset=${32}
-          .horizontalAlign=${'right'}
           @tap-item=${this.handleOverflowItemTap}
           ?hidden=${this.isLoading() ||
           !this.menuActions ||
@@ -778,9 +766,8 @@
             <gr-create-change-dialog
               id="createFollowUpChange"
               .branch=${this.change?.branch}
-              .baseChange=${this.change?.id}
+              .baseChange=${this.change?.change_id}
               .repoName=${this.change?.project}
-              .privateByDefault=${this.privateByDefault}
             ></gr-create-change-dialog>
           </div>
         </gr-dialog>
@@ -860,7 +847,7 @@
   private renderUIActionIcon(action: UIActionInfo) {
     if (!action.icon) return nothing;
     return html`
-      <gr-icon icon=${action.icon} ?filled=${action.filled}></gr-icon>
+      <gr-icon icon=${action.icon} ?filled=${!!action.filled}></gr-icon>
     `;
   }
 
@@ -1040,12 +1027,7 @@
   }
 
   private indexOfActionButtonWithKey(key: string) {
-    for (let i = 0; i < this.additionalActions.length; i++) {
-      if (this.additionalActions[i].__key === key) {
-        return i;
-      }
-    }
-    return -1;
+    return this.additionalActions.findIndex(action => action.__key === key);
   }
 
   private actionsChanged() {
@@ -1368,7 +1350,10 @@
   }
 
   // private but used in test
-  getRevision(change: ChangeInfo, patchNum?: PatchSetNumber) {
+  getRevision(
+    change: ChangeInfo,
+    patchNum?: PatchSetNumber
+  ): RevisionInfo | null {
     for (const rev of Object.values(change.revisions ?? {})) {
       if (rev._number === patchNum) {
         return rev;
@@ -1409,7 +1394,7 @@
     this.showActionDialog(this.confirmSubmitDialog);
   }
 
-  private handleActionTap(e: MouseEvent, key: string, type: string) {
+  private async handleActionTap(e: MouseEvent, key: string, type: string) {
     e.preventDefault();
     let el = e.target as Element;
     while (el.tagName.toLowerCase() !== 'gr-button') {
@@ -1432,10 +1417,10 @@
       );
       return;
     }
-    this.handleAction(type as ActionType, key);
+    await this.handleAction(type as ActionType, key);
   }
 
-  private handleOverflowItemTap(e: CustomEvent<MenuAction>) {
+  private async handleOverflowItemTap(e: CustomEvent<MenuAction>) {
     e.preventDefault();
     const el = e.target as Element;
     const key = e.detail.action.__key;
@@ -1452,18 +1437,18 @@
       );
       return;
     }
-    this.handleAction(e.detail.action.__type, e.detail.action.__key);
+    await this.handleAction(e.detail.action.__type, e.detail.action.__key);
   }
 
   // private but used in test
-  handleAction(type: ActionType, key: string) {
+  async handleAction(type: ActionType, key: string) {
     this.reporting.reportInteraction(`${type}-${key}`);
     switch (type) {
       case ActionType.REVISION:
         this.handleRevisionAction(key);
         break;
       case ActionType.CHANGE:
-        this.handleChangeAction(key);
+        await this.handleChangeAction(key);
         break;
       default:
         this.fireAction(
@@ -1475,10 +1460,17 @@
   }
 
   // private but used in test
-  handleChangeAction(key: string) {
+  async handleChangeAction(key: string) {
+    if (
+      !(await this.getPluginLoader().jsApiService.handleBeforeChangeAction(
+        key,
+        this.change
+      ))
+    )
+      return;
     switch (key) {
       case ChangeActions.REVERT:
-        this.showRevertDialog();
+        void this.showRevertDialog();
         break;
       case ChangeActions.ABANDON:
         assertIsDefined(this.confirmAbandonDialog, 'confirmAbandonDialog');
@@ -1861,7 +1853,7 @@
     action: UIActionInfo,
     revAction: boolean,
     payload?: RequestPayload,
-    toReport?: Object
+    toReport?: object
   ) {
     const cleanupFn = this.setLoadingOnButtonWithKey(action);
     this.reporting.reportInteraction(Interaction.CHANGE_ACTION_FIRED, {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index c566280..ed4c9c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -45,11 +45,10 @@
 } from '../../../types/common';
 import {ActionType, RevisionActions} from '../../../api/change-actions';
 import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {UIActionInfo} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
 import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
 import {GrConfirmSubmitDialog} from '../gr-confirm-submit-dialog/gr-confirm-submit-dialog';
@@ -68,6 +67,7 @@
   changeModelToken,
 } from '../../../models/change/change-model';
 import {assertIsDefined} from '../../../utils/common-util';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 
 // TODO(dhruvsri): remove use of _populateRevertMessage as it's private
 suite('gr-change-actions tests', () => {
@@ -398,7 +398,7 @@
         )
       );
       assert.equal(
-        (element.revisionActions['plugin~action'] as UIActionInfo)!.__url,
+        (element.revisionActions['plugin~action'] as UIActionInfo).__url,
         'the-url'
       );
     });
@@ -416,7 +416,7 @@
         stub.calledWith(element.changeNum, undefined, '/plugin~action')
       );
       assert.equal(
-        (element.actions['plugin~action'] as UIActionInfo)!.__url,
+        (element.actions['plugin~action'] as UIActionInfo).__url,
         'the-url'
       );
     });
@@ -645,9 +645,9 @@
       const fetchChangesStub = sinon
         .stub(
           queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase'),
-          'fetchRecentChanges'
+          'initiateFetchInfo'
         )
-        .returns(Promise.resolve([]));
+        .returns();
       await element.updateComplete;
       queryAndAssert<GrButton>(
         element,
@@ -696,8 +696,8 @@
         '#confirmRebase'
       );
       const fetchChangesStub = sinon
-        .stub(confirmRebaseDialog, 'fetchRecentChanges')
-        .returns(Promise.resolve([]));
+        .stub(confirmRebaseDialog, 'initiateFetchInfo')
+        .returns();
       sinon
         .stub(confirmRebaseDialog, 'getValidationOptions')
         .returns([{name: 'o1', description: 'option 1'}]);
@@ -754,9 +754,9 @@
       const fetchChangesStub = sinon
         .stub(
           queryAndAssert<GrConfirmRebaseDialog>(element, '#confirmRebase'),
-          'fetchRecentChanges'
+          'initiateFetchInfo'
         )
-        .returns(Promise.resolve([]));
+        .returns();
       await element.updateComplete;
       const rebaseButton = queryAndAssert<GrButton>(
         element,
@@ -1083,7 +1083,7 @@
         element.handleCherrypickConfirm();
         await element.updateComplete;
 
-        const autogrowEl = queryAndAssert<IronAutogrowTextareaElement>(
+        const autogrowEl = queryAndAssert<GrAutogrowTextarea>(
           queryAndAssert<GrConfirmCherrypickDialog>(
             element,
             '#confirmCherrypick'
@@ -1203,7 +1203,7 @@
           await element.updateComplete;
           const radioButtons = queryAll<HTMLInputElement>(
             confirmCherrypick,
-            "input[name='cherryPickOptions']"
+            "md-radio[name='cherryPickOptions']"
           );
           assert.equal(radioButtons.length, 2);
           radioButtons[1].click();
@@ -1670,20 +1670,20 @@
           let expectedMsg =
             'Revert submission 199 0' +
             '\n\n' +
-            'Reason for revert: <INSERT REASONING HERE>' +
+            'Reason for revert: <MUST SPECIFY REASON HERE>' +
             '\n\n' +
             'Reverted changes: /q/submissionid:199+0\n';
           assert.equal(confirmRevertDialog.message, expectedMsg);
           const radioInputs = queryAll<HTMLInputElement>(
             confirmRevertDialog,
-            'input[name="revertOptions"]'
+            'md-radio[name="revertOptions"]'
           );
           radioInputs[0].click();
           await element.updateComplete;
           expectedMsg =
             'Revert "random commit message"\n\nThis reverts ' +
             'commit 2000.\n\nReason' +
-            ' for revert: <INSERT REASONING HERE>\n';
+            ' for revert: <MUST SPECIFY REASON HERE>\n';
           assert.equal(confirmRevertDialog.message, expectedMsg);
         });
 
@@ -1728,18 +1728,18 @@
           await element.updateComplete;
           const radioInputs = queryAll<HTMLInputElement>(
             confirmRevertDialog,
-            'input[name="revertOptions"]'
+            'md-radio[name="revertOptions"]'
           );
           const revertSubmissionMsg =
             'Revert submission 199 0' +
             '\n\n' +
-            'Reason for revert: <INSERT REASONING HERE>' +
+            'Reason for revert: <MUST SPECIFY REASON HERE>' +
             '\n\n' +
             'Reverted changes: /q/submissionid:199+0\n';
           const singleChangeMsg =
             'Revert "random commit message"\n\nThis reverts ' +
             'commit 2000.\n\nReason' +
-            ' for revert: <INSERT REASONING HERE>\n';
+            ' for revert: <MUST SPECIFY REASON HERE>\n';
           assert.equal(confirmRevertDialog.message, revertSubmissionMsg);
           const newRevertMsg = revertSubmissionMsg + 'random';
           const newSingleChangeMsg = singleChangeMsg + 'random';
@@ -1833,7 +1833,7 @@
           const msg =
             'Revert "random commit message"\n\n' +
             'This reverts commit 2000.\n\nReason ' +
-            'for revert: <INSERT REASONING HERE>\n';
+            'for revert: <MUST SPECIFY REASON HERE>\n';
           assert.equal(confirmRevertDialog.message, msg);
           let editedMsg = msg + 'hello';
           confirmRevertDialog.message += 'hello';
@@ -1852,10 +1852,10 @@
           // Contains generic template reason so doesn't submit
           assert.isFalse(fireActionStub.called);
           confirmRevertDialog.message = confirmRevertDialog.message.replace(
-            '<INSERT REASONING HERE>',
+            '<MUST SPECIFY REASON HERE>',
             ''
           );
-          editedMsg = editedMsg.replace('<INSERT REASONING HERE>', '');
+          editedMsg = editedMsg.replace('<MUST SPECIFY REASON HERE>', '');
           confirmButton.click();
           await element.updateComplete;
           assert.equal(fireActionStub.getCall(0).args[0], '/revert');
@@ -1904,7 +1904,7 @@
         assert.isOk(
           query(
             queryAndAssert<GrDropdown>(element, '#moreActions'),
-            'span[data-id="private-change"]'
+            '[data-id="private-change"]'
           )
         );
         element.setActionOverflow(ActionType.CHANGE, 'private', false);
@@ -1913,7 +1913,7 @@
         assert.isNotOk(
           query(
             queryAndAssert<GrDropdown>(element, '#moreActions'),
-            'span[data-id="private-change"]'
+            '[data-id="private-change"]'
           )
         );
       });
@@ -1958,7 +1958,7 @@
         assert.isOk(
           query(
             queryAndAssert<GrDropdown>(element, '#moreActions'),
-            'span[data-id="private.delete-change"]'
+            '[data-id="private.delete-change"]'
           )
         );
         element.setActionOverflow(ActionType.CHANGE, 'private.delete', false);
@@ -1967,7 +1967,7 @@
         assert.isNotOk(
           query(
             queryAndAssert<GrDropdown>(element, '#moreActions'),
-            'span[data-id="private.delete-change"]'
+            '[data-id="private.delete-change"]'
           )
         );
       });
@@ -2639,7 +2639,7 @@
             confirmRevertDialog,
             'populate'
           );
-          element.showRevertDialog();
+          await element.showRevertDialog();
           await waitUntil(() => !!populateRevertDialogStub.called);
           assert.deepEqual(populateRevertDialogStub.lastCall.args[1], {
             validation_options: [{name: 'o1', description: 'option 1'}],
@@ -2723,6 +2723,11 @@
       });
 
       suite('failure modes', () => {
+        let clock: SinonFakeTimers;
+        setup(() => {
+          clock = sinon.useFakeTimers();
+        });
+
         test('non-latest', () => {
           stubRestApi('getChange').returns(
             Promise.resolve({
@@ -2764,8 +2769,14 @@
           const executeChangeActionStub = stubRestApi(
             'executeChangeAction'
           ).callsFake(
-            (_num, _method, _patchNum, _endpoint, _payload, onErr) => {
-              // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+            (
+              _num: any,
+              _method: any,
+              _patchNum: any,
+              _endpoint: any,
+              _payload: any,
+              onErr: any
+            ) => {
               onErr!();
               return Promise.resolve(new Response());
             }
@@ -2792,7 +2803,7 @@
         test('revert single change change not reachable', async () => {
           let getChangeCall = 0;
           let errorFired = false;
-          stubRestApi('getChange').callsFake((_, errFn) => {
+          stubRestApi('getChange').callsFake((_: any, errFn: any) => {
             ++getChangeCall;
             if (getChangeCall === 1) {
               return Promise.resolve({
@@ -2837,7 +2848,7 @@
             cleanup,
             {} as UIActionInfo
           );
-          await element.handleResponse(
+          const responsePromise = element.handleResponse(
             {
               __key: 'revert',
               __type: ActionType.CHANGE,
@@ -2846,6 +2857,15 @@
             response
           );
 
+          // Wait for all retry attempts
+          for (let i = 0; i < 5; i++) {
+            clock.tick(1000);
+            await clock.runAllAsync();
+            await element.updateComplete;
+          }
+
+          await responsePromise;
+
           assert.isTrue(errorFired);
           assert.isFalse(setUrlStub.called);
           assert.isFalse(setReviewOnRevertStub.called);
diff --git a/polygerrit-ui/app/elements/change/gr-change-autocomplete/gr-change-autocomplete.ts b/polygerrit-ui/app/elements/change/gr-change-autocomplete/gr-change-autocomplete.ts
new file mode 100644
index 0000000..b1e5e82
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-autocomplete/gr-change-autocomplete.ts
@@ -0,0 +1,145 @@
+/**
+ * @license
+ * Copyright 2024 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {css, html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
+import {getAppContext} from '../../../services/app-context';
+import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {NumericChangeId} from '../../../types/common';
+import {ValueChangedEvent} from '../../../types/events';
+import '../../shared/gr-autocomplete/gr-autocomplete';
+
+export interface ChangeSuggestion {
+  description: string;
+  changeNum: NumericChangeId;
+}
+
+/**
+ * An autocomplete component for selecting a Gerrit change.
+ * It fetches recent open changes on the host and provides them as suggestions
+ * based on the user's input.
+ */
+@customElement('gr-change-autocomplete')
+export class GrChangeAutocomplete extends LitElement {
+  @property({type: String})
+  text = '';
+
+  @property({type: Number})
+  excludeChangeNum?: NumericChangeId;
+
+  @property({type: String})
+  projectQuery = '';
+
+  @state()
+  private query: AutocompleteQuery = input => this.getChangeSuggestions(input);
+
+  @state()
+  private recentChanges: ChangeSuggestion[] = [];
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  constructor() {
+    super();
+  }
+
+  override connectedCallback() {
+    super.connectedCallback();
+  }
+
+  static override get styles() {
+    return css`
+      :host {
+        display: block;
+      }
+      gr-autocomplete {
+        width: 100%;
+      }
+    `;
+  }
+
+  override render() {
+    return html`
+      <gr-autocomplete
+        .query=${this.query}
+        .text=${this.text}
+        @text-changed=${(e: ValueChangedEvent) => {
+          this.text = e.detail.value;
+        }}
+        allow-non-suggested-values
+        placeholder="Change number or subject"
+      >
+      </gr-autocomplete>
+    `;
+  }
+
+  async fetchRecentChanges() {
+    try {
+      const res = await this.restApiService.getChanges(
+        /* changeNumber=*/ 450,
+        `is:open -age:90d ${this.projectQuery}`,
+        /* offset=*/ undefined,
+        /* options=*/ undefined,
+        throwingErrorCallback
+      );
+      if (!res) {
+        this.recentChanges = [];
+        return this.recentChanges;
+      }
+      const changes: ChangeSuggestion[] = [];
+      for (const change of res) {
+        changes.push({
+          description: `${change._number}: ${change.subject}`,
+          changeNum: change._number,
+        });
+      }
+      this.recentChanges = changes;
+    } catch (e) {
+      console.error('Failed to fetch recent changes:', e);
+      this.recentChanges = [];
+    }
+    return this.recentChanges;
+  }
+
+  private getRecentChanges() {
+    if (this.recentChanges.length > 0) {
+      return Promise.resolve(this.recentChanges);
+    }
+    return this.fetchRecentChanges();
+  }
+
+  private getChangeSuggestions(input: string) {
+    return this.getRecentChanges().then(changes =>
+      this.filterChanges(input, changes)
+    );
+  }
+
+  private filterChanges(
+    input: string,
+    changes: ChangeSuggestion[]
+  ): AutocompleteSuggestion[] {
+    return changes
+      .filter(
+        change =>
+          change.description.includes(input) &&
+          (this.excludeChangeNum === undefined ||
+            change.changeNum !== this.excludeChangeNum)
+      )
+      .map(
+        change =>
+          ({
+            name: change.description,
+            value: `${change.changeNum}`,
+          } as AutocompleteSuggestion)
+      );
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-change-autocomplete': GrChangeAutocomplete;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-change-autocomplete/gr-change-autocomplete_test.ts b/polygerrit-ui/app/elements/change/gr-change-autocomplete/gr-change-autocomplete_test.ts
new file mode 100644
index 0000000..87c4667
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-autocomplete/gr-change-autocomplete_test.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2024 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-change-autocomplete';
+import {GrChangeAutocomplete} from './gr-change-autocomplete';
+import {stubRestApi} from '../../../test/test-utils';
+import {NumericChangeId} from '../../../types/common';
+import {createChangeViewChange} from '../../../test/test-data-generators';
+import {assert, fixture, html} from '@open-wc/testing';
+
+suite('gr-change-autocomplete tests', () => {
+  let element: GrChangeAutocomplete;
+
+  setup(async () => {
+    stubRestApi('getChanges').returns(
+      Promise.resolve([
+        {
+          ...createChangeViewChange(),
+          _number: 123 as NumericChangeId,
+          subject: 'my first awesome change',
+        },
+        {
+          ...createChangeViewChange(),
+          _number: 124 as NumericChangeId,
+          subject: 'my second awesome change',
+        },
+        {
+          ...createChangeViewChange(),
+          _number: 245 as NumericChangeId,
+          subject: 'my third awesome change',
+        },
+      ])
+    );
+
+    element = await fixture<GrChangeAutocomplete>(
+      html`<gr-change-autocomplete></gr-change-autocomplete>`
+    );
+    await element.updateComplete;
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <gr-autocomplete
+          allow-non-suggested-values=""
+          placeholder="Change number or subject"
+        >
+        </gr-autocomplete>
+      `
+    );
+  });
+
+  test('suggestions are filtered by input', async () => {
+    const query = element['query'];
+    let suggestions = await query('123');
+    assert.equal(suggestions.length, 1);
+    assert.equal(suggestions[0].name, '123: my first awesome change');
+
+    suggestions = await query('12');
+    assert.equal(suggestions.length, 2);
+
+    suggestions = await query('awesome');
+    assert.equal(suggestions.length, 3);
+
+    suggestions = await query('third');
+    assert.equal(suggestions.length, 1);
+  });
+
+  test('suggestions do not include excluded change', async () => {
+    element.excludeChangeNum = 123 as NumericChangeId;
+    await element.updateComplete;
+
+    const query = element['query'];
+    let suggestions = await query('123');
+    assert.equal(suggestions.length, 0);
+
+    suggestions = await query('awesome');
+    assert.equal(suggestions.length, 2);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index d6c189e..aa0a8ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -44,9 +44,9 @@
   LabelNameToInfoMap,
   NumericChangeId,
   ParentCommitInfo,
-  RevisionPatchSetNum,
   RepoName,
   RevisionInfo,
+  RevisionPatchSetNum,
   ServerInfo,
   WebLinkInfo,
 } from '../../../types/common';
@@ -55,29 +55,27 @@
 import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip';
 import {getAppContext} from '../../../services/app-context';
 import {
-  Metadata,
-  isSectionSet,
   DisplayRules,
+  isSectionSet,
+  Metadata,
 } from '../../../utils/change-metadata-util';
 import {
-  fireAlert,
   fire,
-  fireReload,
+  fireAlert,
   fireError,
+  fireReload,
 } from '../../../utils/event-util';
 import {
   EditRevisionInfo,
   isDefined,
   ParsedChangeInfo,
 } from '../../../types/types';
-import {
-  AutocompleteQuery,
-  AutocompleteSuggestion,
-} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {getRevertCreatedChangeIds} from '../../../utils/message-util';
 import {Interaction} from '../../../constants/reporting';
 import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
-import {LitElement, css, html, nothing, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
@@ -166,9 +164,11 @@
 
   @state() showAllSections = false;
 
-  @state() queryTopic?: AutocompleteQuery;
+  @state() queryIdentity: AutocompleteQuery;
 
-  @state() queryHashtag?: AutocompleteQuery;
+  @state() queryTopic: AutocompleteQuery;
+
+  @state() queryHashtag: AutocompleteQuery;
 
   private restApiService = getAppContext().restApiService;
 
@@ -224,6 +224,7 @@
     );
     this.queryTopic = (input: string) => this.getTopicSuggestions(input);
     this.queryHashtag = (input: string) => this.getHashtagSuggestions(input);
+    this.queryIdentity = (input: string) => this.getIdentitySuggestions(input);
   }
 
   static override get styles() {
@@ -499,7 +500,7 @@
                 this.handleIdentityChanged(e, role)}
               showAsEditPencil
               autocomplete
-              .query=${(text: string) => this.getIdentitySuggestions(text)}
+              .query=${this.queryIdentity}
             ></gr-editable-label>
           `
         )}
@@ -660,7 +661,7 @@
         ${when(
           this.showTopicChip(),
           () => html` <gr-linked-chip
-            .text=${this.change?.topic}
+            .text=${this.change?.topic ?? ''}
             limit="40"
             href=${createSearchUrl({topic: this.change!.topic!})}
             ?removable=${!this.topicReadOnly}
@@ -672,7 +673,6 @@
           () =>
             html` <gr-editable-label
               class="topicEditableLabel"
-              labelText="Set topic"
               .confirmLabel=${'Set Topic'}
               .value=${this.change?.topic}
               maxLength="1024"
@@ -761,7 +761,6 @@
           !this.hashtagReadOnly,
           () => html`
             <gr-editable-label
-              labelText="Add a hashtag"
               .placeholder=${this.computeHashtagPlaceholder()}
               .readOnly=${this.hashtagReadOnly}
               @changed=${this.handleHashtagChanged}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index fe1548c..0da17d0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -9,37 +9,37 @@
 
 import {ChangeRole, GrChangeMetadata} from './gr-change-metadata';
 import {
-  createServerInfo,
-  createParsedChange,
+  createAccountDetailWithId,
   createAccountWithId,
+  createCommit,
   createCommitInfoWithRequiredCommit,
-  createWebLinkInfo,
+  createConfig,
   createGerritInfo,
   createGitPerson,
-  createCommit,
+  createParsedChange,
   createRevision,
-  createAccountDetailWithId,
-  createConfig,
+  createServerInfo,
+  createWebLinkInfo,
 } from '../../../test/test-data-generators';
 import {
   ChangeStatus,
-  SubmitType,
   GpgKeyInfoStatus,
   InheritedBooleanInfoConfiguredValue,
+  SubmitType,
 } from '../../../constants/constants';
 import {
-  EmailAddress,
   AccountId,
   CommitId,
-  ServerInfo,
-  RevisionInfo,
-  ParentCommitInfo,
-  TopicName,
-  RevisionPatchSetNum,
-  NumericChangeId,
-  LabelValueToDescriptionMap,
-  Hashtag,
   CommitInfo,
+  EmailAddress,
+  Hashtag,
+  LabelValueToDescriptionMap,
+  NumericChangeId,
+  ParentCommitInfo,
+  RevisionInfo,
+  RevisionPatchSetNum,
+  ServerInfo,
+  TopicName,
 } from '../../../types/common';
 import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label';
 import {PluginApi} from '../../../api/plugin';
@@ -53,7 +53,7 @@
 import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {nothing} from 'lit';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index fc902c3..62ac816 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -4,13 +4,17 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import './gr-checks-chip';
+import './gr-summary-chip';
 import '../gr-comments-summary/gr-comments-summary';
 import '../../shared/gr-icon/gr-icon';
 import '../../checks/gr-checks-action';
-import {LitElement, css, html, nothing} from 'lit';
-import {customElement, state} from 'lit/decorators.js';
+import '../gr-ai-prompt-dialog/gr-ai-prompt-dialog';
+import {css, html, LitElement, nothing} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {sharedStyles} from '../../../styles/shared-styles';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {getAppContext} from '../../../services/app-context';
 import {CheckRun, ErrorMessages} from '../../../models/checks/checks-model';
 import {Action, Category, CheckResult, RunStatus} from '../../../api/checks';
@@ -26,9 +30,9 @@
 } from '../../../models/checks/checks-util';
 import {getMentionedThreads, isUnresolved} from '../../../utils/comment-util';
 import {AccountInfo, CommentThread, DropdownLink} from '../../../types/common';
+import {FlowInfo, FlowStageState} from '../../../api/rest-api';
 import {Tab} from '../../../constants/constants';
 import {ChecksTabState} from '../../../types/events';
-import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {modifierPressed} from '../../../utils/dom-util';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {resolve} from '../../../models/dependency';
@@ -39,6 +43,10 @@
 import {when} from 'lit/directives/when.js';
 import {combineLatest} from 'rxjs';
 import {userModelToken} from '../../../models/user/user-model';
+import {assertIsDefined} from '../../../utils/common-util';
+import {GrAiPromptDialog} from '../gr-ai-prompt-dialog/gr-ai-prompt-dialog';
+import {flowsModelToken} from '../../../models/flows/flows-model';
+import {KnownExperimentId} from '../../../services/flags/flags';
 
 function handleSpaceOrEnter(e: KeyboardEvent, handler: () => void) {
   if (modifierPressed(e)) return;
@@ -94,6 +102,15 @@
   @state()
   draftCount = 0;
 
+  @state()
+  flows: FlowInfo[] = [];
+
+  @query('#aiPromptModal')
+  aiPromptModal?: HTMLDialogElement;
+
+  @query('#aiPromptDialog')
+  aiPromptDialog?: GrAiPromptDialog;
+
   private readonly showAllChips = new Map<RunStatus | Category, boolean>();
 
   private readonly getCommentsModel = resolve(this, commentsModelToken);
@@ -104,6 +121,10 @@
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
+  private readonly getFlowsModel = resolve(this, flowsModelToken);
+
+  private readonly flagsService = getAppContext().flagsService;
+
   private readonly reporting = getAppContext().reportingService;
 
   constructor() {
@@ -179,11 +200,17 @@
         this.mentionCount = unresolvedThreadsMentioningSelf.length;
       }
     );
+    subscribe(
+      this,
+      () => this.getFlowsModel().flows$,
+      x => (this.flows = x)
+    );
   }
 
   static override get styles() {
     return [
       sharedStyles,
+      modalStyles,
       spinnerStyles,
       css`
         :host {
@@ -246,16 +273,25 @@
         .login gr-button {
           margin: -4px var(--spacing-s);
         }
+        table.info {
+          width: 100%;
+        }
         td.key {
           padding-right: var(--spacing-l);
           padding-bottom: var(--spacing-s);
           line-height: calc(var(--line-height-normal) + var(--spacing-s));
+          width: 70px;
         }
         td.value {
           padding-right: var(--spacing-l);
           padding-bottom: var(--spacing-s);
           line-height: calc(var(--line-height-normal) + var(--spacing-s));
         }
+        .value-content {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+        }
         /* The basics of .loadingSpin are defined in shared styles. */
         .loadingSpin {
           width: calc(var(--line-height-normal) - 2px);
@@ -531,26 +567,130 @@
     });
   }
 
+  private handleOpenAiPromptDialog() {
+    assertIsDefined(this.aiPromptModal, 'aiPromptModal');
+    this.aiPromptModal.showModal();
+    this.aiPromptDialog?.open();
+  }
+
+  private handleAiPromptDialogClose() {
+    assertIsDefined(this.aiPromptModal, 'aiPromptModal');
+    this.aiPromptModal.close();
+  }
+
   override render() {
     return html`
       <div>
-        <table>
+        <table class="info">
           <tr>
             <td class="key">Comments</td>
             <td class="value">
-              <gr-comments-summary
-                .commentsLoading=${this.commentsLoading}
-                .commentThreads=${this.commentThreads}
-                .draftCount=${this.draftCount}
-                .mentionCount=${this.mentionCount}
-                showCommentCategoryName
-                clickableChips
-              ></gr-comments-summary>
+              <div class="value-content">
+                <gr-comments-summary
+                  .commentsLoading=${this.commentsLoading}
+                  .commentThreads=${this.commentThreads}
+                  .draftCount=${this.draftCount}
+                  .mentionCount=${this.mentionCount}
+                  showCommentCategoryName
+                  clickableChips
+                ></gr-comments-summary>
+                ${when(
+                  this.flagsService.isEnabled(KnownExperimentId.GET_AI_PROMPT),
+                  () =>
+                    html`<gr-button link @click=${this.handleOpenAiPromptDialog}
+                      >Create AI Review Prompt</gr-button
+                    >`
+                )}
+              </div>
             </td>
           </tr>
-          ${this.renderChecksSummary()}
+          ${this.renderChecksSummary()} ${this.renderFlowsSummary()}
         </table>
       </div>
+      ${when(
+        this.flagsService.isEnabled(KnownExperimentId.GET_AI_PROMPT),
+        () => html` <dialog id="aiPromptModal" tabindex="-1">
+          <gr-ai-prompt-dialog
+            id="aiPromptDialog"
+            @close=${this.handleAiPromptDialogClose}
+          ></gr-ai-prompt-dialog>
+        </dialog>`
+      )}
+    `;
+  }
+
+  private getFlowOverallStatus(flow: FlowInfo): FlowStageState {
+    if (
+      flow.stages.some(
+        stage =>
+          stage.state === FlowStageState.FAILED ||
+          stage.state === FlowStageState.TERMINATED
+      )
+    ) {
+      return FlowStageState.FAILED;
+    }
+    if (
+      flow.stages.some(
+        stage =>
+          stage.state === FlowStageState.PENDING ||
+          stage.state === FlowStageState.TERMINATED
+      )
+    ) {
+      return FlowStageState.PENDING; // Using PENDING to represent running/in-progress
+    }
+    if (flow.stages.every(stage => stage.state === FlowStageState.DONE)) {
+      return FlowStageState.DONE;
+    }
+    return FlowStageState.PENDING; // Default or unknown state
+  }
+
+  private renderFlowsSummary() {
+    if (this.flows.length === 0) return nothing;
+    const handler = () => fireShowTab(this, Tab.FLOWS, true);
+    const failed = this.flows.filter(
+      f => this.getFlowOverallStatus(f) === FlowStageState.FAILED
+    ).length;
+    const running = this.flows.filter(
+      f => this.getFlowOverallStatus(f) === FlowStageState.PENDING
+    ).length;
+    const done = this.flows.filter(
+      f => this.getFlowOverallStatus(f) === FlowStageState.DONE
+    ).length;
+    return html`
+      <tr>
+        <td class="key">Flows</td>
+        <td class="value">
+          <div class="flowsSummary">
+            ${failed > 0
+              ? html`<gr-checks-chip
+                  .statusOrCategory=${Category.ERROR}
+                  .text=${`${failed}`}
+                  @click=${handler}
+                  @keydown=${(e: KeyboardEvent) =>
+                    handleSpaceOrEnter(e, handler)}
+                ></gr-checks-chip>`
+              : ''}
+            ${running > 0
+              ? html`<gr-checks-chip
+                  .statusOrCategory=${RunStatus.RUNNING}
+                  .text=${`${running}`}
+                  @click=${handler}
+                  @keydown=${(e: KeyboardEvent) =>
+                    handleSpaceOrEnter(e, handler)}
+                ></gr-checks-chip>`
+              : ''}
+            ${done > 0
+              ? html`<gr-checks-chip
+                  .statusOrCategory=${Category.SUCCESS}
+                  .text=${`${done}`}
+                  @click=${handler}
+                  @keydown=${(e: KeyboardEvent) =>
+                    handleSpaceOrEnter(e, handler)}
+                ></gr-checks-chip>`
+              : ''}
+          </div>
+        </td>
+      </tr>
     `;
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts
new file mode 100644
index 0000000..318187d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-change-summary';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrChangeSummary} from './gr-change-summary';
+import {
+  createCheckResult,
+  createComment,
+  createDraft,
+  createRun,
+} from '../../../test/test-data-generators';
+import {testResolver} from '../../../test/common-test-setup';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {checksModelToken} from '../../../models/checks/checks-model';
+import {Category, RunStatus} from '../../../api/checks';
+import {CheckRun} from '../../../models/checks/checks-model';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+
+suite('gr-change-summary screenshot tests', () => {
+  let element: GrChangeSummary;
+
+  setup(async () => {
+    const commentsModel = testResolver(commentsModelToken);
+    testResolver(checksModelToken);
+
+    commentsModel.setState({
+      comments: {
+        'a.txt': [createComment(), {...createComment(), unresolved: true}],
+      },
+      drafts: {
+        'a.txt': [createDraft(), createDraft(), createDraft()],
+      },
+      discardedDrafts: [],
+      portedComments: {},
+      portedDrafts: {},
+    });
+
+    const runs: CheckRun[] = [
+      createRun({status: RunStatus.RUNNING, checkName: 'runner-1'}),
+      createRun({
+        status: RunStatus.COMPLETED,
+        checkName: 'success-check',
+        results: [createCheckResult({category: Category.SUCCESS})],
+      }),
+      createRun({
+        status: RunStatus.COMPLETED,
+        checkName: 'info-check',
+        results: [createCheckResult({category: Category.INFO})],
+      }),
+      createRun({
+        status: RunStatus.COMPLETED,
+        checkName: 'warning-check',
+        results: [createCheckResult({category: Category.WARNING})],
+      }),
+      createRun({
+        status: RunStatus.COMPLETED,
+        checkName: 'error-check',
+        results: [createCheckResult({category: Category.ERROR})],
+      }),
+    ];
+
+    element = await fixture<GrChangeSummary>(
+      html`<gr-change-summary></gr-change-summary>`
+    );
+    element.runs = runs;
+    element.showChecksSummary = true;
+    await element.updateComplete;
+  });
+
+  test('screenshot with chips', async () => {
+    await visualDiff(element, 'gr-change-summary-with-chips');
+    await visualDiffDarkTheme(element, 'gr-change-summary-with-chips');
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index c2ea4e5..7538656 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrChangeSummary} from './gr-change-summary';
 import {queryAll, queryAndAssert} from '../../../utils/common-util';
 import {fakeRun0} from '../../../models/checks/checks-fakes';
@@ -16,7 +16,7 @@
   createDraft,
   createRun,
 } from '../../../test/test-data-generators';
-import {Timestamp} from '../../../api/rest-api';
+import {FlowInfo, FlowStageState, Timestamp} from '../../../api/rest-api';
 import {testResolver} from '../../../test/common-test-setup';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {
@@ -26,16 +26,31 @@
 import {GrChecksChip} from './gr-checks-chip';
 import {CheckRun} from '../../../models/checks/checks-model';
 import {Category, RunStatus} from '../../../api/checks';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+import {getAppContext} from '../../../services/app-context';
+import {KnownExperimentId} from '../../../services/flags/flags';
+
+function createFlow(partial: Partial<FlowInfo> = {}): FlowInfo {
+  return {
+    uuid: 'test-uuid',
+    owner: createAccountWithEmail(),
+    created: '2020-01-01 00:00:00.000000000' as Timestamp,
+    stages: [],
+    ...partial,
+  };
+}
 
 suite('gr-change-summary test', () => {
   let element: GrChangeSummary;
   let commentsModel: CommentsModel;
   let userModel: UserModel;
+  let flowsModel: FlowsModel;
 
   setup(async () => {
     element = await fixture(html`<gr-change-summary></gr-change-summary>`);
     commentsModel = testResolver(commentsModelToken);
     userModel = testResolver(userModelToken);
+    flowsModel = testResolver(flowsModelToken);
   });
 
   test('is defined', () => {
@@ -44,36 +59,112 @@
   });
 
   test('renders', async () => {
-    commentsModel.setState({
-      drafts: {
-        a: [createDraft(), createDraft(), createDraft()],
-      },
-      discardedDrafts: [],
-    });
-    element.commentsLoading = false;
-    element.commentThreads = [
-      createCommentThread([createComment()]),
-      createCommentThread([{...createComment(), unresolved: true}]),
-    ];
-    await element.updateComplete;
-    assert.shadowDom.equal(
-      element,
-      /* HTML */ `<div>
-        <table>
-          <tbody>
-            <tr>
-              <td class="key">Comments</td>
-              <td class="value">
-                <gr-comments-summary
-                  clickablechips=""
-                  showcommentcategoryname=""
-                ></gr-comments-summary>
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div> `
-    );
+    const flagsService = getAppContext().flagsService;
+    const isEnabledStub = sinon.stub(flagsService, 'isEnabled');
+    isEnabledStub.returns(false);
+    isEnabledStub.withArgs(KnownExperimentId.GET_AI_PROMPT).returns(true);
+
+    try {
+      commentsModel.setState({
+        drafts: {
+          a: [createDraft(), createDraft(), createDraft()],
+        },
+        discardedDrafts: [],
+      });
+      element.commentsLoading = false;
+      element.commentThreads = [
+        createCommentThread([createComment()]),
+        createCommentThread([{...createComment(), unresolved: true}]),
+      ];
+      await element.updateComplete;
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <div>
+            <table class="info">
+              <tbody>
+                <tr>
+                  <td class="key">Comments</td>
+                  <td class="value">
+                    <div class="value-content">
+                      <gr-comments-summary
+                        clickablechips=""
+                        showcommentcategoryname=""
+                      ></gr-comments-summary>
+                      <gr-button
+                        aria-disabled="false"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        Create AI Review Prompt
+                      </gr-button>
+                    </div>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+          <dialog id="aiPromptModal" tabindex="-1">
+            <gr-ai-prompt-dialog id="aiPromptDialog" role="dialog">
+            </gr-ai-prompt-dialog>
+          </dialog>
+        `
+      );
+    } finally {
+      isEnabledStub.restore();
+    }
+  });
+
+  test('does not render AI review prompt when experiment is disabled', async () => {
+    const flagsService = getAppContext().flagsService;
+    const isEnabledStub = sinon.stub(flagsService, 'isEnabled');
+    isEnabledStub.returns(true);
+    isEnabledStub.withArgs(KnownExperimentId.GET_AI_PROMPT).returns(false);
+
+    try {
+      const disabledElement = await fixture<GrChangeSummary>(
+        html`<gr-change-summary></gr-change-summary>`
+      );
+
+      commentsModel.setState({
+        drafts: {
+          a: [createDraft(), createDraft(), createDraft()],
+        },
+        discardedDrafts: [],
+      });
+      disabledElement.commentsLoading = false;
+      disabledElement.commentThreads = [
+        createCommentThread([createComment()]),
+        createCommentThread([{...createComment(), unresolved: true}]),
+      ];
+
+      await disabledElement.updateComplete;
+      assert.shadowDom.equal(
+        disabledElement,
+        /* HTML */ `
+          <div>
+            <table class="info">
+              <tbody>
+                <tr>
+                  <td class="key">Comments</td>
+                  <td class="value">
+                    <div class="value-content">
+                      <gr-comments-summary
+                        clickablechips=""
+                        showcommentcategoryname=""
+                      ></gr-comments-summary>
+                    </div>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        `
+      );
+    } finally {
+      isEnabledStub.restore();
+    }
   });
 
   test('renders checks summary message', async () => {
@@ -167,6 +258,62 @@
     });
   });
 
+  suite('flows summary', () => {
+    test('renders', async () => {
+      flowsModel.setState({
+        flows: [
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.PENDING},
+            ],
+          }),
+          createFlow({
+            stages: [{expression: {condition: ''}, state: FlowStageState.DONE}],
+          }),
+          createFlow({
+            stages: [{expression: {condition: ''}, state: FlowStageState.DONE}],
+          }),
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.FAILED},
+            ],
+          }),
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.FAILED},
+            ],
+          }),
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.FAILED},
+            ],
+          }),
+        ],
+        loading: false,
+      });
+      await element.updateComplete;
+      const flowsSummary = queryAndAssert(element, '.flowsSummary');
+      assert.dom.equal(
+        flowsSummary,
+        /* HTML */ `
+          <div class="flowsSummary">
+            <gr-checks-chip> </gr-checks-chip>
+            <gr-checks-chip> </gr-checks-chip>
+            <gr-checks-chip> </gr-checks-chip>
+          </div>
+        `
+      );
+      const chips = queryAll<GrChecksChip>(element, 'gr-checks-chip');
+      assert.equal(chips.length, 3);
+      assert.equal(chips[0].statusOrCategory, Category.ERROR);
+      assert.equal(chips[0].text, '3');
+      assert.equal(chips[1].statusOrCategory, RunStatus.RUNNING);
+      assert.equal(chips[1].text, '1');
+      assert.equal(chips[2].statusOrCategory, Category.SUCCESS);
+      assert.equal(chips[2].text, '2');
+    });
+  });
+
   test('renders mentions summary', async () => {
     commentsModel.setState({
       drafts: {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts
index 65a19fe..1315385 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts
@@ -3,7 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {getAppContext} from '../../../services/app-context';
@@ -190,7 +190,7 @@
   private renderChip(clazz: string, ariaLabel: string, icon: ChecksIcon) {
     return html`
       <div class=${clazz} role="link" tabindex="0" aria-label=${ariaLabel}>
-        <gr-icon icon=${icon.name} ?filled=${icon.filled}></gr-icon>
+        <gr-icon icon=${icon.name} ?filled=${!!icon.filled}></gr-icon>
         ${this.renderLinks()}
         <div class="text">${this.text}</div>
       </div>
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts
index 412f042..6816609 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrChecksChip} from './gr-checks-chip';
 import {Category} from '../../../api/checks';
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts
index 5588f40..e2b846c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../shared/gr-icon/gr-icon';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {getAppContext} from '../../../services/app-context';
@@ -35,7 +35,7 @@
   category?: CommentTabState;
 
   @property({type: Boolean})
-  clickable?: Boolean;
+  clickable?: boolean;
 
   private readonly reporting = getAppContext().reportingService;
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip_test.ts
index 31f64c9..2968071 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-summary-chip_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrSummaryChip, SummaryChipStyles} from './gr-summary-chip';
 import {CommentTabState} from '../../../types/events';
 
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 79d19bb..6b8e026 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
@@ -3,11 +3,10 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {BehaviorSubject, combineLatest} from 'rxjs';
+import {BehaviorSubject} from 'rxjs';
 import '../gr-copy-links/gr-copy-links';
-import '@polymer/paper-tabs/paper-tabs';
 import '../../../styles/gr-a11y-styles';
-import '../../../styles/gr-paper-styles';
+import '../../../styles/gr-material-styles';
 import '../../../styles/shared-styles';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
@@ -32,12 +31,13 @@
 import '../gr-reply-dialog/gr-reply-dialog';
 import '../gr-thread-list/gr-thread-list';
 import '../../checks/gr-checks-tab';
+import '../gr-flows/gr-flows';
 import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {pluralize, trimWithEllipsis} from '../../../utils/string-util';
 import {untilRendered, whenVisible} from '../../../utils/dom-util';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {ChangeStatus, Tab, DiffViewMode} from '../../../constants/constants';
+import {ChangeStatus, DiffViewMode, Tab} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
 import {
   computeAllPatchSets,
@@ -66,27 +66,19 @@
   LabelNameToInfoMap,
   NumericChangeId,
   PARENT,
-  PatchSetNum,
   QuickLabelInfo,
   RevisionInfo,
   RevisionPatchSetNum,
   ServerInfo,
   UrlEncodedCommentId,
-  isRobot,
 } from '../../../types/common';
 import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
 import {GrIncludedInDialog} from '../gr-included-in-dialog/gr-included-in-dialog';
 import {GrDownloadDialog} from '../gr-download-dialog/gr-download-dialog';
 import {GrChangeMetadata} from '../gr-change-metadata/gr-change-metadata';
-import {
-  assertIsDefined,
-  assert,
-  queryAll,
-  queryAndAssert,
-} from '../../../utils/common-util';
+import {assert, assertIsDefined, queryAll} from '../../../utils/common-util';
 import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
 import {isUnresolved} from '../../../utils/comment-util';
-import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
 import {GrFileList} from '../gr-file-list/gr-file-list';
 import {
   EditRevisionInfo,
@@ -105,7 +97,7 @@
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrMessagesList} from '../gr-messages-list/gr-messages-list';
 import {GrThreadList} from '../gr-thread-list/gr-thread-list';
-import {fireAlert, fire, fireReload} from '../../../utils/event-util';
+import {fire, fireAlert, fireReload} from '../../../utils/event-util';
 import {
   debounce,
   DelayedTask,
@@ -130,7 +122,7 @@
 import {changeModelToken} from '../../../models/change/change-model';
 import {css, html, LitElement, nothing} from 'lit';
 import {a11yStyles} from '../../../styles/gr-a11y-styles';
-import {paperStyles} from '../../../styles/gr-paper-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {when} from 'lit/directives/when.js';
@@ -153,6 +145,9 @@
 import {relatedChangesModelToken} from '../../../models/change/related-changes-model';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {assign} from '../../../utils/location-util';
+import '@material/web/tabs/secondary-tab';
+import '@material/web/tabs/tabs';
+import {MdTabs} from '@material/web/tabs/tabs';
 
 const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
 
@@ -165,17 +160,6 @@
 const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
 
 const PREFIX = '#message-';
-
-const ROBOT_COMMENTS_LIMIT = 10;
-
-const ReloadToastMessage = {
-  NEWER_REVISION: 'A newer patch set has been uploaded',
-  RESTORED: 'This change has been restored',
-  ABANDONED: 'This change has been abandoned',
-  MERGED: 'This change has been merged',
-  NEW_MESSAGE: 'There are new messages on this change',
-};
-
 @customElement('gr-change-view')
 export class GrChangeView extends LitElement {
   /**
@@ -226,7 +210,7 @@
 
   @query('#replyBtn') replyBtn?: GrButton;
 
-  @query('#tabs') tabs?: PaperTabsElement;
+  @query('#tabs') tabs?: MdTabs;
 
   @query('gr-messages-list') messagesList?: GrMessagesList;
 
@@ -359,9 +343,6 @@
   @state()
   private pluginTabsContentEndpoints: string[] = [];
 
-  @state()
-  private currentRobotCommentsPatchSet?: PatchSetNum;
-
   /**
    * This can be a string only for plugin provided tabs.
    */
@@ -373,12 +354,6 @@
   unresolvedOnly = true;
 
   @state()
-  private showAllRobotComments = false;
-
-  @state()
-  private showRobotCommentsButton = false;
-
-  @state()
   draftCount = 0;
 
   private throttledToggleChangeStar?: (e: KeyboardEvent) => void;
@@ -386,10 +361,6 @@
   @state()
   private showChecksTab = false;
 
-  // visible for testing
-  @state()
-  showFindingsTab = false;
-
   @state()
   private isViewCurrent = false;
 
@@ -399,6 +370,9 @@
   @state()
   private revertingChange?: ChangeInfo;
 
+  @state()
+  isFlowsEnabled = false;
+
   // Private but used in tests.
   @state()
   scrollCommentId?: UrlEncodedCommentId;
@@ -550,7 +524,13 @@
     );
     this.shortcutsController.addAbstract(
       Shortcut.OPEN_COPY_LINKS_DROPDOWN,
-      () => this.copyLinksDropdown?.openDropdown()
+      () => {
+        const button = this.shadowRoot?.querySelector<HTMLElement>(
+          '#copyLinkDialogButton'
+        );
+        if (!button) return;
+        this.copyLinksDropdown?.openDropdown(button);
+      }
     );
   }
 
@@ -563,7 +543,15 @@
     subscribe(
       this,
       () => this.getViewModel().tab$,
-      t => (this.activeTab = t)
+      t => {
+        // We don't want to jump during the initial page load. This scrolling
+        // works only when change is already loaded and user clicks on link
+        // to same page when tabs are not in view, but tabs needs to be rendered.
+        if (this.tabs) {
+          this.tabs.scrollIntoView({block: 'nearest'});
+        }
+        this.activeTab = t;
+      }
     );
     subscribe(
       this,
@@ -588,17 +576,6 @@
     );
     subscribe(
       this,
-      () =>
-        combineLatest([
-          this.getConfigModel().enableRobotComments$,
-          this.getCommentsModel().robotCommentCount$,
-        ]),
-      ([enableRobotComments, count]) => {
-        this.showFindingsTab = enableRobotComments && count > 0;
-      }
-    );
-    subscribe(
-      this,
       () => this.getViewModel().childView$,
       childView => {
         this.isViewCurrent = childView === ChangeChildView.OVERVIEW;
@@ -652,7 +629,12 @@
       changeNum => {
         // The change view is tied to a specific change number, so don't update
         // changeNum to undefined and only set it once.
-        if (changeNum && !this.changeNum) this.changeNum = changeNum;
+        if (changeNum && !this.changeNum) {
+          this.changeNum = changeNum;
+          this.restApiService.getIfFlowsIsEnabled(this.changeNum).then(res => {
+            this.isFlowsEnabled = res?.enabled ?? false;
+          });
+        }
       }
     );
     subscribe(
@@ -749,14 +731,6 @@
   override firstUpdated() {
     this.maybeScrollToMessage(window.location.hash);
     this.maybeShowRevertDialog();
-    // _onTabSizingChanged is called when iron-items-changed event is fired
-    // from iron-selectable but that is called before the element is present
-    // in view which whereas the method requires paper tabs already be visible
-    // as it relies on dom rect calculation.
-    // _onTabSizingChanged ensures the primary tab(Files/Comments/Checks) is
-    // underlined.
-    assertIsDefined(this.tabs, 'tabs');
-    whenVisible(this.tabs, () => this.tabs!._onTabSizingChanged());
   }
 
   /**
@@ -812,10 +786,13 @@
   static override get styles() {
     return [
       a11yStyles,
-      paperStyles,
+      materialStyles,
       sharedStyles,
       modalStyles,
       css`
+        .tabs {
+          display: flex;
+        }
         .container:not(.loading) {
           background-color: var(--background-color-tertiary);
         }
@@ -875,12 +852,15 @@
           font-weight: var(--font-weight-h3);
           line-height: var(--line-height-h3);
           margin-left: var(--spacing-l);
+          line-break: anywhere;
+          whitespace: no-wrap;
+          overflow: auto;
         }
         .changeNumberColon {
           color: transparent;
         }
         .changeCopyClipboard {
-          margin-left: var(--spacing-s);
+          margin-left: var(--spacing-m);
         }
         .showCopyLinkDialogButton {
           --gr-button-padding: 0 0 0 var(--spacing-s);
@@ -899,6 +879,7 @@
         }
         a.changeNumber {
           margin-left: var(--spacing-xs);
+          position: relative;
         }
         gr-reply-dialog {
           width: calc(min(60em, 90vw));
@@ -931,7 +912,7 @@
           max-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
         }
         .commitMessage gr-formatted-text {
-          word-break: break-word;
+          overflow-wrap: anywhere;
         }
         #commitMessageEditor {
           /* Account for border and padding and rounding errors. */
@@ -994,27 +975,17 @@
         gr-commit-info {
           display: inline-block;
         }
-        paper-tabs {
+        md-tabs {
           background-color: var(--background-color-tertiary);
           margin-top: var(--spacing-m);
-          height: calc(var(--line-height-h3) + var(--spacing-m));
-          --paper-tabs-selection-bar-color: var(--link-color);
         }
-        paper-tab {
+        md-secondary-tab {
+          display: inline-flex;
           box-sizing: border-box;
           max-width: 12em;
-          --paper-tab-ink: var(--link-color);
-          --paper-font-common-base_-_font-family: var(--header-font-family);
-          --paper-font-common-base_-_-webkit-font-smoothing: initial;
-          --paper-tab-content_-_margin-bottom: var(--spacing-s);
-          /* paper-tabs uses 700 here, which can look awkward */
-          --paper-tab-content-focused_-_font-weight: var(--font-weight-h3);
-          --paper-tab-content-focused_-_background: var(
-            --gray-background-focus
-          );
-          --paper-tab-content-unselected_-_opacity: 1;
-          --paper-tab-content-unselected_-_color: var(
-            --deemphasized-text-color
+          min-width: 12em;
+          --md-secondary-tab-container-height: calc(
+            var(--line-height-h3) + var(--spacing-m)
           );
         }
         gr-thread-list,
@@ -1038,6 +1009,9 @@
         gr-change-summary {
           margin-left: var(--spacing-m);
         }
+        .changeStarContainer {
+          position: relative;
+        }
         @media screen and (max-width: 75em) {
           .relatedChanges {
             padding: 0;
@@ -1123,9 +1097,6 @@
         .patch-set-dropdown {
           margin: var(--spacing-m) 0 0 var(--spacing-m);
         }
-        .show-robot-comments {
-          margin: var(--spacing-m);
-        }
         .tabContent gr-thread-list::part(threads) {
           padding: var(--spacing-l);
         }
@@ -1146,7 +1117,7 @@
 
   private renderMainContent() {
     return html`
-      <div id="mainContent" class="container" ?hidden=${this.loading}>
+      <div id="mainContent" class="container" ?hidden=${!!this.loading}>
         ${this.renderChangeInfoSection()}
         <h2 class="assistive-tech-only">Files and Comments tabs</h2>
         ${this.renderTabHeaders()} ${this.renderTabContent()}
@@ -1210,33 +1181,36 @@
           ></gr-change-status>`
         )}
       </div>
-      ${this.renderCopyLinksDropdown()}
-      <gr-button
-        flatten
-        down-arrow
-        class="showCopyLinkDialogButton"
-        @click=${(e: MouseEvent) => {
-          // We don't want to handle clicks on the star or the <a> link.
-          // Calling `stopPropagation()` from the click handler of <a> is not an
-          // option, because then the click does not reach the top-level gr-page
-          // click handler and would result is a full page reload.
-          if ((e.target as HTMLElement)?.nodeName !== 'GR-BUTTON') return;
-          this.copyLinksDropdown?.toggleDropdown();
-        }}
-        ><gr-change-star
-          id="changeStar"
-          .change=${this.change}
-          @toggle-star=${(e: CustomEvent<ChangeStarToggleStarDetail>) =>
-            this.handleToggleStar(e)}
-          ?hidden=${!this.loggedIn}
-        ></gr-change-star>
-        <a
-          class="changeNumber"
-          aria-label=${`Change ${this.change?._number}`}
-          href=${ifDefined(this.computeChangeUrl(true))}
-          >${this.change?._number}</a
-        >
-      </gr-button>
+      <div class="changeStarContainer">
+        ${this.renderCopyLinksDropdown()}
+        <gr-button
+          id="copyLinkDialogButton"
+          flatten
+          down-arrow
+          class="showCopyLinkDialogButton"
+          @click=${(e: MouseEvent) => {
+            // We don't want to handle clicks on the star or the <a> link.
+            // Calling `stopPropagation()` from the click handler of <a> is not an
+            // option, because then the click does not reach the top-level gr-page
+            // click handler and would result is a full page reload.
+            if ((e.target as HTMLElement)?.nodeName !== 'GR-BUTTON') return;
+            this.copyLinksDropdown?.toggleDropdown(e.target as HTMLElement);
+          }}
+          ><gr-change-star
+            id="changeStar"
+            .change=${this.change}
+            @toggle-star=${(e: CustomEvent<ChangeStarToggleStarDetail>) =>
+              this.handleToggleStar(e)}
+            ?hidden=${!this.loggedIn}
+          ></gr-change-star>
+          <a
+            class="changeNumber"
+            aria-label=${`Change ${this.change?._number}`}
+            href=${ifDefined(this.computeChangeUrl(true))}
+            >${this.change?._number}</a
+          >
+        </gr-button>
+      </div>
       <div class="headerSubject">
         ${trimWithEllipsis(this.change?.subject, 80)}
       </div>
@@ -1295,7 +1269,8 @@
         value: this.change.current_revision,
       });
     }
-    return html`<gr-copy-links .copyLinks=${links}> </gr-copy-links>`;
+    return html`<gr-copy-links .copyLinks=${links} .shortcutPrefix=${'l -'}>
+    </gr-copy-links>`;
   }
 
   private renderCommitActions() {
@@ -1384,54 +1359,68 @@
 
   private renderTabHeaders() {
     return html`
-      <paper-tabs
-        id="tabs"
-        @selected-changed=${this.onPaperTabSelectionChanged}
-      >
-        <paper-tab @click=${this.onPaperTabClick} data-name=${Tab.FILES}
-          ><span>Files</span></paper-tab
-        >
-        <paper-tab
-          @click=${this.onPaperTabClick}
-          data-name=${Tab.COMMENT_THREADS}
-          class="commentThreads"
-        >
-          <gr-tooltip-content
-            has-tooltip
-            title=${ifDefined(this.computeTotalCommentCounts())}
+      <div class="tabs">
+        <md-tabs id="tabs" @change=${this.onMdTabsChange}>
+          <md-secondary-tab
+            @click=${this.onMdSecondaryTabClick}
+            @keydown=${this.onMdSecondaryTabKeydown}
+            data-name=${Tab.FILES}
+            ><span>Files</span></md-secondary-tab
           >
-            <span>Comments</span></gr-tooltip-content
+          <md-secondary-tab
+            @click=${this.onMdSecondaryTabClick}
+            @keydown=${this.onMdSecondaryTabKeydown}
+            data-name=${Tab.COMMENT_THREADS}
+            class="commentThreads"
           >
-        </paper-tab>
-        ${when(
-          this.showChecksTab,
-          () => html`
-            <paper-tab data-name=${Tab.CHECKS} @click=${this.onPaperTabClick}
-              ><span>Checks</span></paper-tab
+            <gr-tooltip-content
+              has-tooltip
+              title=${ifDefined(this.computeTotalCommentCounts())}
             >
-          `
-        )}
-        ${this.pluginTabsHeaderEndpoints.map(
-          tabHeader => html`
-            <paper-tab data-name=${tabHeader} @click=${this.onPaperTabClick}>
-              <gr-endpoint-decorator name=${tabHeader}>
-                <gr-endpoint-param name="change" .value=${this.change}>
-                </gr-endpoint-param>
-                <gr-endpoint-param name="revision" .value=${this.revision}>
-                </gr-endpoint-param>
-              </gr-endpoint-decorator>
-            </paper-tab>
-          `
-        )}
-        ${when(
-          this.showFindingsTab,
-          () => html`
-            <paper-tab data-name=${Tab.FINDINGS} @click=${this.onPaperTabClick}>
-              <span>Findings</span>
-            </paper-tab>
-          `
-        )}
-      </paper-tabs>
+              <span>Comments</span></gr-tooltip-content
+            >
+          </md-secondary-tab>
+          ${when(
+            this.showChecksTab,
+            () => html`
+              <md-secondary-tab
+                data-name=${Tab.CHECKS}
+                @click=${this.onMdSecondaryTabClick}
+                @keydown=${this.onMdSecondaryTabKeydown}
+                ><span>Checks</span></md-secondary-tab
+              >
+            `
+          )}
+          ${when(
+            this.flagService.isEnabled(KnownExperimentId.SHOW_FLOWS_TAB) &&
+              this.isFlowsEnabled,
+            () => html`
+              <md-secondary-tab
+                data-name=${Tab.FLOWS}
+                @click=${this.onMdSecondaryTabClick}
+                @keydown=${this.onMdSecondaryTabKeydown}
+                ><span>Flows</span></md-secondary-tab
+              >
+            `
+          )}
+          ${this.pluginTabsHeaderEndpoints.map(
+            tabHeader => html`
+              <md-secondary-tab
+                data-name=${tabHeader}
+                @click=${this.onMdSecondaryTabClick}
+                @keydown=${this.onMdSecondaryTabKeydown}
+              >
+                <gr-endpoint-decorator name=${tabHeader}>
+                  <gr-endpoint-param name="change" .value=${this.change}>
+                  </gr-endpoint-param>
+                  <gr-endpoint-param name="revision" .value=${this.revision}>
+                  </gr-endpoint-param>
+                </gr-endpoint-decorator>
+              </md-secondary-tab>
+            `
+          )}
+        </md-tabs>
+      </div>
     `;
   }
 
@@ -1439,7 +1428,7 @@
     return html`
       <section class="tabContent">
         ${this.renderFilesTab()} ${this.renderCommentsTab()}
-        ${this.renderChecksTab()} ${this.renderFindingsTab()}
+        ${this.renderChecksTab()} ${this.renderFlowsTab()}
         ${this.renderPluginTab()}
       </section>
     `;
@@ -1456,7 +1445,7 @@
           .changeUrl=${this.computeChangeUrl()}
           .editMode=${this.editMode}
           .loggedIn=${this.loggedIn}
-          .shownFileCount=${this.shownFileCount}
+          .shownFileCount=${this.shownFileCount ?? 0}
           .filesExpanded=${this.fileList?.filesExpanded}
           @open-diff-prefs=${this.handleOpenDiffPrefs}
           @open-download-dialog=${this.handleOpenDownloadDialog}
@@ -1464,10 +1453,7 @@
           @collapse-diffs=${this.collapseAllDiffs}
         >
         </gr-file-list-header>
-        ${when(
-          this.flagService.isEnabled(KnownExperimentId.REVISION_PARENTS_DATA),
-          () => html`<gr-revision-parents></gr-revision-parents>`
-        )}
+        <gr-revision-parents></gr-revision-parents>
         <gr-file-list
           id="fileList"
           .change=${this.change}
@@ -1493,9 +1479,8 @@
     return html`
       <h3 class="assistive-tech-only">Comments</h3>
       <gr-thread-list
-        .threads=${this.commentThreads}
+        .threads=${this.commentThreads ?? []}
         .commentTabState=${this.tabState}
-        only-show-robot-comments-with-human-reply
         .unresolvedOnly=${this.unresolvedOnly}
         .scrollCommentId=${this.scrollCommentId}
         show-comment-context
@@ -1511,33 +1496,11 @@
     `;
   }
 
-  private renderFindingsTab() {
-    if (this.activeTab !== Tab.FINDINGS) return nothing;
-    if (!this.showFindingsTab) return nothing;
-    const robotCommentThreads = this.computeRobotCommentThreads();
-    const robotCommentsPatchSetDropdownItems =
-      this.computeRobotCommentsPatchSetDropdownItems();
+  private renderFlowsTab() {
+    if (this.activeTab !== Tab.FLOWS) return nothing;
     return html`
-      <gr-dropdown-list
-        class="patch-set-dropdown"
-        .items=${robotCommentsPatchSetDropdownItems}
-        .value=${this.currentRobotCommentsPatchSet}
-        @value-change=${this.handleRobotCommentPatchSetChanged}
-      >
-      </gr-dropdown-list>
-      <gr-thread-list .threads=${robotCommentThreads} hide-dropdown>
-      </gr-thread-list>
-      ${when(
-        this.showRobotCommentsButton,
-        () => html`
-          <gr-button
-            class="show-robot-comments"
-            @click=${this.toggleShowRobotComments}
-          >
-            ${this.showAllRobotComments ? 'Show Less' : 'Show More'}
-          </gr-button>
-        `
-      )}
+      <h3 class="assistive-tech-only">Flows</h3>
+      <gr-flows></gr-flows>
     `;
   }
 
@@ -1566,11 +1529,13 @@
         </gr-endpoint-param>
       </gr-endpoint-decorator>
 
-      <paper-tabs>
-        <paper-tab data-name="_changeLog" class="changeLog">
-          Change Log
-        </paper-tab>
-      </paper-tabs>
+      <div class="tabs">
+        <md-tabs>
+          <md-secondary-tab data-name="_changeLog" class="changeLog">
+            Change Log
+          </md-secondary-tab>
+        </md-tabs>
+      </div>
       <section class="changeLog">
         <h2 class="assistive-tech-only">Change Log</h2>
         <gr-messages-list
@@ -1584,11 +1549,11 @@
   }
 
   override updated() {
-    const tabs = [...queryAll<HTMLElement>(this.tabs!, 'paper-tab')];
+    const tabs = [...queryAll<HTMLElement>(this.tabs!, 'md-secondary-tab')];
     const tabIndex = tabs.findIndex(t => t.dataset['name'] === this.activeTab);
 
-    if (tabIndex !== -1 && this.tabs!.selected !== tabIndex) {
-      this.tabs!.selected = tabIndex;
+    if (tabIndex !== -1 && this.tabs!.activeTabIndex !== tabIndex) {
+      this.tabs!.activeTabIndex = tabIndex;
     }
     this.reportChangeDisplayed();
     this.reportFullyLoaded();
@@ -1619,12 +1584,13 @@
     }
   }
 
-  onPaperTabSelectionChanged(e: ValueChangedEvent) {
+  onMdTabsChange(e: Event) {
     if (!this.tabs) return;
-    const tabs = [...queryAll<HTMLElement>(this.tabs, 'paper-tab')];
+    const tabs = [...queryAll<HTMLElement>(this.tabs, 'md-secondary-tab')];
     if (!tabs) return;
 
-    const tabIndex = Number(e.detail.value);
+    const activeTabIndex = (e.target as MdTabs).activeTabIndex;
+    const tabIndex = Number(activeTabIndex);
     assert(
       Number.isInteger(tabIndex) && 0 <= tabIndex && tabIndex < tabs.length,
       `${tabIndex} must be integer`
@@ -1648,7 +1614,7 @@
    * tab can also be opened via the url in which case the correct value to
    * unresolvedOnly is never assigned.
    */
-  private onPaperTabClick(e: MouseEvent) {
+  private onMdSecondaryTabClick(e: MouseEvent) {
     let target = e.target as HTMLElement | null;
     let tabName: string | undefined;
     // target can be slot child of papertab, so we search for tabName in parents
@@ -1669,10 +1635,22 @@
 
     this.reporting.reportInteraction(Interaction.SHOW_TAB, {
       tabName,
-      src: 'paper-tab-click',
+      src: 'md-secondary-tab-click',
     });
   }
 
+  /**
+   * Handles keydown on the tab to emulate a click on Enter.
+   * This is needed for a11y since the underlying component does not do this.
+   */
+  private onMdSecondaryTabKeydown(e: KeyboardEvent) {
+    if (e.key === 'Enter') {
+      e.preventDefault();
+      e.stopPropagation();
+      (e.currentTarget as HTMLElement).click();
+    }
+  }
+
   private handleEditingChanged(e: ValueChangedEvent<boolean>) {
     this.editingCommitMessage = e.detail.value;
   }
@@ -1683,7 +1661,7 @@
   }
 
   // Private but used in tests.
-  handleCommitMessageSave(e: EditableContentSaveEvent) {
+  async handleCommitMessageSave(e: EditableContentSaveEvent) {
     assertIsDefined(this.change, 'change');
     assertIsDefined(this.changeNum, 'changeNum');
     // to prevent 2 requests at the same time
@@ -1692,6 +1670,19 @@
     const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
     const committerEmail = e.detail.committerEmail;
 
+    // The BEFORE event handlers are async and can potentially block
+    // the message edit from going through.  By contrast, the second
+    // set of event handlers always fire (and should probably fire
+    // after the message has been saved successfully, but the current
+    // behavior is what it is).
+    if (
+      !(await this.getPluginLoader().jsApiService.handleBeforeCommitMessage(
+        this.change,
+        message
+      ))
+    )
+      return;
+
     this.getPluginLoader().jsApiService.handleCommitMessage(
       this.change,
       message
@@ -1747,76 +1738,6 @@
     return false;
   }
 
-  // Private but used in tests.
-  robotCommentCountPerPatchSet(threads: CommentThread[]) {
-    return threads.reduce((robotCommentCountMap, thread) => {
-      const comments = thread.comments;
-      const robotCommentsCount = comments.reduce(
-        (acc, comment) => (isRobot(comment) ? acc + 1 : acc),
-        0
-      );
-      if (comments[0].patch_set)
-        robotCommentCountMap[`${comments[0].patch_set}`] =
-          (robotCommentCountMap[`${comments[0].patch_set}`] || 0) +
-          robotCommentsCount;
-      return robotCommentCountMap;
-    }, {} as {[patchset: string]: number});
-  }
-
-  // Private but used in tests.
-  computeText(
-    patch: RevisionInfo | EditRevisionInfo,
-    commentThreads: CommentThread[]
-  ) {
-    const commentCount = this.robotCommentCountPerPatchSet(commentThreads);
-    const commentCnt = commentCount[patch._number] || 0;
-    if (commentCnt === 0) return `Patchset ${patch._number}`;
-    return `Patchset ${patch._number} (${pluralize(commentCnt, 'finding')})`;
-  }
-
-  private computeRobotCommentsPatchSetDropdownItems() {
-    if (!this.change || !this.commentThreads || !this.change.revisions)
-      return [];
-
-    return Object.values(this.change.revisions)
-      .filter(patch => patch._number !== EDIT)
-      .map(patch => {
-        return {
-          text: this.computeText(patch, this.commentThreads!),
-          value: patch._number,
-        };
-      })
-      .sort((a, b) => (b.value as number) - (a.value as number));
-  }
-
-  private handleRobotCommentPatchSetChanged(e: CustomEvent<{value: string}>) {
-    const patchSet = Number(e.detail.value) as PatchSetNum;
-    if (patchSet === this.currentRobotCommentsPatchSet) return;
-    this.currentRobotCommentsPatchSet = patchSet;
-  }
-
-  private toggleShowRobotComments() {
-    this.showAllRobotComments = !this.showAllRobotComments;
-  }
-
-  // Private but used in tests.
-  computeRobotCommentThreads() {
-    if (!this.commentThreads || !this.currentRobotCommentsPatchSet) return [];
-    const threads = this.commentThreads.filter(thread => {
-      const comments = thread.comments || [];
-      return (
-        comments.length &&
-        isRobot(comments[0]) &&
-        comments[0].patch_set === this.currentRobotCommentsPatchSet
-      );
-    });
-    this.showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT;
-    return threads.slice(
-      0,
-      this.showAllRobotComments ? undefined : ROBOT_COMMENTS_LIMIT
-    );
-  }
-
   private computeTotalCommentCounts() {
     const unresolvedCount = this.change?.unresolved_comment_count ?? 0;
     const draftCount = this.draftCount;
@@ -1872,19 +1793,6 @@
       assertIsDefined(this.downloadModal);
       assertIsDefined(this.downloadDialog);
       this.downloadDialog.focus();
-      const downloadCommands = queryAndAssert(
-        this.downloadDialog,
-        'gr-download-commands'
-      );
-      const paperTabs = queryAndAssert<PaperTabsElement>(
-        downloadCommands,
-        'paper-tabs'
-      );
-      // Paper Tabs normally listen to 'iron-resize' event to call this method.
-      // After migrating to Dialog element, this event is no longer fired
-      // which means this method is not called which ends up styling the
-      // selected paper tab with an underline.
-      paperTabs._onTabSizingChanged();
     });
   }
 
@@ -2006,14 +1914,6 @@
     this.allPatchSets = computeAllPatchSets(this.change);
     if (!this.change) return;
     this.labelsChanged(oldChange?.labels, this.change.labels);
-    if (
-      this.change.current_revision &&
-      this.change.revisions &&
-      this.change.revisions[this.change.current_revision]
-    ) {
-      this.currentRobotCommentsPatchSet =
-        this.change.revisions[this.change.current_revision]._number;
-    }
   }
 
   /**
@@ -2329,6 +2229,14 @@
   }
 
   private startUpdateCheckTimer() {
+    // Check if there are change updates provider already configured
+    // If yes, then polling for new updates is not required as the provider
+    // will send a signal instead.
+    const updateProviders =
+      this.getPluginLoader().pluginsModel.getChangeUpdatesPlugins();
+    if (updateProviders.length > 0) {
+      return;
+    }
     const delay = this.serverConfig?.change?.update_delay ?? 0;
     if (delay <= MIN_CHECK_INTERVAL_SECS) return;
 
@@ -2337,51 +2245,7 @@
         this.startUpdateCheckTimer();
         return;
       }
-      const change = this.change;
-      this.getChangeModel()
-        .fetchChangeUpdates(change, /* includeExtraOptions = */ true)
-        .then(result => {
-          let toastMessage = null;
-          if (!result.isLatest) {
-            toastMessage = ReloadToastMessage.NEWER_REVISION;
-          } else if (result.newStatus === ChangeStatus.MERGED) {
-            toastMessage = ReloadToastMessage.MERGED;
-          } else if (result.newStatus === ChangeStatus.ABANDONED) {
-            toastMessage = ReloadToastMessage.ABANDONED;
-          } else if (result.newStatus === ChangeStatus.NEW) {
-            toastMessage = ReloadToastMessage.RESTORED;
-          } else if (result.newMessages) {
-            toastMessage = ReloadToastMessage.NEW_MESSAGE;
-            if (result.newMessages.author?.name) {
-              toastMessage += ` from ${result.newMessages.author.name}`;
-            }
-          }
-
-          // We have to make sure that the update is still relevant for the user.
-          // Since starting to fetch the change update the user may have sent a
-          // reply, or the change might have been reloaded, or it could be in the
-          // process of being reloaded.
-          const changeWasReloaded = change !== this.change;
-          if (
-            !toastMessage ||
-            this.loading ||
-            changeWasReloaded ||
-            !this.isViewCurrent
-          ) {
-            this.startUpdateCheckTimer();
-            return;
-          }
-
-          this.cancelUpdateCheckTimer();
-          fire(this, 'show-alert', {
-            message: toastMessage,
-            // Persist this alert.
-            dismissOnNavigation: true,
-            showDismiss: true,
-            action: 'Reload',
-            callback: () => this.getChangeModel().navigateToChangeResetReload(),
-          });
-        });
+      this.getChangeModel().throttledShowUpdateChangeNotification();
     }, delay * 1000);
   }
 
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 132e847..0409f80 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
@@ -11,8 +11,8 @@
 import {
   ChangeStatus,
   CommentSide,
-  DiffViewMode,
   createDefaultPreferences,
+  DiffViewMode,
   Tab,
 } from '../../../constants/constants';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
@@ -29,34 +29,32 @@
   waitUntilVisible,
 } from '../../../test/test-utils';
 import {
-  createChangeViewState,
+  createAccountDetailWithId,
   createChangeMessages,
+  createChangeViewChange,
+  createChangeViewState,
+  createParsedChange,
   createRevision,
   createRevisions,
   createServerInfo,
   createUserConfig,
   TEST_NUMERIC_CHANGE_ID,
   TEST_PROJECT_NAME,
-  createChangeViewChange,
-  createAccountDetailWithId,
-  createParsedChange,
 } from '../../../test/test-data-generators';
 import {GrChangeView} from './gr-change-view';
 import {
   AccountId,
   BasePatchSetNum,
+  CommentThread,
   CommitId,
   EDIT,
   NumericChangeId,
   PARENT,
+  RepoName,
   RevisionPatchSetNum,
-  RobotId,
-  RobotCommentInfo,
+  SavingState,
   Timestamp,
   UrlEncodedCommentId,
-  RepoName,
-  CommentThread,
-  SavingState,
 } from '../../../types/common';
 import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
 import {SinonFakeTimers} from 'sinon';
@@ -70,9 +68,8 @@
 import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
 import {GrThreadList} from '../gr-thread-list/gr-thread-list';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {Modifier} from '../../../utils/dom-util';
-import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrCopyLinks} from '../gr-copy-links/gr-copy-links';
 import {
   ChangeChildView,
@@ -90,8 +87,6 @@
   let userModel: UserModel;
   let changeModel: ChangeModel;
 
-  const ROBOT_COMMENTS_LIMIT = 10;
-
   // TODO: should have a mock service to generate VALID fake data
   const THREADS: CommentThread[] = [
     {
@@ -103,21 +98,6 @@
             name: 'user',
             username: 'user',
           },
-          patch_set: 2 as RevisionPatchSetNum,
-          robot_id: 'rb1' as RobotId,
-          id: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId,
-          line: 5,
-          updated: '2018-02-08 18:49:18.000000000' as Timestamp,
-          message: 'test',
-          unresolved: true,
-        },
-        {
-          path: '/COMMIT_MSG',
-          author: {
-            _account_id: 1000000 as AccountId,
-            name: 'user',
-            username: 'user',
-          },
           patch_set: 4 as RevisionPatchSetNum,
           id: 'ecf0b9fa_fe1a5f62_1' as UrlEncodedCommentId,
           line: 5,
@@ -146,21 +126,6 @@
     {
       comments: [
         {
-          path: '/COMMIT_MSG',
-          author: {
-            _account_id: 1000000 as AccountId,
-            name: 'user',
-            username: 'user',
-          },
-          patch_set: 3 as RevisionPatchSetNum,
-          id: 'ecf0b9fa_fe5f62' as UrlEncodedCommentId,
-          robot_id: 'rb2' as RobotId,
-          line: 5,
-          updated: '2018-02-08 18:49:18.000000000' as Timestamp,
-          message: 'test',
-          unresolved: true,
-        },
-        {
           path: 'test.txt',
           author: {
             _account_id: 1000000 as AccountId,
@@ -208,7 +173,6 @@
         {
           path: '/COMMIT_MSG',
           author: {
-            // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
             _account_id: 1000000 as AccountId,
             name: 'user',
             username: 'user',
@@ -247,23 +211,7 @@
       commentSide: CommentSide.REVISION,
     },
     {
-      comments: [
-        {
-          path: '/COMMIT_MSG',
-          author: {
-            _account_id: 1000000 as AccountId,
-            name: 'user',
-            username: 'user',
-          },
-          patch_set: 4 as RevisionPatchSetNum,
-          id: 'rc1' as UrlEncodedCommentId,
-          line: 5,
-          updated: '2019-02-08 18:49:18.000000000' as Timestamp,
-          message: 'test',
-          unresolved: true,
-          robot_id: 'rc1' as RobotId,
-        },
-      ],
+      comments: [],
       patchNum: 4 as RevisionPatchSetNum,
       path: '/COMMIT_MSG',
       line: 5,
@@ -280,21 +228,6 @@
             username: 'user',
           },
           patch_set: 4 as RevisionPatchSetNum,
-          id: 'rc2' as UrlEncodedCommentId,
-          line: 5,
-          updated: '2019-03-08 18:49:18.000000000' as Timestamp,
-          message: 'test',
-          unresolved: true,
-          robot_id: 'rc2' as RobotId,
-        },
-        {
-          path: '/COMMIT_MSG',
-          author: {
-            _account_id: 1000000 as AccountId,
-            name: 'user',
-            username: 'user',
-          },
-          patch_set: 4 as RevisionPatchSetNum,
           id: 'c2_1' as UrlEncodedCommentId,
           line: 5,
           updated: '2019-03-08 18:49:18.000000000' as Timestamp,
@@ -331,8 +264,10 @@
     stubRestApi('getAccount').returns(
       Promise.resolve(createAccountDetailWithId(5))
     );
+    stubRestApi('getIfFlowsIsEnabled').returns(
+      Promise.resolve({enabled: true})
+    );
     stubRestApi('getDiffComments').returns(Promise.resolve({}));
-    stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
     stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
     window.Gerrit.install(
@@ -381,16 +316,20 @@
               <h1 class="assistive-tech-only">Change :</h1>
               <div class="headerTitle">
                 <div class="changeStatuses"></div>
-                <gr-button
-                  aria-disabled="false"
-                  class="showCopyLinkDialogButton"
-                  down-arrow=""
-                  flatten=""
-                  role="button"
-                  tabindex="0"
-                  ><gr-change-star id="changeStar"> </gr-change-star>
-                  <a aria-label="Change undefined" class="changeNumber"> </a>
-                </gr-button>
+                <div class="changeStarContainer">
+                  <gr-button
+                    aria-disabled="false"
+                    class="showCopyLinkDialogButton"
+                    down-arrow=""
+                    flatten=""
+                    id="copyLinkDialogButton"
+                    role="button"
+                    tabindex="0"
+                  >
+                    <gr-change-star id="changeStar"> </gr-change-star>
+                    <a aria-label="Change undefined" class="changeNumber"> </a>
+                  </gr-button>
+                </div>
                 <div class="headerSubject"></div>
                 <gr-copy-clipboard
                   class="changeCopyClipboard"
@@ -430,7 +369,7 @@
                         id="commitMessageEditor"
                         remove-zero-width-space=""
                       >
-                        <gr-formatted-text></gr-formatted-text>
+                        <gr-formatted-text> </gr-formatted-text>
                       </gr-editable-content>
                     </div>
                     <h3 class="assistive-tech-only">
@@ -443,7 +382,7 @@
                     </gr-endpoint-decorator>
                   </div>
                   <div class="relatedChanges">
-                    <gr-related-changes-list></gr-related-changes-list>
+                    <gr-related-changes-list> </gr-related-changes-list>
                   </div>
                   <div class="emptySpace"></div>
                 </div>
@@ -451,45 +390,42 @@
             </div>
           </section>
           <h2 class="assistive-tech-only">Files and Comments tabs</h2>
-          <paper-tabs dir="null" id="tabs" role="tablist" tabindex="0">
-            <paper-tab
-              aria-disabled="false"
-              aria-selected="true"
-              class="iron-selected"
-              data-name="files"
-              role="tab"
-              tabindex="0"
-            >
-              <span> Files </span>
-            </paper-tab>
-            <paper-tab
-              aria-disabled="false"
-              aria-selected="false"
-              class="commentThreads"
-              data-name="comments"
-              role="tab"
-              tabindex="-1"
-            >
-              <gr-tooltip-content has-tooltip="" title="">
-                <span> Comments </span>
-              </gr-tooltip-content>
-            </paper-tab>
-            <paper-tab
-              aria-disabled="false"
-              aria-selected="false"
-              data-name="change-view-tab-header-url"
-              role="tab"
-              tabindex="-1"
-            >
-              <gr-endpoint-decorator name="change-view-tab-header-url">
-                <gr-endpoint-param name="change"> </gr-endpoint-param>
-                <gr-endpoint-param name="revision"> </gr-endpoint-param>
-              </gr-endpoint-decorator>
-            </paper-tab>
-          </paper-tabs>
+          <div class="tabs">
+            <md-tabs id="tabs">
+              <md-secondary-tab
+                active=""
+                data-name="files"
+                md-tab=""
+                tabindex="0"
+              >
+                <span> Files </span>
+              </md-secondary-tab>
+              <md-secondary-tab
+                class="commentThreads"
+                data-name="comments"
+                md-tab=""
+                tabindex="-1"
+              >
+                <gr-tooltip-content has-tooltip="" title="">
+                  <span> Comments </span>
+                </gr-tooltip-content>
+              </md-secondary-tab>
+              <md-secondary-tab
+                data-name="change-view-tab-header-url"
+                md-tab=""
+                tabindex="0"
+              >
+                <gr-endpoint-decorator name="change-view-tab-header-url">
+                  <gr-endpoint-param name="change"> </gr-endpoint-param>
+                  <gr-endpoint-param name="revision"> </gr-endpoint-param>
+                </gr-endpoint-decorator>
+              </md-secondary-tab>
+            </md-tabs>
+          </div>
           <section class="tabContent">
             <div>
               <gr-file-list-header id="fileListHeader"> </gr-file-list-header>
+              <gr-revision-parents> </gr-revision-parents>
               <gr-file-list id="fileList"> </gr-file-list>
             </div>
           </section>
@@ -497,18 +433,19 @@
             <gr-endpoint-param name="change"> </gr-endpoint-param>
             <gr-endpoint-param name="revision"> </gr-endpoint-param>
           </gr-endpoint-decorator>
-          <paper-tabs dir="null" role="tablist" tabindex="0">
-            <paper-tab
-              aria-disabled="false"
-              aria-selected="false"
-              class="changeLog"
-              data-name="_changeLog"
-              role="tab"
-              tabindex="-1"
-            >
-              Change Log
-            </paper-tab>
-          </paper-tabs>
+          <div class="tabs">
+            <md-tabs>
+              <md-secondary-tab
+                active=""
+                class="changeLog"
+                data-name="_changeLog"
+                md-tab=""
+                tabindex="0"
+              >
+                Change Log
+              </md-secondary-tab>
+            </md-tabs>
+          </div>
           <section class="changeLog">
             <h2 class="assistive-tech-only">Change Log</h2>
             <gr-messages-list> </gr-messages-list>
@@ -540,6 +477,15 @@
     assert.isTrue(replaceStateStub.called);
   });
 
+  test('renders flows tab if experiment is enabled', async () => {
+    element.isFlowsEnabled = true;
+    stubFlags('isEnabled').returns(true);
+    element.requestUpdate();
+    await element.updateComplete;
+    await waitUntil(() => !!element.isFlowsEnabled);
+    queryAndAssert(element, '[data-name="flows"]');
+  });
+
   test('handleDiffAgainstBase', () => {
     element.change = {
       ...createChangeViewChange(),
@@ -636,12 +582,12 @@
         element.pluginTabsHeaderEndpoints.includes('change-view-tab-header-url')
       );
       const tabs = element.shadowRoot!.querySelector('#tabs')!;
-      const paperTabs = tabs.querySelectorAll<HTMLElement>('paper-tab');
+      const mdTabs = tabs.querySelectorAll<HTMLElement>('md-secondary-tab');
       // 4 Tabs are : Files, Comment Threads, Plugin
-      assert.equal(tabs.querySelectorAll('paper-tab').length, 3);
-      assert.equal(paperTabs[0].dataset.name, 'files');
-      assert.equal(paperTabs[1].dataset.name, 'comments');
-      assert.equal(paperTabs[2].dataset.name, 'change-view-tab-header-url');
+      assert.equal(tabs.querySelectorAll('md-secondary-tab').length, 3);
+      assert.equal(mdTabs[0].dataset.name, 'files');
+      assert.equal(mdTabs[1].dataset.name, 'comments');
+      assert.equal(mdTabs[2].dataset.name, 'change-view-tab-header-url');
     });
 
     test('setActiveTab switched tab correctly', async () => {
@@ -681,8 +627,9 @@
     });
 
     test('switching to plugin tab renders the plugin tab content', async () => {
-      const paperTabs = element.shadowRoot!.querySelector('#tabs')!;
-      paperTabs.querySelectorAll('paper-tab')[2].click();
+      const mdTabs = element.shadowRoot!.querySelector('#tabs')!;
+      mdTabs.querySelectorAll<HTMLElement>('md-secondary-tab')[2].click();
+      await element.updateComplete;
       await element.updateComplete;
       const tabContent = queryAndAssert(element, '.tabContent');
       const endpoint = queryAndAssert(tabContent, 'gr-endpoint-decorator');
@@ -942,8 +889,8 @@
       };
       element.commentThreads = THREADS;
       await element.updateComplete;
-      const paperTabs = element.shadowRoot!.querySelector('#tabs')!;
-      const tabs = paperTabs.querySelectorAll('paper-tab');
+      const mdTabs = element.shadowRoot!.querySelector('#tabs')!;
+      const tabs = mdTabs.querySelectorAll<HTMLElement>('md-secondary-tab');
       assert.isTrue(tabs.length > 1);
       assert.equal(tabs[1].dataset.name, 'comments');
       tabs[1].click();
@@ -965,112 +912,6 @@
     });
   });
 
-  suite('Findings robot-comment tab', () => {
-    setup(async () => {
-      element.changeNum = TEST_NUMERIC_CHANGE_ID;
-      element.change = {
-        ...createChangeViewChange(),
-        revisions: {
-          rev2: createRevision(2),
-          rev1: createRevision(1),
-          rev13: createRevision(13),
-          rev3: createRevision(3),
-          rev4: createRevision(4),
-        },
-        current_revision: 'rev4' as CommitId,
-      };
-      element.commentThreads = THREADS;
-      element.showFindingsTab = true;
-      await element.updateComplete;
-      const paperTabs = element.shadowRoot!.querySelector('#tabs')!;
-      const tabs = paperTabs.querySelectorAll('paper-tab');
-      assert.isTrue(tabs.length > 3);
-      assert.equal(tabs[3].dataset.name, 'findings');
-      tabs[3].click();
-      await element.updateComplete;
-    });
-
-    test('robot comments count per patchset', () => {
-      const count = element.robotCommentCountPerPatchSet(THREADS);
-      const expectedCount = {
-        2: 1,
-        3: 1,
-        4: 2,
-      };
-      assert.deepEqual(count, expectedCount);
-      assert.equal(
-        element.computeText(createRevision(2), THREADS),
-        'Patchset 2 (1 finding)'
-      );
-      assert.equal(
-        element.computeText(createRevision(4), THREADS),
-        'Patchset 4 (2 findings)'
-      );
-      assert.equal(
-        element.computeText(createRevision(5), THREADS),
-        'Patchset 5'
-      );
-    });
-
-    test('only robot comments are rendered', () => {
-      assert.equal(element.computeRobotCommentThreads().length, 2);
-      assert.equal(
-        (
-          element.computeRobotCommentThreads()[0]
-            .comments[0] as RobotCommentInfo
-        ).robot_id,
-        'rc1'
-      );
-      assert.equal(
-        (
-          element.computeRobotCommentThreads()[1]
-            .comments[0] as RobotCommentInfo
-        ).robot_id,
-        'rc2'
-      );
-    });
-
-    test('changing patchsets resets robot comments', async () => {
-      assertIsDefined(element.change);
-      const newChange = {...element.change};
-      newChange.current_revision = 'rev3' as CommitId;
-      element.change = newChange;
-      await element.updateComplete;
-      assert.equal(element.computeRobotCommentThreads().length, 1);
-    });
-
-    test('Show more button is hidden', () => {
-      assert.isNull(element.shadowRoot!.querySelector('.show-robot-comments'));
-    });
-
-    suite('robot comments show more button', () => {
-      setup(async () => {
-        const arr = [];
-        for (let i = 0; i <= 30; i++) {
-          arr.push(...THREADS);
-        }
-        element.commentThreads = arr;
-        await element.updateComplete;
-      });
-
-      test('Show more button is rendered', () => {
-        assert.isOk(element.shadowRoot!.querySelector('.show-robot-comments'));
-        assert.equal(
-          element.computeRobotCommentThreads().length,
-          ROBOT_COMMENTS_LIMIT
-        );
-      });
-
-      test('Clicking show more button renders all comments', async () => {
-        element
-          .shadowRoot!.querySelector<GrButton>('.show-robot-comments')!
-          .click();
-        await element.updateComplete;
-        assert.equal(element.computeRobotCommentThreads().length, 62);
-      });
-    });
-  });
-
   test('reply button is a login button when logged out', async () => {
     assertIsDefined(element.replyBtn);
     element.loggedIn = false;
@@ -1203,15 +1044,17 @@
       });
 
     assertIsDefined(element.commitMessageEditor);
-    element.handleCommitMessageSave(
+    await element.handleCommitMessageSave(
       mockEvent('test \n  test ', committerEmail)
     );
     assert.equal(putStub.lastCall.args[1], 'test\n  test');
     element.commitMessageEditor.disabled = false;
-    element.handleCommitMessageSave(mockEvent('  test\ntest', committerEmail));
+    await element.handleCommitMessageSave(
+      mockEvent('  test\ntest', committerEmail)
+    );
     assert.equal(putStub.lastCall.args[1], '  test\ntest');
     element.commitMessageEditor.disabled = false;
-    element.handleCommitMessageSave(
+    await element.handleCommitMessageSave(
       mockEvent('\n\n\n\n\n\n\n\n', committerEmail)
     );
     assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
@@ -1536,7 +1379,7 @@
     const stub = sinon.stub(element, 'handleToggleStar');
 
     const changeStar = queryAndAssert<GrChangeStar>(element, '#changeStar');
-    queryAndAssert<HTMLButtonElement>(changeStar, 'button')!.click();
+    queryAndAssert<HTMLButtonElement>(changeStar, 'button').click();
     assert.isTrue(stub.called);
   });
 
diff --git a/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts b/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts
index 9123cd6..91a2a68 100644
--- a/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary.ts
@@ -6,13 +6,11 @@
 import '../gr-change-summary/gr-summary-chip';
 import '../../shared/gr-avatar/gr-avatar-stack';
 import '../../shared/gr-icon/gr-icon';
-import {LitElement, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {
   getFirstComment,
-  hasHumanReply,
   isResolved,
-  isRobotThread,
   isUnresolved,
 } from '../../../utils/comment-util';
 import {pluralize} from '../../../utils/string-util';
@@ -96,9 +94,7 @@
   }
 
   override render() {
-    const commentThreads =
-      this.commentThreads?.filter(t => !isRobotThread(t) || hasHumanReply(t)) ??
-      [];
+    const commentThreads = this.commentThreads ?? [];
     const countResolvedComments = commentThreads.filter(isResolved).length;
     const unresolvedThreads = commentThreads.filter(isUnresolved);
     const countUnresolvedComments = unresolvedThreads.length;
diff --git a/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary_test.ts b/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary_test.ts
index d8eb246..f1cea2d 100644
--- a/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-comments-summary/gr-comments-summary_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrCommentsSummary} from './gr-comments-summary';
 import {
   createComment,
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index e85bcf4..20a16aa 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -12,7 +12,7 @@
   WebLinkInfo,
 } from '../../../types/common';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
@@ -51,6 +51,9 @@
         gr-weblink {
           margin-right: 0;
         }
+        gr-copy-clipboard {
+          margin-left: var(--spacing-s);
+        }
       `,
     ];
   }
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
index 3fed767..93be41e8e 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
@@ -11,7 +11,7 @@
   createServerInfo,
 } from '../../../test/test-data-generators';
 import {CommitId} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {queryAndAssert} from '../../../utils/common-util';
 
 suite('gr-commit-info tests', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
index ec34923..4c36c5f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
@@ -3,19 +3,18 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../shared/gr-dialog/gr-dialog';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import {Key, Modifier} from '../../../utils/dom-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
-import {BindValueChangeEvent} from '../../../types/events';
 import {ShortcutController} from '../../lit/shortcut-controller';
 import {ChangeActionDialog} from '../../../types/common';
 import {fireNoBubble} from '../../../utils/event-util';
 import {formStyles} from '../../../styles/form-styles';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -40,7 +39,7 @@
    * @event cancel
    */
 
-  @query('#messageInput') private messageInput?: IronAutogrowTextareaElement;
+  @query('#messageInput') private messageInput?: GrAutogrowTextarea;
 
   @property({type: String})
   message = '';
@@ -82,7 +81,7 @@
           display: block;
           width: 100%;
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           font-family: var(--monospace-font-family);
           font-size: var(--font-size-mono);
           line-height: var(--line-height-mono);
@@ -106,16 +105,14 @@
         <div class="header" slot="header">Abandon Change</div>
         <div class="main" slot="main">
           <label for="messageInput">Abandon Message</label>
-          <iron-autogrow-textarea
+          <gr-autogrow-textarea
             id="messageInput"
             class="message"
             autocomplete="on"
             placeholder="&lt;Insert reasoning here&gt;"
-            .bindValue=${this.message}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
-              this.handleBindValueChanged(e);
-            }}
-          ></iron-autogrow-textarea>
+            .value=${this.message}
+            @input=${this.handleInputChanged}
+          ></gr-autogrow-textarea>
         </div>
       </gr-dialog>
     `;
@@ -123,7 +120,7 @@
 
   resetFocus() {
     assertIsDefined(this.messageInput, 'messageInput');
-    this.messageInput.textarea.focus();
+    this.messageInput.focus();
   }
 
   // private but used in test
@@ -145,7 +142,8 @@
     fireNoBubble(this, 'cancel', {});
   }
 
-  private handleBindValueChanged(e: BindValueChangeEvent) {
-    this.message = e.detail.value ?? '';
+  private handleInputChanged(e: InputEvent) {
+    const value = (e.target as GrAutogrowTextarea).value ?? '';
+    this.message = value;
   }
 }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
index e449332..ee2d541 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
@@ -9,7 +9,7 @@
 import {GrConfirmAbandonDialog} from './gr-confirm-abandon-dialog';
 import {queryAndAssert} from '../../../test/test-utils';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-confirm-abandon-dialog tests', () => {
   let element: GrConfirmAbandonDialog;
@@ -28,14 +28,13 @@
           <div class="header" slot="header">Abandon Change</div>
           <div class="main" slot="main">
             <label for="messageInput"> Abandon Message </label>
-            <iron-autogrow-textarea
-              aria-disabled="false"
+            <gr-autogrow-textarea
               autocomplete="on"
               class="message"
               id="messageInput"
               placeholder="<Insert reasoning here>"
             >
-            </iron-autogrow-textarea>
+            </gr-autogrow-textarea>
           </div>
         </gr-dialog>
       `
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts
index 61cc227..f13eaa8 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {queryAndAssert} from '../../../utils/common-util';
 import './gr-confirm-cherrypick-conflict-dialog';
@@ -57,7 +57,7 @@
     queryAndAssert<GrButton>(
       queryAndAssert<GrDialog>(element, 'gr-dialog'),
       'gr-button#cancel'
-    )!.click();
+    ).click();
     await element.updateComplete;
 
     assert.isTrue(cancelHandler.called);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index 0f8d045..5124f58 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -3,33 +3,32 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '@polymer/iron-input/iron-input';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../../styles/shared-styles';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-dialog/gr-dialog';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {getAppContext} from '../../../services/app-context';
 import {
-  ChangeInfo,
   BranchName,
-  RepoName,
-  CommitId,
-  ChangeInfoId,
-  TopicName,
   ChangeActionDialog,
+  ChangeInfo,
+  ChangeInfoId,
+  CommitId,
   EmailInfo,
   GitPersonInfo,
+  RepoName,
+  TopicName,
 } from '../../../types/common';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
   AutocompleteQuery,
-  AutocompleteSuggestion,
   GrTypedAutocomplete,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {
-  HttpMethod,
   ChangeStatus,
+  HttpMethod,
   ProgressStatus,
 } from '../../../constants/constants';
 import {subscribe} from '../../lit/subscription-controller';
@@ -48,6 +47,11 @@
 import {formStyles} from '../../../styles/form-styles';
 import {branchName} from '../../../utils/patch-set-util';
 import {changeModelToken} from '../../../models/change/change-model';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/radio/radio';
+import '@material/web/checkbox/checkbox';
 
 const SUGGESTIONS_LIMIT = 15;
 const CHANGE_SUBJECT_LIMIT = 50;
@@ -144,7 +148,8 @@
   @state()
   latestCommitter?: GitPersonInfo;
 
-  private selectedChangeIds = new Set<ChangeInfoId>();
+  @state()
+  selectedChangeIds = new Set<ChangeInfoId>();
 
   private readonly restApiService = getAppContext().restApiService;
 
@@ -185,6 +190,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       formStyles,
       sharedStyles,
       css`
@@ -208,15 +214,19 @@
           display: block;
           width: 100%;
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           font-family: var(--monospace-font-family);
           font-size: var(--font-size-mono);
           line-height: var(--line-height-mono);
           width: 73ch; /* Add a char to account for the border. */
         }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
         .cherryPickTopicLayout {
           display: flex;
           align-items: center;
+          margin-top: var(--spacing-l);
           margin-bottom: var(--spacing-m);
         }
         .cherryPickSingleChange,
@@ -261,12 +271,8 @@
       <gr-dialog
         confirm-label="Cherry Pick"
         .cancelLabel=${this.computeCancelLabel()}
-        ?disabled=${this.computeDisableCherryPick(
-          this.cherryPickType,
-          this.duplicateProjectChanges,
-          this.statuses,
-          this.branch
-        )}
+        ?disabled=${this.computeDisableCherryPick()}
+        title=${this.computeTitleForCherryPick()}
         @confirm=${this.handleConfirmTap}
         @cancel=${this.handleCancelTap}
       >
@@ -287,6 +293,7 @@
               .text=${this.branch}
               .query=${this.query}
               placeholder="Destination branch"
+              .showBlueFocusBorder=${true}
               @text-changed=${(e: BindValueChangeEvent) =>
                 (this.branch = e.detail.value as BranchName)}
             >
@@ -316,24 +323,24 @@
   private renderCherrypickTopicLayout() {
     return html`
       <div class="cherryPickTopicLayout">
-        <input
+        <md-radio
           name="cherryPickOptions"
-          type="radio"
           id="cherryPickSingleChange"
-          @change=${this.handlecherryPickSingleChangeClicked}
           checked
-        />
+          @change=${this.handlecherryPickSingleChangeClicked}
+        >
+        </md-radio>
         <label for="cherryPickSingleChange" class="cherryPickSingleChange">
           Cherry Pick single change
         </label>
       </div>
       <div class="cherryPickTopicLayout">
-        <input
+        <md-radio
           name="cherryPickOptions"
-          type="radio"
           id="cherryPickTopic"
           @change=${this.handlecherryPickTopicClicked}
-        />
+        >
+        </md-radio>
         <label for="cherryPickTopic" class="cherryPickTopic">
           Cherry Pick entire topic (${this.changesCount} Changes)
         </label>
@@ -344,35 +351,37 @@
   private renderCherrypickSingleChangeInputs() {
     return html`
       <label for="baseInput"> Provide base commit sha1 for cherry-pick </label>
-      <iron-input
-        .bindValue=${this.baseCommit}
-        @bind-value-changed=${(e: BindValueChangeEvent) =>
-          (this.baseCommit = e.detail.value)}
+      <md-outlined-text-field
+        id="baseCommitInput"
+        class="showBlueFocusBorder"
+        maxlength="40"
+        placeholder="(optional)"
+        .value=${this.baseCommit ?? ''}
+        @input=${(e: InputEvent) => {
+          const target = e.target as HTMLInputElement;
+          this.baseCommit = target.value;
+        }}
       >
-        <input
-          is="iron-input"
-          id="baseCommitInput"
-          maxlength="40"
-          placeholder="(optional)"
-        />
-      </iron-input>
+      </md-outlined-text-field>
       <label for="messageInput"> Cherry Pick Commit Message </label>
-      <iron-autogrow-textarea
+      <gr-autogrow-textarea
         id="messageInput"
         class="message"
         autocomplete="on"
-        rows="4"
+        .rows=${4}
         .maxRows=${15}
-        .bindValue=${this.message}
-        @bind-value-changed=${(e: BindValueChangeEvent) =>
-          (this.message = e.detail.value ?? '')}
-      ></iron-autogrow-textarea>
+        .value=${this.message}
+        @input=${(e: InputEvent) => {
+          const value = (e.target as GrAutogrowTextarea).value ?? '';
+          this.message = value;
+        }}
+      ></gr-autogrow-textarea>
       ${when(
         this.canShowEmailDropdown(),
         () => html`<div id="cherryPickEmailDropdown">Cherry Pick Committer Email
             <gr-dropdown-list
                 .items=${this.getEmailDropdownItems()}
-                .value=${this.committerEmail}
+                .value=${this.committerEmail ?? ''}
                 @value-change=${this.setCommitterEmail}
             >
             </gr-dropdown-list>
@@ -405,12 +414,11 @@
             item => html`
               <tr>
                 <td>
-                  <input
-                    type="checkbox"
+                  <md-checkbox
                     data-item=${item.id as string}
-                    @change=${this.toggleChangeSelected}
                     ?checked=${this.isChangeSelected(item.id)}
-                  />
+                    @change=${this.toggleChangeSelected}
+                  ></md-checkbox>
                 </td>
                 <td><span> ${this.getChangeId(item)} </span></td>
                 <td><span> ${item.status} </span></td>
@@ -438,6 +446,28 @@
     `;
   }
 
+  // private but compute in tests
+  computeTitleForCherryPick() {
+    if (!this.computeDisableCherryPick()) {
+      return 'Cherry pick changes';
+    }
+    if (this.noChangesSelected()) {
+      return 'Disabled because no changes are selected';
+    }
+
+    if (!this.branch) {
+      return 'Disabled because no branch is selected';
+    }
+
+    if (
+      this.cherryPickType === CherryPickType.TOPIC &&
+      this.duplicateProjectChanges
+    ) {
+      return 'Duplicate projects selected';
+    }
+    return 'Cherry pick button disabled';
+  }
+
   containsDuplicateProject(changes: (ChangeInfo | ParsedChangeInfo)[]) {
     const projects: {[projectName: string]: boolean} = {};
     for (let i = 0; i < changes.length; i++) {
@@ -481,6 +511,8 @@
       this.selectedChangeIds.has(change.id)
     );
     this.duplicateProjectChanges = this.containsDuplicateProject(changes);
+    // Explicitly trigger a re-render
+    this.selectedChangeIds = new Set(this.selectedChangeIds);
   }
 
   private computeTopicErrorMessage() {
@@ -533,18 +565,24 @@
     return isRunningChange ? 'Close' : 'Cancel';
   }
 
-  private computeDisableCherryPick(
-    cherryPickType: CherryPickType,
-    duplicateProjectChanges: boolean,
-    statuses: Statuses,
-    branch: BranchName
-  ) {
-    if (!branch) return true;
+  private noChangesSelected() {
+    const changes = this.changes.filter(change =>
+      this.selectedChangeIds.has(change.id)
+    );
+    if (this.cherryPickType === CherryPickType.TOPIC && changes.length === 0)
+      return true;
+    return false;
+  }
+
+  private computeDisableCherryPick() {
+    if (!this.branch) return true;
+    if (this.noChangesSelected()) return true;
     const duplicateProject =
-      cherryPickType === CherryPickType.TOPIC && duplicateProjectChanges;
+      this.cherryPickType === CherryPickType.TOPIC &&
+      this.duplicateProjectChanges;
     if (duplicateProject) return true;
-    if (!statuses) return false;
-    const isRunningChange = Object.values(statuses).some(
+    if (!this.statuses) return false;
+    const isRunningChange = Object.values(this.statuses).some(
       v => v.status === ProgressStatus.RUNNING
     );
     return isRunningChange;
@@ -600,8 +638,6 @@
       this.selectedChangeIds.has(change.id)
     );
     if (!changes.length) {
-      const errorSpan = this.shadowRoot?.querySelector('.error-message');
-      errorSpan!.innerHTML = 'No change selected';
       return;
     }
     const topic = this.generateRandomCherryPickTopic(changes[0]);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
index d3e48ca..27ac9bc 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.ts
@@ -30,7 +30,7 @@
 import {createChange, createRevision} from '../../../test/test-data-generators';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {ProgressStatus} from '../../../constants/constants';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 
 const CHERRY_PICK_TYPES = {
@@ -82,7 +82,7 @@
   let element: GrConfirmCherrypickDialog;
 
   setup(async () => {
-    stubRestApi('getRepoBranches').callsFake(input => {
+    stubRestApi('getRepoBranches').callsFake((input: string) => {
       if (input.startsWith('test')) {
         return Promise.resolve([
           {
@@ -105,7 +105,12 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <gr-dialog confirm-label="Cherry Pick" disabled="" role="dialog">
+        <gr-dialog
+          confirm-label="Cherry Pick"
+          disabled=""
+          role="dialog"
+          title="Disabled because no branch is selected"
+        >
           <div class="header title" slot="header">
             Cherry Pick Change to Another Branch
           </div>
@@ -122,23 +127,23 @@
               <label for="baseInput">
                 Provide base commit sha1 for cherry-pick
               </label>
-              <iron-input>
-                <input
-                  id="baseCommitInput"
-                  is="iron-input"
-                  maxlength="40"
-                  placeholder="(optional)"
-                />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="baseCommitInput"
+                inputmode=""
+                maxlength="40"
+                placeholder="(optional)"
+                type="text"
+              >
+              </md-outlined-text-field>
               <label for="messageInput"> Cherry Pick Commit Message </label>
-              <iron-autogrow-textarea
-                aria-disabled="false"
+              <gr-autogrow-textarea
                 autocomplete="on"
                 class="message"
                 id="messageInput"
-                rows="4"
               >
-              </iron-autogrow-textarea>
+              </gr-autogrow-textarea>
               <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
             </gr-endpoint-decorator>
           </div>
@@ -231,38 +236,33 @@
       const executeChangeActionStub = stubRestApi(
         'executeChangeAction'
       ).resolves(new Response());
-      const checkboxes = queryAll<HTMLInputElement>(
-        element,
-        'input[type="checkbox"]'
-      );
+      const checkboxes = queryAll<HTMLInputElement>(element, 'md-checkbox');
       assert.equal(checkboxes.length, 2);
       assert.isTrue(checkboxes[0].checked);
       checkboxes[0].click();
+      assert.isTrue(checkboxes[1].checked);
       queryAndAssert<GrDialog>(element, 'gr-dialog').confirmButton!.click();
       await element.updateComplete;
       assert.equal(executeChangeActionStub.callCount, 1);
       assert.isTrue(duplicateChangesStub.called);
     });
 
-    test('deselecting all change shows error message', async () => {
+    test('deselecting all change disables the confirm button', async () => {
       element.branch = 'master' as BranchName;
       await element.updateComplete;
-      const executeChangeActionStub = stubRestApi(
-        'executeChangeAction'
-      ).resolves(new Response());
-      const checkboxes = queryAll<HTMLInputElement>(
-        element,
-        'input[type="checkbox"]'
-      );
+      const checkboxes = queryAll<HTMLInputElement>(element, 'md-checkbox');
       assert.equal(checkboxes.length, 2);
+      assert.equal(element.cherryPickType, CHERRY_PICK_TYPES.TOPIC);
       checkboxes[0].click();
-      checkboxes[1].click();
-      queryAndAssert<GrDialog>(element, 'gr-dialog').confirmButton!.click();
       await element.updateComplete;
-      assert.equal(executeChangeActionStub.callCount, 0);
+      checkboxes[1].click();
+      await element.updateComplete;
+      assert.isTrue(
+        queryAndAssert<GrDialog>(element, 'gr-dialog').confirmButton?.disabled
+      );
       assert.equal(
-        queryAndAssert<HTMLElement>(element, '.error-message').innerText,
-        'No change selected'
+        queryAndAssert<GrDialog>(element, 'gr-dialog').title,
+        'Disabled because no changes are selected'
       );
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index a9bb86e..8bdb2af 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -10,7 +10,7 @@
 import {getAppContext} from '../../../services/app-context';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-dialog/gr-dialog';
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import {Key, Modifier} from '../../../utils/dom-util';
 import {ValueChangedEvent} from '../../../types/events';
 import {ShortcutController} from '../../lit/shortcut-controller';
@@ -18,6 +18,7 @@
 import {fireNoBubble} from '../../../utils/event-util';
 import {formStyles} from '../../../styles/form-styles';
 import {branchName} from '../../../utils/patch-set-util';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 
 const SUGGESTIONS_LIMIT = 15;
 
@@ -103,6 +104,13 @@
         .warning {
           color: var(--error-text-color);
         }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
+        label[for='branchInput'],
+        label[for='messageInput'] {
+          margin-top: var(--spacing-m);
+        }
       `,
     ];
   }
@@ -123,6 +131,7 @@
           <gr-autocomplete
             id="branchInput"
             .text=${this.branch}
+            .showBlueFocusBorder=${true}
             @text-changed=${(e: ValueChangedEvent) =>
               (this.branch = e.detail.value as BranchName)}
             .query=${(text: string) => this.getProjectBranchesSuggestions(text)}
@@ -130,14 +139,18 @@
           >
           </gr-autocomplete>
           <label for="messageInput"> Move Change Message </label>
-          <iron-autogrow-textarea
+          <gr-autogrow-textarea
             id="messageInput"
             class="message"
             autocomplete="on"
             .rows=${4}
             .maxRows=${15}
-            .bindValue=${this.message}
-          ></iron-autogrow-textarea>
+            .value=${this.message}
+            @input=${(e: InputEvent) => {
+              const value = (e.target as GrAutogrowTextarea).value ?? '';
+              this.message = value;
+            }}
+          ></gr-autogrow-textarea>
         </div>
       </gr-dialog>
     `;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
index aca7e0ddc..3fe2e0b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
@@ -8,7 +8,7 @@
 import {GrConfirmMoveDialog} from './gr-confirm-move-dialog';
 import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
 import {BranchName, GitRef, RepoName} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 
 suite('gr-confirm-move-dialog tests', () => {
@@ -49,12 +49,11 @@
             <gr-autocomplete id="branchInput" placeholder="Destination branch">
             </gr-autocomplete>
             <label for="messageInput"> Move Change Message </label>
-            <iron-autogrow-textarea
-              aria-disabled="false"
+            <gr-autogrow-textarea
               id="messageInput"
               class="message"
               autocomplete="on"
-            ></iron-autogrow-textarea>
+            ></gr-autogrow-textarea>
           </div>
         </gr-dialog>
       `
@@ -75,6 +74,7 @@
       element,
       'gr-autocomplete'
     );
+    autoComplete.debounceWait = 10;
     const branches = await autoComplete.query!('nonexistent');
     assert.equal(branches.length, 0);
   });
@@ -84,6 +84,7 @@
       element,
       'gr-autocomplete'
     );
+    autoComplete.debounceWait = 10;
     const branches = await autoComplete.query!('test-branch');
     assert.equal(branches.length, 1);
     assert.equal(branches[0].name, 'test-branch');
@@ -94,6 +95,7 @@
       element,
       'gr-autocomplete'
     );
+    autoComplete.debounceWait = 10;
     const branches = await autoComplete.query!('');
     assert.equal(branches.length, 0);
   });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index c40cd1e..c9fd772 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../shared/gr-account-chip/gr-account-chip';
-import {css, html, LitElement, PropertyValues} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {when} from 'lit/directives/when.js';
 import {
@@ -13,22 +13,16 @@
   BranchName,
   ChangeActionDialog,
   EmailInfo,
-  NumericChangeId,
   GitPersonInfo,
+  NumericChangeId,
   ValidationOptionsInfo,
 } from '../../../types/common';
 import '../../shared/gr-dialog/gr-dialog';
-import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../gr-validation-options/gr-validation-options';
-import {
-  AutocompleteQuery,
-  AutocompleteSuggestion,
-  GrAutocomplete,
-} from '../../shared/gr-autocomplete/gr-autocomplete';
+import '../gr-change-autocomplete/gr-change-autocomplete';
 import {getAppContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {ValueChangedEvent} from '../../../types/events';
-import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {fireNoBubbleNoCompose} from '../../../utils/event-util';
 import {resolve} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
@@ -37,11 +31,12 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {formStyles} from '../../../styles/form-styles';
 import {GrValidationOptions} from '../gr-validation-options/gr-validation-options';
-
-export interface RebaseChange {
-  name: string;
-  value: NumericChangeId;
-}
+import {GrChangeAutocomplete} from '../gr-change-autocomplete/gr-change-autocomplete';
+import '@material/web/radio/radio';
+import {MdRadio} from '@material/web/radio/radio';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
+import '@material/web/checkbox/checkbox';
 
 export interface ConfirmRebaseEventDetail {
   base: string | null;
@@ -78,9 +73,15 @@
   disableActions = false;
 
   @state()
+  private loading = false;
+
+  @state()
   changeNum?: NumericChangeId;
 
   @state()
+  project?: string;
+
+  @state()
   hasParent?: boolean;
 
   @state()
@@ -90,12 +91,6 @@
   shouldRebaseChain = false;
 
   @state()
-  private query: AutocompleteQuery;
-
-  @state()
-  recentChanges?: RebaseChange[];
-
-  @state()
   allowConflicts = false;
 
   @state()
@@ -111,13 +106,13 @@
   committerEmailDropdownItems: EmailInfo[] = [];
 
   @query('#rebaseOnParentInput')
-  private rebaseOnParentInput?: HTMLInputElement;
+  private rebaseOnParentInput?: MdRadio;
 
   @query('#rebaseOnTipInput')
-  private rebaseOnTipInput?: HTMLInputElement;
+  private rebaseOnTipInput?: MdRadio;
 
   @query('#rebaseOnOtherInput')
-  rebaseOnOtherInput?: HTMLInputElement;
+  rebaseOnOtherInput?: MdRadio;
 
   @query('#rebaseAllowConflicts')
   private rebaseAllowConflicts?: HTMLInputElement;
@@ -125,8 +120,8 @@
   @query('#rebaseChain')
   private rebaseChain?: HTMLInputElement;
 
-  @query('#parentInput')
-  parentInput!: GrAutocomplete;
+  @query('gr-change-autocomplete')
+  changeAutocomplete!: GrChangeAutocomplete;
 
   @query('gr-validation-options')
   private validationOptionsEl?: GrValidationOptions;
@@ -156,7 +151,6 @@
 
   constructor() {
     super();
-    this.query = input => this.getChangeSuggestions(input);
     subscribe(
       this,
       () => this.getUserModel().account$,
@@ -174,6 +168,11 @@
     );
     subscribe(
       this,
+      () => this.getChangeModel().repo$,
+      x => (this.project = x)
+    );
+    subscribe(
+      this,
       () => this.getRelatedChangesModel().hasParent$,
       x => (this.hasParent = x)
     );
@@ -189,19 +188,12 @@
     this.loadCommitterEmailDropdownItems();
   }
 
-  override willUpdate(changedProperties: PropertyValues): void {
-    if (
-      changedProperties.has('rebaseOnCurrent') ||
-      changedProperties.has('hasParent')
-    ) {
-      this.updateSelectedOption();
-    }
-  }
-
   static override get styles() {
     return [
       formStyles,
       sharedStyles,
+      materialStyles,
+      spinnerStyles,
       css`
         :host {
           display: block;
@@ -217,16 +209,23 @@
         .message {
           font-style: italic;
         }
+        .parentRevisionContainer {
+          margin-top: var(--spacing-m);
+        }
         .parentRevisionContainer label,
         .parentRevisionContainer input[type='text'] {
           display: block;
           width: 100%;
         }
-        .rebaseCheckbox {
-          margin-top: var(--spacing-m);
+        div.rebaseOption,
+        div.checkbox-container {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-m);
+          margin-top: var(--spacing-xl);
         }
-        .rebaseOption {
-          margin: var(--spacing-m) 0;
+        .rebaseCheckbox {
+          margin-top: var(--spacing-xl);
         }
         .rebaseOnBehalfMsg {
           margin-top: var(--spacing-m);
@@ -234,6 +233,26 @@
         .rebaseWithCommitterEmail {
           margin-top: var(--spacing-m);
         }
+        .main {
+          margin-top: var(--spacing-xl);
+          margin-bottom: var(--spacing-xl);
+        }
+        .loadingSpin {
+          width: calc(var(--line-height-normal) - 2px);
+          height: calc(var(--line-height-normal) - 2px);
+          display: inline-block;
+          vertical-align: top;
+          position: relative;
+          top: 1px;
+        }
+        .zeroState {
+          color: var(--deemphasized-text-color);
+        }
+        @media screen and (max-width: 50em) {
+          #confirmDialog {
+            height: 90vh;
+          }
+        }
       `,
     ];
   }
@@ -248,145 +267,158 @@
         @cancel=${this.handleCancelTap}
       >
         <div class="header" slot="header">Confirm rebase</div>
-        <div class="main" slot="main">
-          <div
-            id="rebaseOnParent"
-            class="rebaseOption"
-            ?hidden=${!this.displayParentOption()}
+        <div class="main" slot="main">${this.renderRebaseDialog()}</div>
+      </gr-dialog>
+    `;
+  }
+
+  private renderRebaseDialog() {
+    return html` <div
+        id="rebaseOnParent"
+        class="rebaseOption loading"
+        ?hidden=${!this.displayParentOption()}
+      >
+        <md-radio
+          id="rebaseOnParentInput"
+          name="rebaseOptions"
+          ?checked=${this.displayParentOption()}
+        >
+        </md-radio>
+        <label id="rebaseOnParentLabel" for="rebaseOnParentInput">
+          Rebase on parent change
+        </label>
+      </div>
+      <div class="message" ?hidden=${this.hasParent !== undefined}>
+        Still loading parent information ...
+      </div>
+      <div
+        id="parentUpToDateMsg"
+        class="message"
+        ?hidden=${!this.displayParentUpToDateMsg()}
+      >
+        This change is up to date with its parent.
+      </div>
+      <div
+        id="rebaseOnTip"
+        class="rebaseOption"
+        ?hidden=${!this.displayTipOption()}
+      >
+        <md-radio
+          id="rebaseOnTipInput"
+          name="rebaseOptions"
+          ?checked=${!this.displayParentOption() && this.displayTipOption()}
+          ?disabled=${!this.displayTipOption()}
+        >
+        </md-radio>
+        <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
+          Rebase on top of the ${this.branch} branch<span
+            ?hidden=${!this.hasParent || this.shouldRebaseChain}
           >
-            <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" />
-            <label id="rebaseOnParentLabel" for="rebaseOnParentInput">
-              Rebase on parent change
-            </label>
-          </div>
-          <div class="message" ?hidden=${this.hasParent !== undefined}>
-            Still loading parent information ...
-          </div>
-          <div
-            id="parentUpToDateMsg"
-            class="message"
-            ?hidden=${!this.displayParentUpToDateMsg()}
-          >
-            This change is up to date with its parent.
-          </div>
-          <div
-            id="rebaseOnTip"
-            class="rebaseOption"
-            ?hidden=${!this.displayTipOption()}
-          >
-            <input
-              id="rebaseOnTipInput"
-              name="rebaseOptions"
-              type="radio"
-              ?disabled=${!this.displayTipOption()}
-            />
-            <label id="rebaseOnTipLabel" for="rebaseOnTipInput">
-              Rebase on top of the ${this.branch} branch<span
-                ?hidden=${!this.hasParent || this.shouldRebaseChain}
-              >
-                (breaks relation chain)
-              </span>
-            </label>
-          </div>
-          <div
-            id="tipUpToDateMsg"
-            class="message"
-            ?hidden=${this.displayTipOption()}
-          >
-            Change is up to date with the target branch already (${this.branch})
-          </div>
-          <div id="rebaseOnOther" class="rebaseOption">
-            <input
-              id="rebaseOnOtherInput"
-              name="rebaseOptions"
-              type="radio"
-              @click=${this.handleRebaseOnOther}
-            />
-            <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
-              Rebase on a specific change, ref, or commit
-              <span ?hidden=${!this.hasParent || this.shouldRebaseChain}>
-                (breaks relation chain)
-              </span>
-            </label>
-          </div>
-          <div class="parentRevisionContainer">
-            <gr-autocomplete
-              id="parentInput"
-              .query=${this.query}
-              .text=${this.text}
-              @text-changed=${(e: ValueChangedEvent) =>
-                (this.text = e.detail.value)}
-              @click=${this.handleEnterChangeNumberClick}
-              allow-non-suggested-values
-              placeholder="Change number, ref, or commit hash"
-            >
-            </gr-autocomplete>
-          </div>
-          <div class="rebaseCheckbox">
-            <input
-              id="rebaseAllowConflicts"
-              type="checkbox"
-              @change=${() => {
-                this.allowConflicts = !!this.rebaseAllowConflicts?.checked;
-                this.loadCommitterEmailDropdownItems();
-              }}
-            />
-            <label for="rebaseAllowConflicts"
-              >Allow rebase with conflicts</label
-            >
-            <gr-validation-options
-              .validationOptions=${this.validationOptions}
-            ></gr-validation-options>
-          </div>
-          ${when(
-            !this.isCurrentUserEqualToLatestUploader() && this.allowConflicts,
-            () =>
-              html`<span class="message"
-                >Rebase cannot be done on behalf of the uploader when allowing
-                conflicts.</span
-              >`
-          )}
-          ${when(
-            this.hasParent,
-            () =>
-              html`<div class="rebaseCheckbox">
-                <input
-                  id="rebaseChain"
-                  type="checkbox"
-                  @change=${() => {
-                    this.shouldRebaseChain = !!this.rebaseChain?.checked;
-                    if (this.shouldRebaseChain) {
-                      this.selectedEmailForRebase = undefined;
-                    }
-                  }}
-                />
-                <label for="rebaseChain">Rebase all ancestors</label>
-              </div>`
-          )}
-          ${when(
-            !this.isCurrentUserEqualToLatestUploader(),
-            () => html`<div class="rebaseOnBehalfMsg">Rebase will be done on behalf of${
-              !this.allowConflicts ? ' the uploader:' : ''
-            } <gr-account-chip
+            (breaks relation chain)
+          </span>
+        </label>
+      </div>
+      <div
+        id="tipUpToDateMsg"
+        class="message"
+        ?hidden=${this.displayTipOption()}
+      >
+        Change is up to date with the target branch already (${this.branch})
+      </div>
+      <div id="rebaseOnOther" class="rebaseOption">
+        <md-radio
+          id="rebaseOnOtherInput"
+          name="rebaseOptions"
+          ?checked=${!this.displayParentOption() && !this.displayTipOption()}
+          @click=${this.handleRebaseOnOther}
+        >
+        </md-radio>
+        <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
+          Rebase on a specific change, ref, or commit
+          <span ?hidden=${!this.hasParent || this.shouldRebaseChain}>
+            (breaks relation chain)
+          </span>
+        </label>
+      </div>
+      <div class="parentRevisionContainer">
+        <gr-change-autocomplete
+          .text=${this.text}
+          .excludeChangeNum=${this.changeNum}
+          .projectQuery="project:${this.project}"
+          @text-changed=${(e: ValueChangedEvent) =>
+            (this.text = e.detail.value)}
+          @click=${this.handleEnterChangeNumberClick}
+        >
+        </gr-change-autocomplete>
+      </div>
+      ${when(
+        this.loading,
+        () =>
+          html`<span class="loadingSpin"></span
+            ><span class="zeroState"> loading recent changes</span>`
+      )}
+      <div class="rebaseCheckbox">
+        <div class="checkbox-container">
+          <md-checkbox
+            id="rebaseAllowConflicts"
+            @change=${() => {
+              this.allowConflicts = !!this.rebaseAllowConflicts?.checked;
+              this.loadCommitterEmailDropdownItems();
+            }}
+          ></md-checkbox>
+          <label for="rebaseAllowConflicts">Allow rebase with conflicts</label>
+        </div>
+        ${when(
+          !this.isCurrentUserEqualToLatestUploader() && this.allowConflicts,
+          () =>
+            html`<span class="message"
+              >Rebase cannot be done on behalf of the uploader when allowing
+              conflicts.</span
+            >`
+        )}
+        <gr-validation-options
+          .validationOptions=${this.validationOptions}
+        ></gr-validation-options>
+      </div>
+      ${when(
+        this.hasParent,
+        () =>
+          html`<div class="rebaseCheckbox">
+            <div class="checkbox-container">
+              <md-checkbox
+                id="rebaseChain"
+                @change=${() => {
+                  this.shouldRebaseChain = !!this.rebaseChain?.checked;
+                  if (this.shouldRebaseChain) {
+                    this.selectedEmailForRebase = undefined;
+                  }
+                }}
+              ></md-checkbox>
+              <label for="rebaseChain">Rebase all ancestors</label>
+            </div>
+          </div>`
+      )}
+      ${when(
+        !this.isCurrentUserEqualToLatestUploader(),
+        () => html`<div class="rebaseOnBehalfMsg">Rebase will be done on behalf of${
+          !this.allowConflicts ? ' the uploader:' : ''
+        } <gr-account-chip
                 .account=${this.allowConflicts ? this.account : this.uploader}
               ></gr-account-chip
               ><span></div>`
-          )}
-          ${when(
-            this.canShowCommitterEmailDropdown(),
-            () => html`<div class="rebaseWithCommitterEmail"
+      )}
+      ${when(
+        this.canShowCommitterEmailDropdown(),
+        () => html`<div class="rebaseWithCommitterEmail"
             >Rebase with committer email
                 <gr-dropdown-list
                     .items=${this.getCommitterEmailDropdownItems()}
-                    .value=${this.selectedEmailForRebase}
+                    .value=${this.selectedEmailForRebase ?? ''}
                     @value-change=${this.handleCommitterEmailDropdownItems}
                 >
                 </gr-dropdown-list>
                 <span></div>`
-          )}
-        </div>
-      </gr-dialog>
-    `;
+      )}`;
   }
 
   getValidationOptions() {
@@ -400,8 +432,17 @@
   // in case there are new/updated changes in the generic query since the
   // last time it was run.
   initiateFetchInfo() {
-    this.fetchRecentChanges();
     this.fetchValidationOptions();
+    this.fetchRecentChanges();
+  }
+
+  async fetchRecentChanges() {
+    this.loading = true;
+    try {
+      await this.changeAutocomplete.fetchRecentChanges();
+    } finally {
+      this.loading = false;
+    }
   }
 
   async fetchValidationOptions() {
@@ -414,47 +455,11 @@
     );
   }
 
-  fetchRecentChanges() {
-    return this.restApiService
-      .getChanges(
-        undefined,
-        'is:open -age:90d',
-        /* offset=*/ undefined,
-        /* options=*/ undefined,
-        throwingErrorCallback
-      )
-      .then(response => {
-        if (!response) return [];
-        const changes: RebaseChange[] = [];
-        for (const change of response) {
-          changes.push({
-            name: `${change._number}: ${change.subject}`,
-            value: change._number,
-          });
-        }
-        this.recentChanges = changes;
-        return this.recentChanges;
-      });
-  }
-
   isCurrentUserEqualToLatestUploader() {
     if (!this.account || !this.uploader) return true;
     return this.account._account_id === this.uploader._account_id;
   }
 
-  getRecentChanges() {
-    if (this.recentChanges) {
-      return Promise.resolve(this.recentChanges);
-    }
-    return this.fetchRecentChanges();
-  }
-
-  private getChangeSuggestions(input: string) {
-    return this.getRecentChanges().then(changes =>
-      this.filterChanges(input, changes)
-    );
-  }
-
   private setPreferredAsSelectedEmailForRebase(emails: EmailInfo[]) {
     emails.forEach(e => {
       if (e.preferred) {
@@ -519,23 +524,6 @@
     this.selectedEmailForRebase = e.detail.value;
   }
 
-  filterChanges(
-    input: string,
-    changes: RebaseChange[]
-  ): AutocompleteSuggestion[] {
-    return changes
-      .filter(
-        change => change.name.includes(input) && change.value !== this.changeNum
-      )
-      .map(
-        change =>
-          ({
-            name: change.name,
-            value: `${change.value}`,
-          } as AutocompleteSuggestion)
-      );
-  }
-
   private displayParentOption() {
     return this.hasParent && this.rebaseOnCurrent;
   }
@@ -544,8 +532,8 @@
     return this.hasParent && !this.rebaseOnCurrent;
   }
 
-  private displayTipOption() {
-    return this.rebaseOnCurrent || this.hasParent;
+  private displayTipOption(): boolean {
+    return !!this.rebaseOnCurrent || !!this.hasParent;
   }
 
   /**
@@ -599,31 +587,12 @@
   }
 
   private handleRebaseOnOther() {
-    this.parentInput.focus();
+    this.changeAutocomplete.focus();
   }
 
   private handleEnterChangeNumberClick() {
     if (this.rebaseOnOtherInput) this.rebaseOnOtherInput.checked = true;
   }
-
-  /**
-   * Sets the default radio button based on the state of the app and
-   * the corresponding value to be submitted.
-   */
-  private updateSelectedOption() {
-    const {rebaseOnCurrent, hasParent} = this;
-    if (rebaseOnCurrent === undefined || hasParent === undefined) {
-      return;
-    }
-
-    if (this.displayParentOption()) {
-      if (this.rebaseOnParentInput) this.rebaseOnParentInput.checked = true;
-    } else if (this.displayTipOption()) {
-      if (this.rebaseOnTipInput) this.rebaseOnTipInput.checked = true;
-    } else {
-      if (this.rebaseOnOtherInput) this.rebaseOnOtherInput.checked = true;
-    }
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index d81b32a..30cf475 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -3,37 +3,34 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-confirm-rebase-dialog';
-import {GrConfirmRebaseDialog, RebaseChange} from './gr-confirm-rebase-dialog';
+import {GrConfirmRebaseDialog} from './gr-confirm-rebase-dialog';
+import {query, queryAndAssert} from '../../../test/test-utils';
 import {
-  pressKey,
-  query,
-  queryAndAssert,
-  stubRestApi,
-  waitUntil,
-} from '../../../test/test-utils';
-import {
-  NumericChangeId,
-  BranchName,
-  Timestamp,
   AccountId,
+  BranchName,
   EmailAddress,
+  EmailInfo,
+  Timestamp,
 } from '../../../types/common';
 import {
   createAccountWithEmail,
   createChangeViewChange,
 } from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
-import {Key} from '../../../utils/dom-util';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {testResolver} from '../../../test/common-test-setup';
 import {userModelToken} from '../../../models/user/user-model';
 import {changeModelToken} from '../../../models/change/change-model';
-import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
+import {GrChangeAutocomplete} from '../gr-change-autocomplete/gr-change-autocomplete';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {LoadingStatus} from '../../../types/types';
-import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
+import {
+  DropdownItem,
+  GrDropdownList,
+} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 
 suite('gr-confirm-rebase-dialog tests', () => {
   let element: GrConfirmRebaseDialog;
@@ -62,8 +59,13 @@
       >
         <div class="header" slot="header">Confirm rebase</div>
         <div class="main" slot="main">
-          <div class="rebaseOption" hidden="" id="rebaseOnParent">
-            <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" />
+          <div class="loading rebaseOption" hidden="" id="rebaseOnParent">
+            <md-radio
+              id="rebaseOnParentInput"
+              name="rebaseOptions"
+              tabindex="-1"
+            >
+            </md-radio>
             <label for="rebaseOnParentInput" id="rebaseOnParentLabel">
               Rebase on parent change
             </label>
@@ -75,12 +77,13 @@
             This change is up to date with its parent.
           </div>
           <div class="rebaseOption" hidden="" id="rebaseOnTip">
-            <input
+            <md-radio
               disabled=""
               id="rebaseOnTipInput"
               name="rebaseOptions"
-              type="radio"
-            />
+              tabindex="-1"
+            >
+            </md-radio>
             <label for="rebaseOnTipInput" id="rebaseOnTipLabel">
               Rebase on top of the test branch
               <span hidden=""> (breaks relation chain) </span>
@@ -90,25 +93,28 @@
             Change is up to date with the target branch already (test)
           </div>
           <div class="rebaseOption" id="rebaseOnOther">
-            <input id="rebaseOnOtherInput" name="rebaseOptions" type="radio" />
+            <md-radio
+              checked=""
+              id="rebaseOnOtherInput"
+              name="rebaseOptions"
+              tabindex="0"
+            >
+            </md-radio>
             <label for="rebaseOnOtherInput" id="rebaseOnOtherLabel">
               Rebase on a specific change, ref, or commit
               <span hidden=""> (breaks relation chain) </span>
             </label>
           </div>
           <div class="parentRevisionContainer">
-            <gr-autocomplete
-              allow-non-suggested-values=""
-              id="parentInput"
-              placeholder="Change number, ref, or commit hash"
-            >
-            </gr-autocomplete>
+            <gr-change-autocomplete> </gr-change-autocomplete>
           </div>
           <div class="rebaseCheckbox">
-            <input id="rebaseAllowConflicts" type="checkbox" />
-            <label for="rebaseAllowConflicts">
-              Allow rebase with conflicts
-            </label>
+            <div class="checkbox-container">
+              <md-checkbox id="rebaseAllowConflicts"> </md-checkbox>
+              <label for="rebaseAllowConflicts">
+                Allow rebase with conflicts
+              </label>
+            </div>
             <gr-validation-options> </gr-validation-options>
           </div>
         </div>
@@ -251,8 +257,8 @@
         'gr-dropdown-list'
       );
       assert.deepStrictEqual(
-        dropdownList.items!.map(e => e.value),
-        element.committerEmailDropdownItems.map(e => e.email)
+        dropdownList.items!.map((e: DropdownItem) => e.value),
+        element.committerEmailDropdownItems.map((e: EmailInfo) => e.email)
       );
     });
 
@@ -286,8 +292,8 @@
         'gr-dropdown-list'
       );
       assert.deepStrictEqual(
-        dropdownList.items!.map(e => e.value),
-        element.committerEmailDropdownItems.map(e => e.email)
+        dropdownList.items!.map((e: DropdownItem) => e.value),
+        element.committerEmailDropdownItems.map((e: EmailInfo) => e.email)
       );
     });
   });
@@ -427,7 +433,7 @@
     );
     assert.deepEqual((fireStub.lastCall.args[0] as CustomEvent).detail, {
       allowConflicts: false,
-      base: '123',
+      base: '',
       rebaseChain: true,
       onBehalfOfUploader: true,
       committerEmail: null,
@@ -457,8 +463,20 @@
     assert.equal(element.text, '');
   });
 
-  test('_getSelectedBase', async () => {
-    element.text = '5fab321c';
+  test('getSelectedBase', async () => {
+    const autocomplete = queryAndAssert<GrChangeAutocomplete>(
+      element,
+      'gr-change-autocomplete'
+    );
+    const innerAutocomplete = queryAndAssert<GrAutocomplete>(
+      autocomplete,
+      'gr-autocomplete'
+    );
+
+    innerAutocomplete.dispatchEvent(
+      new CustomEvent('text-changed', {detail: {value: '5fab321c'}})
+    );
+    await innerAutocomplete.updateComplete;
     await element.updateComplete;
 
     queryAndAssert<HTMLInputElement>(element, '#rebaseOnParentInput').checked =
@@ -471,96 +489,6 @@
     assert.equal(element.getSelectedBase(), '');
     queryAndAssert<HTMLInputElement>(element, '#rebaseOnTipInput').checked =
       false;
-    assert.equal(element.getSelectedBase(), element.text);
-    element.text = '101: Test';
-    await element.updateComplete;
-
-    assert.equal(element.getSelectedBase(), '101');
-  });
-
-  suite('parent suggestions', () => {
-    let recentChanges: RebaseChange[];
-    let getChangesStub: sinon.SinonStub;
-    setup(() => {
-      recentChanges = [
-        {
-          name: '123: my first awesome change',
-          value: 123 as NumericChangeId,
-        },
-        {
-          name: '124: my second awesome change',
-          value: 124 as NumericChangeId,
-        },
-        {
-          name: '245: my third awesome change',
-          value: 245 as NumericChangeId,
-        },
-      ];
-
-      getChangesStub = stubRestApi('getChanges').returns(
-        Promise.resolve([
-          {
-            ...createChangeViewChange(),
-            _number: 123 as NumericChangeId,
-            subject: 'my first awesome change',
-          },
-          {
-            ...createChangeViewChange(),
-            _number: 124 as NumericChangeId,
-            subject: 'my second awesome change',
-          },
-          {
-            ...createChangeViewChange(),
-            _number: 245 as NumericChangeId,
-            subject: 'my third awesome change',
-          },
-        ])
-      );
-    });
-
-    test('_getRecentChanges', async () => {
-      const recentChangesSpy = sinon.spy(element, 'getRecentChanges');
-      await element.getRecentChanges();
-      await element.updateComplete;
-
-      assert.deepEqual(element.recentChanges, recentChanges);
-      assert.equal(getChangesStub.callCount, 1);
-
-      // When called a second time, should not re-request recent changes.
-      await element.getRecentChanges();
-      await element.updateComplete;
-
-      assert.equal(recentChangesSpy.callCount, 2);
-      assert.equal(getChangesStub.callCount, 1);
-    });
-
-    test('_filterChanges', async () => {
-      assert.equal(element.filterChanges('123', recentChanges).length, 1);
-      assert.equal(element.filterChanges('12', recentChanges).length, 2);
-      assert.equal(element.filterChanges('awesome', recentChanges).length, 3);
-      assert.equal(element.filterChanges('third', recentChanges).length, 1);
-
-      element.changeNum = 123 as NumericChangeId;
-      await element.updateComplete;
-
-      assert.equal(element.filterChanges('123', recentChanges).length, 0);
-      assert.equal(element.filterChanges('124', recentChanges).length, 1);
-      assert.equal(element.filterChanges('awesome', recentChanges).length, 2);
-    });
-
-    test('input text change triggers function', async () => {
-      const recentChangesSpy = sinon.spy(element, 'getRecentChanges');
-      pressKey(
-        queryAndAssert(queryAndAssert(element, '#parentInput'), '#input'),
-        Key.ENTER
-      );
-      await element.updateComplete;
-      element.text = '1';
-
-      await waitUntil(() => recentChangesSpy.calledOnce);
-      element.text = '12';
-
-      await waitUntil(() => recentChangesSpy.calledTwice);
-    });
+    assert.equal(element.getSelectedBase(), '5fab321c');
   });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index 3d69471..52ae1c2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -6,8 +6,8 @@
 import '../../shared/gr-dialog/gr-dialog';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../gr-validation-options/gr-validation-options';
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {LitElement, html, css, nothing} from 'lit';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {
   ChangeActionDialog,
@@ -17,16 +17,18 @@
 } from '../../../types/common';
 import {fire, fireAlert} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {BindValueChangeEvent} from '../../../types/events';
 import {resolve} from '../../../models/dependency';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {createSearchUrl} from '../../../models/views/search';
 import {ParsedChangeInfo} from '../../../types/types';
 import {formStyles} from '../../../styles/form-styles';
 import {GrValidationOptions} from '../gr-validation-options/gr-validation-options';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import '@material/web/radio/radio';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.';
-const INSERT_REASON_STRING = '<INSERT REASONING HERE>';
+const SPECIFY_REASON_STRING = '<MUST SPECIFY REASON HERE>';
 
 // TODO(dhruvsri): clean up repeated definitions after moving to js modules
 export enum RevertType {
@@ -85,6 +87,7 @@
     return [
       formStyles,
       sharedStyles,
+      materialStyles,
       css`
         :host {
           display: block;
@@ -101,16 +104,21 @@
         .revertSubmissionLayout {
           display: flex;
           align-items: center;
+          margin-top: var(--spacing-l);
+          margin-bottom: var(--spacing-m);
         }
         .label {
           margin-left: var(--spacing-m);
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           font-family: var(--monospace-font-family);
           font-size: var(--font-size-mono);
           line-height: var(--line-height-mono);
           width: 73ch; /* Add a char to account for the border. */
         }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
         .error {
           color: var(--error-text-color);
           margin-bottom: var(--spacing-m);
@@ -118,6 +126,9 @@
         label[for='messageInput'] {
           margin-top: var(--spacing-m);
         }
+        gr-validation-options {
+          margin-bottom: var(--spacing-m);
+        }
       `,
     ];
   }
@@ -137,13 +148,13 @@
           ${this.showRevertSubmission
             ? html`
                 <div class="revertSubmissionLayout">
-                  <input
-                    name="revertOptions"
-                    type="radio"
+                  <md-radio
                     id="revertSingleChange"
-                    @change=${() => this.handleRevertSingleChangeClicked()}
+                    name="revertOptions"
                     ?checked=${this.computeIfSingleRevert()}
-                  />
+                    @change=${() => this.handleRevertSingleChangeClicked()}
+                  >
+                  </md-radio>
                   <label
                     for="revertSingleChange"
                     class="label revertSingleChange"
@@ -152,13 +163,13 @@
                   </label>
                 </div>
                 <div class="revertSubmissionLayout">
-                  <input
-                    name="revertOptions"
-                    type="radio"
+                  <md-radio
                     id="revertSubmission"
+                    name="revertOptions"
+                    ?checked=${this.computeIfRevertSubmission()}
                     @change=${() => this.handleRevertSubmissionClicked()}
-                    .checked=${this.computeIfRevertSubmission()}
-                  />
+                  >
+                  </md-radio>
                   <label for="revertSubmission" class="label revertSubmission">
                     Revert entire submission (${this.changesCount} Changes)
                   </label>
@@ -167,14 +178,17 @@
             : nothing}
           <gr-endpoint-decorator name="confirm-revert-change">
             <label for="messageInput"> Revert Commit Message </label>
-            <iron-autogrow-textarea
+            <gr-autogrow-textarea
               id="messageInput"
               class="message"
               .autocomplete=${'on'}
               .maxRows=${15}
-              .bindValue=${this.message}
-              @bind-value-changed=${this.handleBindValueChanged}
-            ></iron-autogrow-textarea>
+              .value=${this.message}
+              @input=${(e: InputEvent) => {
+                const value = (e.target as GrAutogrowTextarea).value ?? '';
+                this.message = value;
+              }}
+            ></gr-autogrow-textarea>
           </gr-endpoint-decorator>
           <gr-validation-options
             .validationOptions=${this.validationOptions}
@@ -230,16 +244,23 @@
     commitMessage: string,
     commitHash?: CommitId
   ) {
+    if (!commitHash) {
+      fireAlert(this, ERR_COMMIT_NOT_FOUND);
+      return;
+    }
+
     // Figure out what the revert title should be.
     const originalTitle = (commitMessage || '').split('\n')[0];
     let revertTitle = `Revert "${originalTitle}"`;
-    const match = originalTitle.match(/^Revert(?:\^([0-9]+))? "(.*)"$/);
-    if (match) {
+    const revertTitleRegex = originalTitle.match(
+      /^Revert(?:\^([0-9]+))? "(.*)"$/
+    );
+    if (revertTitleRegex) {
       let revertNum = 2;
-      if (match[1]) {
-        revertNum = Number(match[1]) + 1;
+      if (revertTitleRegex[1]) {
+        revertNum = Number(revertTitleRegex[1]) + 1;
       }
-      revertTitle = `Revert^${revertNum} "${match[2]}"`;
+      revertTitle = `Revert^${revertNum} "${revertTitleRegex[2]}"`;
     }
 
     if (!commitHash) {
@@ -250,7 +271,8 @@
 
     const message =
       `${revertTitle}\n\n${revertCommitText}\n\n` +
-      `Reason for revert: ${INSERT_REASON_STRING}\n`;
+      `Reason for revert: ${SPECIFY_REASON_STRING}\n`;
+
     // This is to give plugins a chance to update message
     this.message = this.modifyRevertMsg(change, commitMessage, message);
     this.revertType = RevertType.REVERT_SINGLE_CHANGE;
@@ -285,8 +307,7 @@
     const message =
       `Revert submission ${change.submission_id}` +
       '\n\n' +
-      'Reason for revert: <INSERT ' +
-      'REASONING HERE>\n\n' +
+      `Reason for revert: ${SPECIFY_REASON_STRING}\n\n` +
       'Reverted changes: ' +
       createSearchUrl({query: `submissionid:${change.submission_id}`}) +
       '\n';
@@ -301,10 +322,6 @@
     this.showRevertSubmission = true;
   }
 
-  private handleBindValueChanged(e: BindValueChangeEvent) {
-    this.message = e.detail.value ?? '';
-  }
-
   private handleRevertSingleChangeClicked() {
     this.showErrorMessage = false;
     if (this.message)
@@ -326,7 +343,7 @@
     e.stopPropagation();
     if (
       this.message === this.originalRevertMessages[this.revertType] ||
-      this.message.includes(INSERT_REASON_STRING)
+      this.message.includes(SPECIFY_REASON_STRING)
     ) {
       this.showErrorMessage = true;
       return;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
index f4abe0c..7077099 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {createParsedChange} from '../../../test/test-data-generators';
 import {ChangeSubmissionId, CommitId} from '../../../types/common';
@@ -32,11 +32,10 @@
             </div>
             <gr-endpoint-decorator name="confirm-revert-change">
               <label for="messageInput"> Revert Commit Message </label>
-              <iron-autogrow-textarea
+              <gr-autogrow-textarea
                 id="messageInput"
                 class="message"
-                aria-disabled="false"
-              ></iron-autogrow-textarea>
+              ></gr-autogrow-textarea>
             </gr-endpoint-decorator>
             <gr-validation-options></gr-validation-options>
           </div>
@@ -67,7 +66,7 @@
     const expected =
       'Revert "one line commit"\n\n' +
       'This reverts commit abcd123.\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
+      'Reason for revert: <MUST SPECIFY REASON HERE>\n';
     assert.equal(element.message, expected);
   });
 
@@ -81,21 +80,7 @@
     const expected =
       'Revert "many lines"\n\n' +
       'This reverts commit abcd123.\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
-    assert.equal(element.message, expected);
-  });
-
-  test('issue above change id', () => {
-    assert.isNotOk(element.message);
-    element.populateRevertSingleChangeMessage(
-      createParsedChange(),
-      'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
-      'abcd123' as CommitId
-    );
-    const expected =
-      'Revert "much lines"\n\n' +
-      'This reverts commit abcd123.\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
+      'Reason for revert: <MUST SPECIFY REASON HERE>\n';
     assert.equal(element.message, expected);
   });
 
@@ -109,7 +94,7 @@
     const expected =
       'Revert^2 "one line commit"\n\n' +
       'This reverts commit abcd123.\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
+      'Reason for revert: <MUST SPECIFY REASON HERE>\n';
     assert.equal(element.message, expected);
   });
 
@@ -123,7 +108,7 @@
     const expected =
       'Revert^3 "one line commit"\n\n' +
       'This reverts commit abcd123.\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n';
+      'Reason for revert: <MUST SPECIFY REASON HERE>\n';
     assert.equal(element.message, expected);
   });
 
@@ -140,7 +125,7 @@
 
     const expected =
       'Revert submission 5545\n\n' +
-      'Reason for revert: <INSERT REASONING HERE>\n\n' +
+      'Reason for revert: <MUST SPECIFY REASON HERE>\n\n' +
       'Reverted changes: /q/submissionid:5545\n';
     assert.equal(element.message, expected);
   });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 8dba1bf..c87b878 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -19,7 +19,7 @@
 import {pluralize} from '../../../utils/string-util';
 import {isUnresolved} from '../../../utils/comment-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {subscribe} from '../../lit/subscription-controller';
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
index 646aefe..d221e16 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
@@ -12,7 +12,7 @@
 import {EDIT} from '../../../types/common';
 import {GrConfirmSubmitDialog} from './gr-confirm-submit-dialog';
 import './gr-confirm-submit-dialog';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-confirm-submit-dialog tests', () => {
   let element: GrConfirmSubmitDialog;
diff --git a/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links.ts b/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links.ts
index bf56890..c9c30d5 100644
--- a/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links.ts
+++ b/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links.ts
@@ -4,22 +4,23 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import '@polymer/iron-dropdown/iron-dropdown';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {LitElement, html, css, nothing} from 'lit';
-import {Ref, createRef, ref} from 'lit/directives/ref.js';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {createRef, ref, Ref} from 'lit/directives/ref.js';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {strToClassName} from '../../../utils/dom-util';
-import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
-import {copyToClipbard, queryAndAssert} from '../../../utils/common-util';
-import {ValueChangedEvent} from '../../../types/events';
+import {copyToClipboard, queryAndAssert} from '../../../utils/common-util';
 import {formStyles} from '../../../styles/form-styles';
 import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import '@material/web/menu/menu';
+import {MdMenu} from '@material/web/menu/menu';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 export interface CopyLink {
   label: string;
   shortcut: string;
   value: string;
+  multiline?: boolean;
 }
 
 const AWAIT_MAX_ITERS = 10;
@@ -32,22 +33,35 @@
   @property({type: Array})
   copyLinks: CopyLink[] = [];
 
+  @property({type: String})
+  horizontalAlign: 'left' | 'right' = 'left';
+
+  @property({type: String})
+  shortcutPrefix = 'l - ';
+
+  @property({type: Number})
+  verticalOffset = 10;
+
   @state() isDropdownOpen = false;
 
-  @query('iron-dropdown') private dropdown?: IronDropdownElement;
+  // private but used in screenshot tests
+  @query('md-menu') dropdown?: MdMenu;
 
   static override get styles() {
     return [
       formStyles,
       css`
-        iron-dropdown {
-          box-shadow: var(--elevation-level-2);
-          width: min(90vw, 640px);
-          background-color: var(--dialog-background-color);
-          border-radius: var(--border-radius);
+        md-menu {
+          white-space: nowrap;
+          --md-menu-container-color: var(--dialog-background-color);
+          --md-menu-top-space: 0px;
+          --md-menu-bottom-space: 0px;
         }
-        [slot='dropdown-content'] {
+        .dropdown-content {
+          width: min(90vw, 640px);
           padding: var(--spacing-m) var(--spacing-l) var(--spacing-m);
+          box-shadow: var(--elevation-level-2);
+          border-radius: var(--border-radius);
         }
         .copy-link-row {
           margin-bottom: var(--spacing-m);
@@ -59,22 +73,65 @@
     ];
   }
 
+  override connectedCallback() {
+    super.connectedCallback();
+    if (this.isDropdownOpen) {
+      this.setUpGlobalEventListeners();
+    }
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('isDropdownOpen')) {
+      if (this.isDropdownOpen) {
+        this.setUpGlobalEventListeners();
+      } else {
+        this.cleanUpGlobalEventListeners();
+      }
+    }
+  }
+
+  private setUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.addEventListener('resize', this.onWindowResize, passiveOptions);
+    window.addEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private cleanUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.removeEventListener('resize', this.onWindowResize, passiveOptions);
+    window.removeEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private readonly onWindowResize = () => {
+    this.dropdown?.reposition();
+  };
+
   override render() {
     if (!this.copyLinks) return nothing;
-    return html`<iron-dropdown
-      .horizontalAlign=${'left'}
-      .verticalAlign=${'top'}
-      .verticalOffset=${20}
+    return html`<md-menu
+      default-focus="none"
+      tabindex="-1"
+      .menuCorner=${this.horizontalAlign === 'left'
+        ? 'start-start'
+        : 'end-start'}
+      ?quick=${true}
+      .yOffset=${this.verticalOffset}
+      @opened=${() => {
+        this.isDropdownOpen = true;
+      }}
+      @closed=${() => {
+        this.isDropdownOpen = false;
+      }}
       @keydown=${this.handleKeydown}
-      @opened-changed=${(e: ValueChangedEvent<boolean>) =>
-        (this.isDropdownOpen = e.detail.value)}
     >
       ${this.renderCopyLinks()}
-    </iron-dropdown>`;
+    </md-menu> `;
   }
 
   private renderCopyLinks() {
-    return html`<div slot="dropdown-content">
+    return html`<div class="dropdown-content">
       ${this.copyLinks?.map((link, index) =>
         this.renderCopyLinkRow(link, index)
       )}
@@ -82,15 +139,16 @@
   }
 
   private renderCopyLinkRow(copyLink: CopyLink, index?: number) {
-    const {label, shortcut, value} = copyLink;
+    const {label, shortcut, value, multiline} = copyLink;
     const id = `${strToClassName(label, '')}-field`;
     return html`<div class="copy-link-row">
       <gr-copy-clipboard
         text=${value}
         label=${label}
-        shortcut=${`l - ${shortcut}`}
+        shortcut=${`${this.shortcutPrefix}${shortcut}`}
         id=${`${id}-copy-clipboard`}
         nowrap
+        ?multiline=${!!multiline}
         ${index === 0 && ref(this.copyClipboardRef)}
       ></gr-copy-clipboard>
     </div>`;
@@ -99,11 +157,14 @@
   private async handleKeydown(e: KeyboardEvent) {
     const copyLink = this.copyLinks?.find(link => link.shortcut === e.key);
     if (!copyLink) return;
-    await copyToClipbard(copyLink.value, copyLink.label);
+    await copyToClipboard(copyLink.value, copyLink.label);
     this.closeDropdown();
   }
 
-  toggleDropdown() {
+  toggleDropdown(button?: HTMLElement) {
+    if (button) {
+      this.dropdown!.anchorElement = button;
+    }
     this.isDropdownOpen ? this.closeDropdown() : this.openDropdown();
   }
 
@@ -111,13 +172,16 @@
     this.dropdown?.close();
   }
 
-  openDropdown() {
-    this.dropdown?.open();
+  openDropdown(button?: HTMLElement) {
+    if (button) {
+      this.dropdown!.anchorElement = button;
+    }
+    this.dropdown?.show();
     this.awaitOpen(() => {
       if (!this.copyClipboardRef?.value) return;
-      queryAndAssert<HTMLInputElement>(
+      queryAndAssert<MdOutlinedTextField>(
         this.copyClipboardRef.value,
-        'input'
+        'md-outlined-text-field'
       )?.select();
     });
   }
diff --git a/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_screenshot_test.ts
new file mode 100644
index 0000000..5e36644
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_screenshot_test.ts
@@ -0,0 +1,54 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-copy-links';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrCopyLinks} from './gr-copy-links';
+import {waitUntil} from '../../../test/test-utils';
+
+suite('gr-copy-links screenshot tests', () => {
+  let element: GrCopyLinks;
+
+  setup(async () => {
+    const links = [
+      {
+        label: 'Change ID',
+        shortcut: 'd',
+        value: '123456',
+      },
+      {
+        label: 'Description',
+        shortcut: 'm',
+        value:
+          'This is a test commit message\nWith multiple lines\nAnd more details',
+        multiline: true,
+      },
+    ];
+    element = await fixture<GrCopyLinks>(
+      html`<gr-copy-links .copyLinks=${links}></gr-copy-links>`
+    );
+    await element.updateComplete;
+    // md-menu requires anchor to be set
+    // so we create a dummy element to allow
+    // us to open it.
+    const button = document.createElement('button');
+    const mdMenu = element.shadowRoot?.querySelector('md-menu');
+    mdMenu!.anchorElement = button;
+    element.openDropdown();
+    await waitUntil(() => element.isDropdownOpen);
+    await element.updateComplete;
+  });
+
+  test('dropdown screenshot', async () => {
+    await visualDiff(
+      element.shadowRoot?.querySelector('.dropdown-content'),
+      'gr-copy-links'
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_test.ts b/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_test.ts
index f2c536b..b589115 100644
--- a/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-copy-links/gr-copy-links_test.ts
@@ -5,13 +5,13 @@
  */
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import './gr-copy-links';
 import {GrCopyLinks} from './gr-copy-links';
 import {pressKey, queryAndAssert, waitUntil} from '../../../test/test-utils';
-import {IronDropdownElement} from '@polymer/iron-dropdown';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {MdMenu} from '@material/web/menu/menu';
 
 suite('gr-copy-links tests', () => {
   let element: GrCopyLinks;
@@ -27,6 +27,12 @@
       html`<gr-copy-links .copyLinks=${links}></gr-copy-links>`
     );
     await element.updateComplete;
+    // md-menu requires anchor to be set
+    // so we create a dummy element to allow
+    // us to open it.
+    const button = document.createElement('button');
+    const mdMenu = element.shadowRoot?.querySelector('md-menu');
+    mdMenu!.anchorElement = button;
     element.openDropdown();
     await waitUntil(() => element.isDropdownOpen);
     await element.updateComplete;
@@ -35,22 +41,20 @@
   test('renders', () => {
     assert.shadowDom.equal(
       element,
-      /* HTML */ `<iron-dropdown
-        aria-disabled="false"
-        horizontal-align="left"
-        vertical-align="top"
-      >
-      <div slot="dropdown-content">
+      /* HTML */ `<md-menu default-focus="none" open="" quick="" tabindex="-1">
+        <div class="dropdown-content">
           <div class="copy-link-row">
-            <gr-copy-clipboard label="Change ID" nowrap="" shortcut="l - d"
-                text="123456" id="Change_ID-field-copy-clipboard">
+            <gr-copy-clipboard
+              id="Change_ID-field-copy-clipboard"
+              label="Change ID"
+              nowrap=""
+              shortcut="l - d"
+              text="123456"
+            >
             </gr-copy-clipboard>
           </div>
-      </iron-dropdown>`,
-      {
-        // iron-dropdown sizing seems to vary between local & CI
-        ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
-      }
+        </div>
+      </md-menu>`
     );
   });
 
@@ -68,11 +72,8 @@
 
   test('shorcuts writes to clipboard', () => {
     const clipboardStub = sinon.stub(window.navigator.clipboard, 'writeText');
-    const ironDropdown = queryAndAssert<IronDropdownElement>(
-      element,
-      'iron-dropdown'
-    );
-    pressKey(ironDropdown, 'd');
+    const mdMenu = queryAndAssert<MdMenu>(element, 'md-menu');
+    pressKey(mdMenu, 'd');
     assert.isTrue(clipboardStub.called);
     assert.isTrue(clipboardStub.calledWith('123456'));
   });
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 93529de..5bccb07 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -8,12 +8,12 @@
 import {DownloadInfo, PatchSetNum} from '../../../types/common';
 import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-commands';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {copyToClipbard, hasOwnProperty} from '../../../utils/common-util';
+import {copyToClipboard, hasOwnProperty} from '../../../utils/common-util';
 import {fire} from '../../../utils/event-util';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
-import {customElement, state, query} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {BindValueChangeEvent} from '../../../types/events';
 import {ShortcutController} from '../../lit/shortcut-controller';
@@ -23,6 +23,8 @@
 import {ParsedChangeInfo} from '../../../types/types';
 import {configModelToken} from '../../../models/config/config-model';
 import {shorten} from '../../../utils/patch-set-util';
+import {getAppContext} from '../../../services/app-context';
+import {fireError} from '../../../utils/event-util';
 
 type DownloadKind = 'zip' | 'raw' | 'base64';
 
@@ -54,6 +56,8 @@
 
   private readonly getConfigModel = resolve(this, configModelToken);
 
+  private readonly restApiService = getAppContext().restApiService;
+
   constructor() {
     super();
     subscribe(
@@ -183,6 +187,19 @@
       <div class="patchFiles">
         <label>Patch file</label>
         <div>
+          <gr-button
+            id="copy-clipboard-button"
+            link=""
+            class="copyToClipboard"
+            title="Copy Patch to clipboard"
+            @click=${this.handleCopyPatch}
+            aria-label="copy"
+            aria-description="Click to copy patch to clipboard"
+          >
+            <div>
+              <gr-icon id="icon" icon="content_copy" small></gr-icon>
+            </div>
+          </gr-button>
           <a id="download" .href=${this.computeDownloadLink('raw')} download>
             ${this.computeDownloadFilename('raw')}
           </a>
@@ -250,13 +267,29 @@
     const index = Number(e.key) - 1;
     const commands = this.computeDownloadCommands();
     if (index > commands.length) return;
-    await copyToClipbard(
+    await copyToClipboard(
       commands[index].command,
       `${commands[index].title} command`
     );
     fire(this, 'close', {});
   }
 
+  private async handleCopyPatch(e: Event) {
+    e.preventDefault();
+    e.stopPropagation();
+    if (!this.change || !this.patchNum) return;
+    const patchContent = await this.restApiService.getPatchContent(
+      this.change._number,
+      this.patchNum
+    );
+    if (!patchContent) {
+      fireError(this, 'Failed to get patch content');
+      return;
+    }
+    await copyToClipboard(patchContent, 'patch file content');
+    this.handleCloseTap(e);
+  }
+
   override focus() {
     if (this.schemes.length) {
       assertIsDefined(this.downloadCommands, 'downloadCommands');
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index 9ed7668..d71c2af 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -24,10 +24,11 @@
   mockPromise,
   queryAll,
   queryAndAssert,
+  stubRestApi,
   waitUntil,
 } from '../../../test/test-utils';
 import {GrDownloadCommands} from '../../shared/gr-download-commands/gr-download-commands';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 
 function getChangeObject() {
@@ -130,11 +131,31 @@
         <div class="patchFiles">
           <label> Patch file </label>
           <div>
-            <a download="" href="" id="download"> </a>
-            <a download="" href=""> </a>
-            <a download="" href=""> </a>
-          </div>
-        </div>
+            <gr-button
+              aria-description="Click to copy patch to clipboard"
+              aria-disabled="false"
+              aria-label="copy"
+              class="copyToClipboard"
+              id="copy-clipboard-button"
+              link=""
+              role="button"
+              tabindex="0"
+              title="Copy Patch to clipboard"
+            >
+              <div>
+                <gr-icon
+                  icon="content_copy"
+                  id="icon"
+                  small=""
+                >
+                </gr-icon>
+              </div>
+            </gr-button>
+           <a download="" href="" id="download"> </a>
+           <a download="" href=""> </a>
+           <a download="" href=""> </a>
+         </div>
+       </div>
         <div class="archivesContainer">
           <label> Archive </label>
           <div class="archives" id="archives">
@@ -293,4 +314,68 @@
     };
     assert.isTrue(element.computeHidePatchFile());
   });
+
+  suite('copy to clipboard functionality', () => {
+    let copyToClipboardStub: sinon.SinonStub;
+    let fireStub: sinon.SinonStub;
+    let getPatchContentStub: sinon.SinonStub;
+
+    setup(() => {
+      copyToClipboardStub = sinon.stub(navigator.clipboard, 'writeText');
+      fireStub = sinon.stub(element, 'dispatchEvent');
+      getPatchContentStub = stubRestApi('getPatchContent');
+      element.change = getChangeObject();
+      element.patchNum = 1 as PatchSetNum;
+    });
+
+    test('successfully copies patch content to clipboard', async () => {
+      const patchContent = 'test patch content';
+      getPatchContentStub.resolves(patchContent);
+
+      const copyButton = queryAndAssert<GrButton>(
+        element,
+        '#copy-clipboard-button'
+      );
+      copyButton.click();
+
+      await waitUntil(() => getPatchContentStub.called);
+      assert.isTrue(
+        getPatchContentStub.calledWith(
+          element.change!._number,
+          element.patchNum
+        )
+      );
+      assert.isTrue(copyToClipboardStub.calledWith(patchContent));
+      assert.isTrue(fireStub.calledWith(sinon.match({type: 'close'})));
+    });
+
+    test('handles failed patch content fetch', async () => {
+      getPatchContentStub.resolves(undefined);
+
+      const copyButton = queryAndAssert<GrButton>(
+        element,
+        '#copy-clipboard-button'
+      );
+      copyButton.click();
+
+      await waitUntil(() => getPatchContentStub.called);
+      assert.isFalse(copyToClipboardStub.called);
+      assert.isTrue(fireStub.called);
+    });
+
+    test('handles error during patch content fetch', async () => {
+      const error = new Error('Network error');
+      getPatchContentStub.rejects(error);
+
+      const copyButton = queryAndAssert<GrButton>(
+        element,
+        '#copy-clipboard-button'
+      );
+      copyButton.click();
+
+      await waitUntil(() => getPatchContentStub.called);
+      assert.isFalse(copyToClipboardStub.called);
+      assert.isFalse(fireStub.called);
+    });
+  });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index 64091b4..dd3bed9 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -6,21 +6,20 @@
 import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
 import '../../diff/gr-patch-range-select/gr-patch-range-select';
 import '../../edit/gr-edit-controls/gr-edit-controls';
-import '../../shared/gr-select/gr-select';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../gr-commit-info/gr-commit-info';
 import {FilesExpandedState} from '../gr-file-list-constants';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {property, customElement, query, state} from 'lit/decorators.js';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {
   AccountInfo,
-  ChangeInfo,
-  PatchSetNum,
-  CommitInfo,
-  ServerInfo,
   BasePatchSetNum,
+  ChangeInfo,
+  CommitInfo,
+  PatchSetNum,
   PatchSetNumber,
+  ServerInfo,
 } from '../../../types/common';
 import {DiffPreferencesInfo} from '../../../types/diff';
 import {GrDiffModeSelector} from '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
index 8bf316f..1d853bc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
@@ -25,7 +25,7 @@
 } from '../../../types/common';
 import {ChangeInfo, ChangeStatus} from '../../../api/rest-api';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {testResolver} from '../../../test/common-test-setup';
 
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 5a9fa12..bbcf97e 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
@@ -11,7 +11,6 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-cursor-manager/gr-cursor-manager';
 import '../../shared/gr-icon/gr-icon';
-import '../../shared/gr-select/gr-select';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../../shared/gr-file-status/gr-file-status';
@@ -51,13 +50,13 @@
 import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
 import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
 import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
-import {Timing, Interaction} from '../../../constants/reporting';
+import {Interaction, Timing} from '../../../constants/reporting';
 import {RevisionInfo} from '../../shared/revision-info/revision-info';
 import {select} from '../../../utils/observable-util';
 import {resolve} from '../../../models/dependency';
 import {browserModelToken} from '../../../models/browser/browser-model';
 import {commentsModelToken} from '../../../models/comments/comments-model';
-import {RunResult, checksModelToken} from '../../../models/checks/checks-model';
+import {checksModelToken, RunResult} from '../../../models/checks/checks-model';
 import {changeModelToken} from '../../../models/change/change-model';
 import {filesModelToken} from '../../../models/change/files-model';
 import {ShortcutController} from '../../lit/shortcut-controller';
@@ -80,8 +79,8 @@
 import {incrementalRepeat} from '../../lit/incremental-repeat';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {
-  createChangeUrl,
   changeViewModelToken,
+  createChangeUrl,
 } from '../../../models/views/change';
 import {userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -235,6 +234,12 @@
   @state()
   files: NormalizedFileInfo[] = [];
 
+  @state()
+  private modifiedFiles: NormalizedFileInfo[] = [];
+
+  @state()
+  private unmodifiedFiles: NormalizedFileInfo[] = [];
+
   // Private but used in tests.
   @state() filesLeftBase: NormalizedFileInfo[] = [];
 
@@ -297,6 +302,9 @@
   @state()
   private dynamicPrependedContentEndpoints?: string[];
 
+  // Intentionally not a @state(), because updated in willUpdate() lifecycle method.
+  sizeBarLayout: SizeBarLayout = createDefaultSizeBarLayout();
+
   private readonly reporting = getAppContext().reportingService;
 
   private readonly restApiService = getAppContext().restApiService;
@@ -404,6 +412,19 @@
             z-index. setting to 1 places it directly above. */
           z-index: 1;
         }
+        .separator-row {
+          background-color: var(--background-color-secondary);
+          border-top: 1px solid var(--border-color);
+          color: var(--deemphasized-text-color);
+          padding: var(--spacing-l);
+        }
+        .separator-row .path {
+          color: var(--deemphasized-text-color);
+          font-style: italic;
+        }
+        .separator-row .path gr-icon {
+          margin-left: var(--spacing-s);
+        }
         .file-row:hover {
           background-color: var(--hover-background-color);
         }
@@ -434,7 +455,7 @@
           flex: 1;
           /* Wrap it into multiple lines if too long. */
           white-space: normal;
-          word-break: break-word;
+          overflow-wrap: anywhere;
         }
         .oldPath {
           color: var(--deemphasized-text-color);
@@ -817,7 +838,13 @@
       this,
       () => this.getFilesModel().filesIncludingUnmodified$,
       files => {
-        this.files = [...files];
+        this.modifiedFiles = files.filter(
+          f => f.status !== FileInfoStatus.UNMODIFIED
+        );
+        this.unmodifiedFiles = files.filter(
+          f => f.status === FileInfoStatus.UNMODIFIED
+        );
+        this.files = [...this.modifiedFiles, ...this.unmodifiedFiles];
       }
     );
     subscribe(
@@ -903,6 +930,7 @@
       this.patchChange = undefined;
       this.filesChanged();
       this.numFilesShown = Math.min(this.files.length, DEFAULT_NUM_FILES_SHOWN);
+      this.updateSizeBarLayout();
       fire(this, 'files-shown-changed', {length: this.numFilesShown});
     }
     if (changedProperties.has('expandedFiles')) {
@@ -910,6 +938,7 @@
     }
     if (changedProperties.has('numFilesShown')) {
       fire(this, 'files-shown-changed', {length: this.numFilesShown});
+      this.updateSizeBarLayout();
     }
   }
 
@@ -1078,22 +1107,46 @@
     );
   }
 
+  private renderSeparator() {
+    const text = 'Unmodified Files';
+    const tooltipText =
+      'Files not modified in this patchset, but referenced by a check or a comment.' +
+      ' May include files outside of this change, also virtual or generated files.';
+    return html`
+      <div class="row separator-row" role="row">
+        <div class="path" role="gridcell">
+          <span>${text}</span>
+          <gr-tooltip-content .title=${tooltipText} has-tooltip>
+            <gr-icon icon="info"></gr-icon>
+          </gr-tooltip-content>
+        </div>
+      </div>
+    `;
+  }
+
   private renderShownFiles() {
     const showDynamicColumns = this.computeShowDynamicColumns();
     const showPrependedDynamicColumns =
       this.computeShowPrependedDynamicColumns();
-    const sizeBarLayout = this.computeSizeBarLayout();
+
+    const separatorIndex = this.modifiedFiles.length;
 
     return incrementalRepeat({
       values: this.files,
-      mapFn: (f, i) =>
-        this.renderFileRow(
+      mapFn: (f, i) => html`
+        ${when(
+          i === separatorIndex &&
+            this.modifiedFiles.length > 0 &&
+            this.unmodifiedFiles.length > 0,
+          () => this.renderSeparator()
+        )}
+        ${this.renderFileRow(
           f as NormalizedFileInfo,
           i,
-          sizeBarLayout,
           showDynamicColumns,
           showPrependedDynamicColumns
-        ),
+        )}
+      `,
       initialCount: this.fileListIncrement,
       targetFrameRate: 1,
       startAt: 0,
@@ -1104,7 +1157,6 @@
   private renderFileRow(
     file: NormalizedFileInfo,
     index: number,
-    sizeBarLayout: SizeBarLayout,
     showDynamicColumns: boolean,
     showPrependedDynamicColumns: boolean
   ) {
@@ -1124,8 +1176,8 @@
         )}
         ${this.renderFileStatus(file)}
         ${this.renderFilePath(file, previousFileName)}
-        ${this.renderFileComments(file)}
-        ${this.renderSizeBar(file, sizeBarLayout)} ${this.renderFileStats(file)}
+        ${this.renderFileComments(file)} ${this.renderSizeBar(file)}
+        ${this.renderFileStats(file)}
         ${when(showDynamicColumns, () =>
           this.renderDynamicContentEndpointsForFile(file)
         )}
@@ -1373,10 +1425,8 @@
     </div>`;
   }
 
-  private renderSizeBar(
-    file: NormalizedFileInfo,
-    sizeBarLayout: SizeBarLayout
-  ) {
+  private renderSizeBar(file: NormalizedFileInfo) {
+    const sizeBarLayout: SizeBarLayout = this.sizeBarLayout;
     return html` <div class="desktop" role="gridcell">
       <!-- The content must be in a separate div. It guarantees, that
           gridcell always visible for screen readers.
@@ -1416,7 +1466,7 @@
           class="removed"
           tabindex="0"
           aria-label=${`${file.lines_deleted} removed`}
-          ?hidden=${file.binary}
+          ?hidden=${!!file.binary}
         >
           -${file.lines_deleted}
         </span>
@@ -1424,7 +1474,7 @@
           class="added"
           tabindex="0"
           aria-label=${`${file.lines_inserted} added`}
-          ?hidden=${file.binary}
+          ?hidden=${!!file.binary}
         >
           +${file.lines_inserted}
         </span>
@@ -1762,7 +1812,10 @@
       icons =>
         html`
           <div class="checkChip ${icons[0].name}">
-            <gr-icon icon=${icons[0].name} ?filled=${icons[0].filled}></gr-icon>
+            <gr-icon
+              icon=${icons[0].name}
+              ?filled=${!!icons[0].filled}
+            ></gr-icon>
             <div>${icons.length}</div>
           </div>
         `
@@ -1913,24 +1966,12 @@
     this.renderInOrder(this.expandedFiles, this.diffs);
   }
 
-  private forEachDiff(fn: (host: GrDiffHost) => void) {
-    const diffs = this.diffs;
-    for (let i = 0; i < diffs.length; i++) {
-      fn(diffs[i]);
-    }
-  }
-
   expandAllDiffs() {
-    // Find the list of paths that are in the file list, but not in the
-    // expanded list.
-    const newFiles: PatchSetFile[] = [];
-    let path: string;
-    for (let i = 0; i < this.numFilesShown; i++) {
-      path = this.files[i].__path;
-      if (!this.expandedFiles.some(f => f.path === path)) {
-        newFiles.push(this.computePatchSetFile(this.files[i]));
-      }
-    }
+    const newFiles = this.files
+      .slice(0, this.numFilesShown)
+      // TODO(b/419187980): Refactor expandedFiles to use a Set for efficiency.
+      .filter(file => !this.expandedFiles.some(f => f.path === file.__path))
+      .map(file => this.computePatchSetFile(file));
 
     this.reporting.reportInteraction(Interaction.FILE_LIST_ALL_DIFFS_EXPANDED);
     this.expandedFiles = newFiles.concat(this.expandedFiles);
@@ -2196,7 +2237,7 @@
   }
 
   private handleToggleLeftPane() {
-    this.forEachDiff(diff => {
+    this.diffs.forEach(diff => {
       diff.toggleLeftDiff();
     });
   }
@@ -2536,19 +2577,14 @@
    * In the given NodeList of diff elements, find the diff for the given path.
    */
   private findDiffByPath(path: string, diffElements: GrDiffHost[]) {
-    for (let i = 0; i < diffElements.length; i++) {
-      if (diffElements[i].path === path) {
-        return diffElements[i];
-      }
-    }
-    return undefined;
+    return diffElements.find(diff => diff.path === path);
   }
 
   /**
    * Compute size bar layout values from the file list.
    * Private but used in tests.
    */
-  computeSizeBarLayout() {
+  updateSizeBarLayout() {
     const stats: SizeBarLayout = createDefaultSizeBarLayout();
     this.files
       .slice(0, this.numFilesShown)
@@ -2569,7 +2605,7 @@
         SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
       stats.additionOffset = stats.maxDeletionWidth + SIZE_BAR_GAP_WIDTH;
     }
-    return stats;
+    this.sizeBarLayout = stats;
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts
index f80f48b..b90d84e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_screenshot_test.ts
@@ -6,13 +6,16 @@
 import '../../../test/common-test-setup';
 import '../../shared/gr-date-formatter/gr-date-formatter';
 import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
 import {visualDiff} from '@web/test-runner-visual-regression';
 import {FileInfo, PARENT, RevisionPatchSetNum} from '../../../api/rest-api';
 import {normalize} from '../../../models/change/files-model';
 import {PatchRange} from '../../../types/common';
 import {DiffPreferencesInfo} from '../../../api/diff';
-import {NormalizedFileInfo, GrFileList} from './gr-file-list';
+import {GrFileList, NormalizedFileInfo} from './gr-file-list';
 import './gr-file-list';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
 
 suite('gr-file-list screenshot tests', () => {
   let element: GrFileList;
@@ -40,7 +43,8 @@
     };
     element = await fixture(
       html`<gr-file-list
-        .patchRange=${patchRange}
+        .patchNum=${patchRange.patchNum}
+        .basePatchNum=${patchRange.basePatchNum}
         .diffPrefs=${diffPrefs}
       ></gr-file-list>`
     );
@@ -54,5 +58,6 @@
     await element.updateComplete;
 
     await visualDiff(element, 'gr-file-list');
+    await visualDiffDarkTheme(element, 'gr-file-list');
   });
 });
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 712cfc0..8b8b0d3 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
@@ -11,12 +11,12 @@
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   mockPromise,
-  query,
-  stubRestApi,
-  waitUntil,
   pressKey,
+  query,
   stubElement,
+  stubRestApi,
   waitEventLoop,
+  waitUntil,
 } from '../../../test/test-utils';
 import {
   BasePatchSetNum,
@@ -55,7 +55,8 @@
 import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
 import {GrEditFileControls} from '../../edit/gr-edit-file-controls/gr-edit-file-controls';
 import {GrIcon} from '../../shared/gr-icon/gr-icon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {GrTooltipContent} from '../../shared/gr-tooltip-content/gr-tooltip-content';
+import {assert, fixture, html} from '@open-wc/testing';
 import {Modifier} from '../../../utils/dom-util';
 import {testResolver} from '../../../test/common-test-setup';
 import {FileMode} from '../../../utils/file-util';
@@ -69,7 +70,11 @@
 
 suite('gr-diff a11y test', () => {
   test('audit', async () => {
-    assert.isAccessible(await fixture(html`<gr-file-list></gr-file-list>`));
+    const element = await fixture<GrFileList>(
+      html`<gr-file-list></gr-file-list>`
+    );
+    await element.updateComplete;
+    await assert.isAccessible(element);
   });
 });
 
@@ -97,7 +102,6 @@
   suite('basic tests', async () => {
     setup(async () => {
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
-      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
       stubElement('gr-diff-host', 'reload').callsFake(() => Promise.resolve());
@@ -434,7 +438,8 @@
       );
     });
 
-    test('correct number of files are shown', async () => {
+    // TODO(milutin): flaky test, enable it back
+    test.skip('correct number of files are shown', async () => {
       element.fileListIncrement = 100;
       element.files = createFiles(250);
       await element.updateComplete;
@@ -1188,11 +1193,11 @@
       const markReviewLabel =
         fileRows[0].querySelector<HTMLSpanElement>('.markReviewed');
       assert.isOk(markReviewLabel);
-      assert.isTrue(commitReviewLabel!.classList.contains('isReviewed'));
-      assert.equal(markReviewLabel!.textContent, 'MARK UNREVIEWED');
+      assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
+      assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
 
       const clickSpy = sinon.spy(element, 'reviewedClick');
-      markReviewLabel!.click();
+      markReviewLabel.click();
       await element.updateComplete;
 
       assert.isTrue(clickSpy.calledOnce);
@@ -1203,10 +1208,10 @@
       element.reviewed = ['myfile.txt'];
       await element.updateComplete;
 
-      assert.isFalse(commitReviewLabel!.classList.contains('isReviewed'));
-      assert.equal(markReviewLabel!.textContent, 'MARK REVIEWED');
+      assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
+      assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
 
-      markReviewLabel!.click();
+      markReviewLabel.click();
       await element.updateComplete;
 
       assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
@@ -1217,8 +1222,8 @@
       element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
       await element.updateComplete;
 
-      assert.isTrue(commitReviewLabel!.classList.contains('isReviewed'));
-      assert.equal(markReviewLabel!.textContent, 'MARK UNREVIEWED');
+      assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
+      assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
     });
 
     test('handleFileListClick', async () => {
@@ -1720,6 +1725,59 @@
     });
   });
 
+  suite('unmodified files separator', () => {
+    setup(async () => {
+      element = await fixture(html`<gr-file-list></gr-file-list>`);
+    });
+
+    test('should not show separator if no unmodified files', async () => {
+      (element as any).modifiedFiles = createFiles(2);
+      (element as any).unmodifiedFiles = [];
+      await element.updateComplete;
+
+      const separator = query(element, '.separator-row');
+      assert.isNotOk(separator);
+    });
+
+    test('should not show separator if no modified files', async () => {
+      (element as any).modifiedFiles = [];
+      (element as any).unmodifiedFiles = createFiles(2);
+      await element.updateComplete;
+
+      const separator = query(element, '.separator-row');
+      assert.isNotOk(separator);
+    });
+
+    test('should show separator if modified and unmodified files', async () => {
+      (element as any).modifiedFiles = createFiles(2);
+      (element as any).unmodifiedFiles = createFiles(3);
+      element.files = [
+        ...(element as any).modifiedFiles,
+        ...(element as any).unmodifiedFiles,
+      ];
+      await element.updateComplete;
+
+      const separator = queryAndAssert(element, '.separator-row');
+      assert.isOk(separator);
+
+      const text = queryAndAssert(separator, 'span');
+      assert.equal(text.textContent?.trim(), 'Unmodified Files');
+
+      const icon = queryAndAssert<GrIcon>(separator, 'gr-icon');
+      assert.equal(icon.icon, 'info');
+
+      const tooltip = queryAndAssert<GrTooltipContent>(
+        separator,
+        'gr-tooltip-content'
+      );
+      assert.equal(
+        tooltip.title,
+        'Files not modified in this patchset, but referenced by a check or a comment.' +
+          ' May include files outside of this change, also virtual or generated files.'
+      );
+    });
+  });
+
   suite('diff url file list', () => {
     test('diff url', () => {
       const path = 'index.php';
@@ -1782,7 +1840,7 @@
 
       element.files = [];
       await element.updateComplete;
-      assert.deepEqual(element.computeSizeBarLayout(), defaultSizeBarLayout);
+      assert.deepEqual(element.sizeBarLayout, defaultSizeBarLayout);
 
       element.files = [
         {
@@ -1807,7 +1865,7 @@
         },
       ];
       await element.updateComplete;
-      const layout = element.computeSizeBarLayout();
+      const layout = element.sizeBarLayout;
       assert.equal(layout.maxInserted, 5);
       assert.equal(layout.maxDeleted, 10);
     });
@@ -2010,7 +2068,6 @@
     setup(async () => {
       stubRestApi('getPreferences').returns(Promise.resolve(undefined));
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
-      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       stubRestApi('getDiff').callsFake(() => Promise.resolve(createDiff()));
       stubElement('gr-diff-host', 'prefetchDiff').callsFake(() => {});
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts
new file mode 100644
index 0000000..f152806
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow.ts
@@ -0,0 +1,366 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {customElement, property, state} from 'lit/decorators.js';
+import {css, html, LitElement} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {grFormStyles} from '../../../styles/gr-form-styles';
+import {FlowInput} from '../../../api/rest-api';
+import {getAppContext} from '../../../services/app-context';
+import {NumericChangeId, ServerInfo} from '../../../types/common';
+import '../../shared/gr-button/gr-button';
+import '../../core/gr-search-autocomplete/gr-search-autocomplete';
+import '@material/web/select/outlined-select.js';
+import '@material/web/select/select-option.js';
+import '@material/web/textfield/outlined-text-field.js';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
+import {flowsModelToken} from '../../../models/flows/flows-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {
+  AutocompleteSuggestion,
+  fetchAccountSuggestions,
+} from '../../../utils/autocomplete-util';
+import {ValueChangedEvent} from '../../../types/events';
+import {SuggestionProvider} from '../../core/gr-search-autocomplete/gr-search-autocomplete';
+import {when} from 'lit/directives/when.js';
+
+const MAX_AUTOCOMPLETE_RESULTS = 10;
+
+@customElement('gr-create-flow')
+export class GrCreateFlow extends LitElement {
+  @property({type: Number}) changeNum?: NumericChangeId;
+
+  // Property so that we can mock it in tests
+  @property({type: String}) hostUrl?: string;
+
+  @state()
+  private stages: {
+    condition: string;
+    action: string;
+    parameterStr: string;
+  }[] = [];
+
+  @state() private currentCondition = '';
+
+  @state() private currentAction = '';
+
+  @state() private currentParameter = '';
+
+  @state() private currentConditionPrefix = 'Gerrit';
+
+  @state() private loading = false;
+
+  @state() private serverConfig?: ServerInfo;
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  private readonly getConfigModel = resolve(this, configModelToken);
+
+  private readonly getFlowsModel = resolve(this, flowsModelToken);
+
+  private readonly projectSuggestions: SuggestionProvider = (
+    predicate,
+    expression
+  ) => this.fetchProjects(predicate, expression);
+
+  private readonly groupSuggestions: SuggestionProvider = (
+    predicate,
+    expression
+  ) => this.fetchGroups(predicate, expression);
+
+  private readonly accountSuggestions: SuggestionProvider = (
+    predicate,
+    expression
+  ) => {
+    const accountFetcher = (expr: string) =>
+      this.restApiService.queryAccounts(
+        expr,
+        MAX_AUTOCOMPLETE_RESULTS,
+        undefined,
+        undefined,
+        throwingErrorCallback
+      );
+    return fetchAccountSuggestions(
+      accountFetcher,
+      predicate,
+      expression,
+      this.serverConfig
+    );
+  };
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getConfigModel().serverConfig$,
+      config => (this.serverConfig = config)
+    );
+  }
+
+  static override get styles() {
+    return [
+      sharedStyles,
+      grFormStyles,
+      css`
+        .add-stage-row {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-s);
+        }
+        .add-stage-row > md-outlined-select,
+        .add-stage-row > md-outlined-text-field,
+        .add-stage-row > gr-search-autocomplete {
+          width: 15em;
+        }
+        table {
+          border-collapse: collapse;
+        }
+        th,
+        td {
+          border: 1px solid var(--border-color);
+          padding: var(--spacing-s);
+          text-align: left;
+        }
+      `,
+    ];
+  }
+
+  override firstUpdated() {
+    this.hostUrl = window.location.origin + window.location.pathname;
+  }
+
+  private renderTable() {
+    return when(
+      this.stages.length > 0,
+      () => html`
+        <table>
+          <thead>
+            <tr>
+              <th>Stage</th>
+              <th>Condition</th>
+              <th>Action</th>
+              <th>Parameters</th>
+              <th></th>
+            </tr>
+          </thead>
+          <tbody>
+            ${this.stages.map(
+              (stage, index) => html`
+                <tr>
+                  <td>${index + 1}</td>
+                  <td>${stage.condition}</td>
+                  <td>${stage.action}</td>
+                  <td>${stage.parameterStr}</td>
+                  <td>
+                    <gr-button
+                      link
+                      @click=${() => this.handleRemoveStage(index)}
+                      title="Delete stage"
+                    >
+                      <gr-icon icon="delete" filled></gr-icon>
+                    </gr-button>
+                  </td>
+                </tr>
+              `
+            )}
+          </tbody>
+        </table>
+      `
+    );
+  }
+
+  override render() {
+    return html`
+      <div>${this.renderTable()}</div>
+      <div class="add-stage-row">
+        <md-outlined-select
+          value=${this.currentConditionPrefix}
+          @change=${(e: Event) => {
+            const select = e.target as HTMLSelectElement;
+            this.currentConditionPrefix = select.value;
+          }}
+        >
+          <md-select-option value="Gerrit">
+            <div slot="headline">Gerrit</div>
+          </md-select-option>
+          <md-select-option value="Other">
+            <div slot="headline">Other</div>
+          </md-select-option>
+        </md-outlined-select>
+        ${this.currentConditionPrefix === 'Gerrit'
+          ? html`<gr-search-autocomplete
+              .placeholder=${'Create condition'}
+              .value=${this.currentCondition}
+              .projectSuggestions=${this.projectSuggestions}
+              .groupSuggestions=${this.groupSuggestions}
+              .accountSuggestions=${this.accountSuggestions}
+              @text-changed=${this.handleGerritConditionTextChanged}
+            ></gr-search-autocomplete>`
+          : html`<md-outlined-text-field
+              label="Condition"
+              .value=${this.currentCondition}
+              @input=${(e: InputEvent) =>
+                (this.currentCondition = (
+                  e.target as MdOutlinedTextField
+                ).value)}
+            ></md-outlined-text-field>`}
+        <span> -> </span>
+        <md-outlined-text-field
+          label="Action"
+          .value=${this.currentAction}
+          @input=${(e: InputEvent) =>
+            (this.currentAction = (e.target as MdOutlinedTextField).value)}
+        ></md-outlined-text-field>
+        <md-outlined-text-field
+          label="Parameters"
+          .value=${this.currentParameter}
+          @input=${(e: InputEvent) =>
+            (this.currentParameter = (e.target as MdOutlinedTextField).value)}
+        ></md-outlined-text-field>
+        <gr-button aria-label="Add Stage" @click=${this.handleAddStage}
+          >Add Stage</gr-button
+        >
+      </div>
+      <gr-button
+        aria-label="Create Flow"
+        ?disabled=${this.loading}
+        @click=${this.handleCreateFlow}
+      >
+        Create Flow
+      </gr-button>
+    `;
+  }
+
+  private handleGerritConditionTextChanged(e: ValueChangedEvent) {
+    this.currentCondition = e.detail.value ?? '';
+  }
+
+  // TODO: Move into the common util file
+  fetchProjects(
+    predicate: string,
+    expression: string
+  ): Promise<AutocompleteSuggestion[]> {
+    return this.restApiService
+      .getSuggestedRepos(
+        expression,
+        MAX_AUTOCOMPLETE_RESULTS,
+        throwingErrorCallback
+      )
+      .then(projects => {
+        if (!projects) {
+          return [];
+        }
+        const keys = Object.keys(projects);
+        return keys.map(key => {
+          return {text: predicate + ':' + key};
+        });
+      });
+  }
+
+  fetchGroups(
+    predicate: string,
+    expression: string
+  ): Promise<AutocompleteSuggestion[]> {
+    if (expression.length === 0) {
+      return Promise.resolve([]);
+    }
+    return this.restApiService
+      .getSuggestedGroups(
+        expression,
+        undefined,
+        MAX_AUTOCOMPLETE_RESULTS,
+        throwingErrorCallback
+      )
+      .then(groups => {
+        if (!groups) {
+          return [];
+        }
+        const keys = Object.keys(groups);
+        return keys.map(key => {
+          return {text: predicate + ':' + key};
+        });
+      });
+  }
+
+  private handleAddStage() {
+    if (this.currentCondition.trim() === '' && this.currentAction.trim() === '')
+      return;
+    const condition =
+      this.currentConditionPrefix === 'Gerrit'
+        ? `${this.hostUrl} is ${this.currentCondition}`
+        : this.currentCondition;
+    this.stages = [
+      ...this.stages,
+      {
+        condition,
+        action: this.currentAction,
+        parameterStr: this.currentParameter,
+      },
+    ];
+    this.currentCondition = '';
+    this.currentAction = '';
+    this.currentParameter = '';
+  }
+
+  private handleRemoveStage(index: number) {
+    this.stages = this.stages.filter((_, i) => i !== index);
+  }
+
+  private async handleCreateFlow() {
+    if (!this.changeNum) return;
+
+    const allStages = [...this.stages];
+    if (
+      this.currentCondition.trim() !== '' ||
+      this.currentAction.trim() !== ''
+    ) {
+      const condition =
+        this.currentConditionPrefix === 'Gerrit'
+          ? `${this.hostUrl} is ${this.currentCondition}`
+          : this.currentCondition;
+      allStages.push({
+        condition,
+        action: this.currentAction,
+        parameterStr: this.currentParameter,
+      });
+    }
+
+    if (allStages.length === 0) return; // Or show an error
+
+    this.loading = true;
+    const flowInput: FlowInput = {
+      stage_expressions: allStages.map(stage => {
+        if (stage.action) {
+          const action: {name: string; parameters?: string[]} = {
+            name: stage.action,
+          };
+          if (stage.parameterStr.length > 0) {
+            action.parameters = stage.parameterStr.split(' ');
+          }
+          return {
+            condition: stage.condition,
+            action,
+          };
+        }
+        return {condition: stage.condition};
+      }),
+    };
+    await this.getFlowsModel().createFlow(flowInput);
+    this.stages = [];
+    this.currentCondition = '';
+    this.currentAction = '';
+    this.currentParameter = '';
+    this.loading = false;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-create-flow': GrCreateFlow;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts
new file mode 100644
index 0000000..ac71093
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-create-flow_test.ts
@@ -0,0 +1,306 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-create-flow';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrCreateFlow} from './gr-create-flow';
+import {queryAll, queryAndAssert} from '../../../test/test-utils';
+import {NumericChangeId} from '../../../types/common';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {GrSearchAutocomplete} from '../../core/gr-search-autocomplete/gr-search-autocomplete';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+import {testResolver} from '../../../test/common-test-setup';
+
+suite('gr-create-flow tests', () => {
+  let element: GrCreateFlow;
+  let flowsModel: FlowsModel;
+
+  setup(async () => {
+    flowsModel = testResolver(flowsModelToken);
+    element = await fixture<GrCreateFlow>(
+      html`<gr-create-flow></gr-create-flow>`
+    );
+    element.changeNum = 123 as NumericChangeId;
+    await element.updateComplete;
+    element.hostUrl =
+      'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321';
+  });
+
+  test('renders initially', () => {
+    assert.isDefined(queryAndAssert(element, 'gr-search-autocomplete'));
+    assert.isDefined(
+      queryAndAssert(element, 'md-outlined-text-field[label="Action"]')
+    );
+    assert.isDefined(
+      queryAndAssert(element, 'md-outlined-text-field[label="Parameters"]')
+    );
+    assert.isDefined(
+      queryAndAssert(element, 'gr-button[aria-label="Add Stage"]')
+    );
+    assert.isDefined(
+      queryAndAssert(element, 'gr-button[aria-label="Create Flow"]')
+    );
+  });
+
+  test('adds and removes stages', async () => {
+    const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
+      element,
+      'gr-search-autocomplete'
+    );
+    const actionInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field[label="Action"]'
+    );
+    const addButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Add Stage"]'
+    );
+
+    searchAutocomplete.value = 'cond 1';
+    await element.updateComplete;
+    actionInput.value = 'act 1';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+    addButton.click();
+    await element.updateComplete;
+
+    assert.deepEqual(element['stages'], [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 1',
+        action: 'act 1',
+        parameterStr: '',
+      },
+    ]);
+    assert.equal(element['currentCondition'], '');
+    assert.equal(element['currentAction'], '');
+
+    searchAutocomplete.value = 'cond 2';
+    await element.updateComplete;
+    actionInput.value = 'act 2';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+    addButton.click();
+    await element.updateComplete;
+
+    assert.deepEqual(element['stages'], [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 1',
+        action: 'act 1',
+        parameterStr: '',
+      },
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 2',
+        action: 'act 2',
+        parameterStr: '',
+      },
+    ]);
+
+    let removeButtons = queryAll<GrButton>(element, 'tr gr-button');
+    assert.lengthOf(removeButtons, 2);
+
+    removeButtons[0].click();
+    await element.updateComplete;
+
+    assert.deepEqual(element['stages'], [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 2',
+        action: 'act 2',
+        parameterStr: '',
+      },
+    ]);
+    removeButtons = queryAll<GrButton>(element, 'tr gr-button');
+    assert.lengthOf(removeButtons, 1);
+  });
+
+  test('creates a flow with one stage', async () => {
+    const createFlowStub = sinon.stub(flowsModel, 'createFlow');
+
+    const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
+      element,
+      'gr-search-autocomplete'
+    );
+    const actionInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field[label="Action"]'
+    );
+    searchAutocomplete.value = 'single condition';
+    await element.updateComplete;
+    actionInput.value = 'single action';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+
+    const createButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Create Flow"]'
+    );
+    createButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(createFlowStub.calledOnce);
+    const flowInput = createFlowStub.lastCall.args[0];
+    assert.deepEqual(flowInput.stage_expressions, [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is single condition',
+        action: {name: 'single action'},
+      },
+    ]);
+  });
+
+  test('creates a flow with parameters', async () => {
+    const createFlowStub = sinon.stub(flowsModel, 'createFlow');
+
+    const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
+      element,
+      'gr-search-autocomplete'
+    );
+    const actionInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field[label="Action"]'
+    );
+    const parametersInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field[label="Parameters"]'
+    );
+    searchAutocomplete.value = 'single condition';
+    await element.updateComplete;
+    actionInput.value = 'single action';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+    parametersInput.value = 'param1 param2';
+    parametersInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+
+    const createButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Create Flow"]'
+    );
+    createButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(createFlowStub.calledOnce);
+    const flowInput = createFlowStub.lastCall.args[0];
+    assert.deepEqual(flowInput.stage_expressions, [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is single condition',
+        action: {name: 'single action', parameters: ['param1', 'param2']},
+      },
+    ]);
+  });
+
+  test('creates a flow with multiple stages', async () => {
+    const createFlowStub = sinon.stub(flowsModel, 'createFlow');
+
+    const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
+      element,
+      'gr-search-autocomplete'
+    );
+    const actionInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field[label="Action"]'
+    );
+    const addButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Add Stage"]'
+    );
+
+    searchAutocomplete.value = 'cond 1';
+    await element.updateComplete;
+    actionInput.value = 'act 1';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+    addButton.click();
+    await element.updateComplete;
+
+    searchAutocomplete.value = 'cond 2';
+    await element.updateComplete;
+    actionInput.value = 'act 2';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+    addButton.click();
+    await element.updateComplete;
+
+    const createButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Create Flow"]'
+    );
+    createButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(createFlowStub.calledOnce);
+    const flowInput = createFlowStub.lastCall.args[0];
+    assert.deepEqual(flowInput.stage_expressions, [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 1',
+        action: {name: 'act 1'},
+      },
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 2',
+        action: {name: 'act 2'},
+      },
+    ]);
+  });
+
+  test('create flow with added stages and current input', async () => {
+    const createFlowStub = sinon.stub(flowsModel, 'createFlow');
+
+    const searchAutocomplete = queryAndAssert<GrSearchAutocomplete>(
+      element,
+      'gr-search-autocomplete'
+    );
+    const actionInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field[label="Action"]'
+    );
+    const addButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Add Stage"]'
+    );
+
+    searchAutocomplete.value = 'cond 1';
+    await element.updateComplete;
+    actionInput.value = 'act 1';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+    addButton.click();
+    await element.updateComplete;
+    searchAutocomplete.value = 'cond 2';
+    await element.updateComplete;
+    actionInput.value = 'act 2';
+    actionInput.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+
+    const createButton = queryAndAssert<GrButton>(
+      element,
+      'gr-button[aria-label="Create Flow"]'
+    );
+    createButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(createFlowStub.calledOnce);
+    const flowInput = createFlowStub.lastCall.args[0];
+    assert.deepEqual(flowInput.stage_expressions, [
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 1',
+        action: {name: 'act 1'},
+      },
+      {
+        condition:
+          'https://gerrit-review.googlesource.com/c/plugins/code-owners/+/441321 is cond 2',
+        action: {name: 'act 2'},
+      },
+    ]);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts
new file mode 100644
index 0000000..e272eda
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-flows.ts
@@ -0,0 +1,323 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {customElement, query, state} from 'lit/decorators.js';
+import {css, html, LitElement, TemplateResult} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {grFormStyles} from '../../../styles/gr-form-styles';
+import {resolve} from '../../../models/dependency';
+import {changeModelToken} from '../../../models/change/change-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {FlowInfo, FlowStageState} from '../../../api/rest-api';
+import {flowsModelToken} from '../../../models/flows/flows-model';
+import {NumericChangeId} from '../../../types/common';
+import './gr-create-flow';
+import {when} from 'lit/directives/when.js';
+import '../../shared/gr-dialog/gr-dialog';
+import '@material/web/select/filled-select';
+import '@material/web/select/select-option';
+
+const iconForFlowStageState = (status: FlowStageState) => {
+  switch (status) {
+    case FlowStageState.DONE:
+      return {icon: 'check_circle', filled: true, class: 'done'};
+    case FlowStageState.PENDING:
+      return {icon: 'timelapse', filled: false, class: 'pending'};
+    case FlowStageState.FAILED:
+      return {icon: 'error', filled: true, class: 'failed'};
+    case FlowStageState.TERMINATED:
+      return {icon: 'error', filled: true, class: 'failed'};
+    default:
+      return {icon: 'help', filled: false, class: 'other'};
+  }
+};
+
+@customElement('gr-flows')
+export class GrFlows extends LitElement {
+  @query('#deleteFlowModal')
+  deleteFlowModal?: HTMLDialogElement;
+
+  @state() private flows: FlowInfo[] = [];
+
+  @state() private changeNum?: NumericChangeId;
+
+  @state() private loading = true;
+
+  @state() private flowIdToDelete?: string;
+
+  @state() private statusFilter: FlowStageState | 'all' = 'all';
+
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
+  private readonly getFlowsModel = resolve(this, flowsModelToken);
+
+  static override get styles() {
+    return [
+      sharedStyles,
+      grFormStyles,
+      css`
+        .container {
+          padding: var(--spacing-l);
+        }
+        hr {
+          margin-top: var(--spacing-l);
+          margin-bottom: var(--spacing-l);
+          border: 0;
+          border-top: 1px solid var(--border-color);
+        }
+        .flow {
+          border: 1px solid var(--border-color);
+          border-radius: var(--border-radius);
+          margin: var(--spacing-m) 0;
+          padding: var(--spacing-m);
+        }
+        .flow-id {
+          font-weight: var(--font-weight-bold);
+        }
+        .flow-header {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-bottom: var(--spacing-s);
+        }
+        .heading-with-button {
+          display: flex;
+          align-items: center;
+        }
+        .hidden {
+          display: none;
+        }
+        table {
+          border-collapse: collapse;
+        }
+        th,
+        td {
+          border: 1px solid var(--border-color);
+          padding: var(--spacing-s);
+          text-align: left;
+        }
+        .main-heading {
+          font-size: var(--font-size-h2);
+          font-weight: var(--font-weight-bold);
+          margin-bottom: var(--spacing-m);
+        }
+        gr-icon {
+          font-size: var(--line-height-normal, 20px);
+          vertical-align: middle;
+        }
+        gr-icon.done {
+          color: var(--success-foreground);
+        }
+        gr-icon.pending {
+          color: var(--deemphasized-text-color);
+        }
+        gr-icon.failed {
+          color: var(--error-foreground);
+        }
+        .owner-container {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-s);
+        }
+        .refresh {
+          top: -4px;
+        }
+      `,
+    ];
+  }
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getChangeModel().changeNum$,
+      changeNum => {
+        this.changeNum = changeNum;
+      }
+    );
+    subscribe(
+      this,
+      () => this.getFlowsModel().flows$,
+      flows => {
+        this.flows = flows;
+      }
+    );
+    subscribe(
+      this,
+      () => this.getFlowsModel().loading$,
+      loading => {
+        this.loading = loading;
+      }
+    );
+  }
+
+  private async deleteFlow() {
+    if (!this.flowIdToDelete) return;
+    await this.getFlowsModel().deleteFlow(this.flowIdToDelete);
+    this.closeConfirmDialog();
+  }
+
+  private openConfirmDialog(flowId: string) {
+    this.deleteFlowModal?.showModal();
+    this.flowIdToDelete = flowId;
+  }
+
+  private closeConfirmDialog() {
+    this.deleteFlowModal?.close();
+    this.flowIdToDelete = undefined;
+  }
+
+  override render() {
+    return html`
+      <div class="container">
+        <h2 class="main-heading">Create new flow</h2>
+        <gr-create-flow .changeNum=${this.changeNum}></gr-create-flow>
+        <hr />
+        ${this.renderFlowsList()}
+      </div>
+      ${this.renderDeleteFlowModal()}
+    `;
+  }
+
+  private renderDeleteFlowModal() {
+    return html` <dialog id="deleteFlowModal">
+      <gr-dialog
+        confirm-label="Delete"
+        @confirm=${() => this.deleteFlow()}
+        @cancel=${() => this.closeConfirmDialog()}
+      >
+        <div class="header" slot="header">Delete Flow</div>
+        <div class="main" slot="main">
+          Are you sure you want to delete this flow?
+        </div>
+      </gr-dialog>
+    </dialog>`;
+  }
+
+  private renderStatus(stage: FlowInfo['stages'][0]): TemplateResult {
+    const icon = iconForFlowStageState(stage.state);
+    return html`<gr-icon
+      class=${icon.class}
+      icon=${icon.icon}
+      ?filled=${icon.filled}
+      aria-label=${stage.state.toLowerCase()}
+      role="img"
+    ></gr-icon>`;
+  }
+
+  private renderFlowsList() {
+    if (this.loading) {
+      return html`<p>Loading...</p>`;
+    }
+    if (this.flows.length === 0) {
+      return html`<p>No flows found for this change.</p>`;
+    }
+    const filteredFlows = this.flows.filter(flow => {
+      if (this.statusFilter === 'all') return true;
+      const lastStage = flow.stages[flow.stages.length - 1];
+      return lastStage.state === this.statusFilter;
+    });
+
+    return html`
+      <div>
+        <div class="heading-with-button">
+          <h2 class="main-heading">Existing Flows</h2>
+          <gr-button
+            link
+            @click=${() => this.getFlowsModel().reload()}
+            aria-label="Refresh flows"
+            title="Refresh flows"
+            class="refresh"
+          >
+            <gr-icon icon="refresh"></gr-icon>
+          </gr-button>
+        </div>
+        <md-filled-select
+          label="Filter by status"
+          @request-selection=${(e: CustomEvent) => {
+            this.statusFilter = (e.target as HTMLSelectElement).value as
+              | FlowStageState
+              | 'all';
+          }}
+        >
+          <md-select-option value="all">
+            <div slot="headline">All</div>
+          </md-select-option>
+          ${Object.values(FlowStageState).map(
+            status => html`
+              <md-select-option value=${status}>
+                <div slot="headline">${status}</div>
+              </md-select-option>
+            `
+          )}
+        </md-filled-select>
+        ${filteredFlows.map(
+          (flow: FlowInfo) => html`
+            <div class="flow">
+              <div class="flow-header">
+                <gr-button
+                  link
+                  @click=${() => this.openConfirmDialog(flow.uuid)}
+                  title="Delete flow"
+                >
+                  <gr-icon icon="delete" filled></gr-icon>
+                </gr-button>
+              </div>
+              <div class="flow-id hidden">Flow ${flow.uuid}</div>
+              <div>
+                Created:
+                <gr-date-formatter withTooltip .dateStr=${flow.created}>
+                </gr-date-formatter>
+              </div>
+              ${when(
+                flow.last_evaluated,
+                () =>
+                  html` <div>
+                    Last Evaluated:
+                    <gr-date-formatter
+                      withTooltip
+                      .dateStr=${flow.last_evaluated}
+                    >
+                    </gr-date-formatter>
+                  </div>`
+              )}
+              <table>
+                <thead>
+                  <tr>
+                    <th>Status</th>
+                    <th>Condition</th>
+                    <th>Action</th>
+                    <th>Parameters</th>
+                    <th>Message</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  ${flow.stages.map(stage => {
+                    const action = stage.expression.action;
+                    return html`
+                      <tr>
+                        <td>${this.renderStatus(stage)}</td>
+                        <td>${stage.expression.condition}</td>
+                        <td>${action ? action.name : ''}</td>
+                        <td>${action ? action.parameters : ''}</td>
+                        <td>${stage.message ?? ''}</td>
+                      </tr>
+                    `;
+                  })}
+                </tbody>
+              </table>
+            </div>
+          `
+        )}
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-flows': GrFlows;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
new file mode 100644
index 0000000..b21b090
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
@@ -0,0 +1,437 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-flows';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrFlows} from './gr-flows';
+import {FlowInfo, FlowStageState, Timestamp} from '../../../api/rest-api';
+import {queryAndAssert} from '../../../test/test-utils';
+import {NumericChangeId} from '../../../types/common';
+import sinon from 'sinon';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+import {testResolver} from '../../../test/common-test-setup';
+
+suite('gr-flows tests', () => {
+  let element: GrFlows;
+  let clock: sinon.SinonFakeTimers;
+  let flowsModel: FlowsModel;
+
+  setup(async () => {
+    clock = sinon.useFakeTimers();
+
+    flowsModel = testResolver(flowsModelToken);
+    // The model is created by the DI system. The test setup replaces the real
+    // model with a mock. To prevent real API calls, we stub the reload method.
+    sinon.stub(flowsModel, 'reload');
+
+    element = await fixture<GrFlows>(html`<gr-flows></gr-flows>`);
+    element['changeNum'] = 123 as NumericChangeId;
+    await element.updateComplete;
+  });
+
+  teardown(() => {
+    clock.restore();
+  });
+
+  test('renders create flow component and no flows', async () => {
+    flowsModel.setState({flows: [], loading: false});
+    await element.updateComplete;
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <div class="container">
+          <h2 class="main-heading">Create new flow</h2>
+          <gr-create-flow></gr-create-flow>
+          <hr />
+          <p>No flows found for this change.</p>
+        </div>
+        <dialog id="deleteFlowModal">
+          <gr-dialog confirm-label="Delete">
+            <div class="header" slot="header">Delete Flow</div>
+            <div class="main" slot="main">
+              Are you sure you want to delete this flow?
+            </div>
+          </gr-dialog>
+        </dialog>
+      `,
+      {ignoreAttributes: ['role']}
+    );
+  });
+
+  test('renders flows', async () => {
+    const flows: FlowInfo[] = [
+      {
+        uuid: 'flow1',
+        owner: {name: 'owner1'},
+        created: '2025-01-01T10:00:00.000Z' as Timestamp,
+        last_evaluated: '2025-01-01T11:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {condition: 'label:Code-Review=+1'},
+            state: FlowStageState.DONE,
+          },
+        ],
+      },
+      {
+        uuid: 'flow2',
+        owner: {name: 'owner2'},
+        created: '2025-01-02T10:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {
+              condition: 'label:Verified=+1',
+              action: {name: 'submit'},
+            },
+            state: FlowStageState.PENDING,
+          },
+        ],
+      },
+    ];
+    flowsModel.setState({flows, loading: false});
+    await element.updateComplete;
+
+    // prettier formats the spacing for "last evaluated" incorrectly
+    assert.shadowDom.equal(
+      element,
+      /* prettier-ignore */ /* HTML */ `
+        <div class="container">
+          <h2 class="main-heading">Create new flow</h2>
+          <gr-create-flow></gr-create-flow>
+          <hr />
+          <div>
+            <div class="heading-with-button">
+              <h2 class="main-heading">Existing Flows</h2>
+              <gr-button
+                aria-label="Refresh flows"
+                link=""
+                title="Refresh flows"
+              >
+                <gr-icon icon="refresh"></gr-icon>
+              </gr-button>
+            </div>
+            <md-filled-select label="Filter by status">
+              <md-select-option value="all">
+                <div slot="headline">All</div>
+              </md-select-option>
+              <md-select-option value="DONE">
+                <div slot="headline">DONE</div>
+              </md-select-option>
+              <md-select-option value="FAILED">
+                <div slot="headline">FAILED</div>
+              </md-select-option>
+              <md-select-option value="PENDING">
+                <div slot="headline">PENDING</div>
+              </md-select-option>
+              <md-select-option value="TERMINATED">
+                <div slot="headline">TERMINATED</div>
+              </md-select-option>
+            </md-filled-select>
+            <div class="flow">
+              <div class="flow-header">
+                <gr-button link title="Delete flow">
+                  <gr-icon icon="delete" filled></gr-icon>
+                </gr-button>
+              </div>
+              <div class="flow-id hidden">Flow flow1</div>
+              <div>
+                Created:
+                <gr-date-formatter withtooltip></gr-date-formatter>
+              </div>
+              <div>
+                Last Evaluated:
+                <gr-date-formatter withtooltip></gr-date-formatter>
+              </div>
+              <table>
+                <thead>
+                  <tr>
+                    <th>Status</th>
+                    <th>Condition</th>
+                    <th>Action</th>
+                    <th>Parameters</th>
+                    <th>Message</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr>
+                    <td>
+                      <gr-icon
+                        aria-label="done"
+                        filled
+                        icon="check_circle"
+                      ></gr-icon>
+                    </td>
+                    <td>label:Code-Review=+1</td>
+                    <td></td>
+                    <td></td>
+                    <td></td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+            <div class="flow">
+              <div class="flow-header">
+                <gr-button link title="Delete flow">
+                  <gr-icon icon="delete" filled></gr-icon>
+                </button>
+              </div>
+              <div class="flow-id hidden">Flow flow2</div>
+              <div>
+                Created:
+                <gr-date-formatter withtooltip></gr-date-formatter>
+              </div>
+              <table>
+                <thead>
+                  <tr>
+                    <th>Status</th>
+                    <th>Condition</th>
+                    <th>Action</th>
+                    <th>Parameters</th>
+                    <th>Message</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr>
+                    <td>
+                      <gr-icon aria-label="pending" icon="timelapse"></gr-icon>
+                    </td>
+                    <td>label:Verified=+1</td>
+                    <td>submit</td>
+                     <td></td>
+                    <td></td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+        <dialog id="deleteFlowModal">
+          <gr-dialog confirm-label="Delete">
+            <div class="header" slot="header">Delete Flow</div>
+            <div class="main" slot="main">
+              Are you sure you want to delete this flow?
+            </div>
+          </gr-dialog>
+        </dialog>
+      `,
+      {
+        ignoreAttributes: [
+          'style',
+          'class',
+          'account',
+          'changenum',
+          'datestr',
+          'aria-disabled',
+          'role',
+          'tabindex',
+          'md-menu-item',
+        ],
+      }
+    );
+  });
+
+  test('deletes a flow after confirmation', async () => {
+    const flows: FlowInfo[] = [
+      {
+        uuid: 'flow1',
+        owner: {name: 'owner1'},
+        created: '2025-01-01T10:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {condition: 'label:Code-Review=+1'},
+            state: FlowStageState.DONE,
+          },
+        ],
+      },
+    ];
+    const deleteFlowStub = sinon.stub(flowsModel, 'deleteFlow');
+    flowsModel.setState({flows, loading: false});
+    await element.updateComplete;
+
+    const deleteButton = queryAndAssert<GrButton>(element, '.flow gr-button');
+    deleteButton.click();
+    await element.updateComplete;
+
+    const dialog = queryAndAssert<HTMLDialogElement>(
+      element,
+      '#deleteFlowModal'
+    );
+    assert.isTrue(dialog.open);
+
+    const grDialog = queryAndAssert<GrDialog>(dialog, 'gr-dialog');
+    const confirmButton = queryAndAssert<GrButton>(grDialog, '#confirm');
+    confirmButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(deleteFlowStub.calledOnceWith('flow1'));
+  });
+
+  test('cancel deleting a flow', async () => {
+    const flows: FlowInfo[] = [
+      {
+        uuid: 'flow1',
+        owner: {name: 'owner1'},
+        created: '2025-01-01T10:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {condition: 'label:Code-Review=+1'},
+            state: FlowStageState.DONE,
+          },
+        ],
+      },
+    ];
+    const deleteFlowStub = sinon.stub(flowsModel, 'deleteFlow');
+    flowsModel.setState({flows, loading: false});
+    await element.updateComplete;
+
+    const deleteButton = queryAndAssert<GrButton>(element, '.flow gr-button');
+    deleteButton.click();
+    await element.updateComplete;
+
+    const dialog = queryAndAssert<HTMLDialogElement>(
+      element,
+      '#deleteFlowModal'
+    );
+    assert.isTrue(dialog.open);
+
+    const grDialog = queryAndAssert<GrDialog>(dialog, 'gr-dialog');
+    const cancelButton = queryAndAssert<GrButton>(grDialog, '#cancel');
+    cancelButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(deleteFlowStub.notCalled);
+    assert.isFalse(dialog.open);
+  });
+
+  test('refreshes flows on button click', async () => {
+    const flow = {
+      uuid: 'flow1',
+      owner: {name: 'owner1'},
+      created: '2025-01-01T10:00:00.000Z' as Timestamp,
+      stages: [],
+    } as FlowInfo;
+    flowsModel.setState({flows: [flow], loading: false});
+    await element.updateComplete;
+
+    const reloadStub = flowsModel.reload as sinon.SinonStub;
+    reloadStub.resetHistory();
+
+    const refreshButton = queryAndAssert<GrButton>(
+      element,
+      '.heading-with-button gr-button'
+    );
+    refreshButton.click();
+    await element.updateComplete;
+
+    assert.isTrue(reloadStub.calledOnce);
+  });
+
+  suite('filter', () => {
+    const flows: FlowInfo[] = [
+      {
+        uuid: 'flow-done',
+        owner: {name: 'owner1'},
+        created: '2025-01-01T10:00:00.000Z' as Timestamp,
+        stages: [
+          {expression: {condition: 'cond-done'}, state: FlowStageState.DONE},
+        ],
+      },
+      {
+        uuid: 'flow-pending',
+        owner: {name: 'owner2'},
+        created: '2025-01-02T10:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {condition: 'cond-pending'},
+            state: FlowStageState.PENDING,
+          },
+        ],
+      },
+      {
+        uuid: 'flow-failed',
+        owner: {name: 'owner3'},
+        created: '2025-01-03T10:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {condition: 'cond-failed'},
+            state: FlowStageState.FAILED,
+          },
+        ],
+      },
+      {
+        uuid: 'flow-terminated',
+        owner: {name: 'owner4'},
+        created: '2025-01-04T10:00:00.000Z' as Timestamp,
+        stages: [
+          {
+            expression: {condition: 'cond-terminated'},
+            state: FlowStageState.TERMINATED,
+          },
+        ],
+      },
+    ];
+
+    setup(async () => {
+      flowsModel.setState({flows, loading: false});
+      await element.updateComplete;
+    });
+
+    test('shows all flows by default', () => {
+      const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 4);
+    });
+
+    test('filters by DONE', async () => {
+      element['statusFilter'] = FlowStageState.DONE;
+      await element.updateComplete;
+
+      const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 1);
+      assert.include(flowElements[0].textContent, 'cond-done');
+    });
+
+    test('filters by PENDING', async () => {
+      element['statusFilter'] = FlowStageState.PENDING;
+      await element.updateComplete;
+
+      const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 1);
+      assert.include(flowElements[0].textContent, 'cond-pending');
+    });
+
+    test('filters by FAILED', async () => {
+      element['statusFilter'] = FlowStageState.FAILED;
+      await element.updateComplete;
+
+      const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 1);
+      assert.include(flowElements[0].textContent, 'cond-failed');
+    });
+
+    test('filters by TERMINATED', async () => {
+      element['statusFilter'] = FlowStageState.TERMINATED;
+      await element.updateComplete;
+
+      const flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 1);
+      assert.include(flowElements[0].textContent, 'cond-terminated');
+    });
+
+    test('shows all when filter is changed to all', async () => {
+      element['statusFilter'] = FlowStageState.DONE;
+      await element.updateComplete;
+      let flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 1);
+
+      element['statusFilter'] = 'all';
+      await element.updateComplete;
+
+      flowElements = element.shadowRoot!.querySelectorAll('.flow');
+      assert.equal(flowElements.length, 4);
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
index 8dda1891..b63fc77 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
@@ -3,20 +3,19 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
 import {IncludedInInfo, NumericChangeId} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
 import {fireNoBubble} from '../../../utils/event-util';
 import {resolve} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
 import {subscribe} from '../../lit/subscription-controller';
 import {formStyles} from '../../../styles/form-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 interface DisplayGroup {
   title: string;
@@ -45,6 +44,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       formStyles,
       fontStyles,
       sharedStyles,
@@ -120,15 +120,16 @@
             >Close</gr-button
           >
         </span>
-        <iron-input
+        <md-outlined-text-field
           id="filterInput"
-          .bindValue=${this.filterText}
-          @bind-value-changed=${(e: BindValueChangeEvent) => {
-            this.filterText = e.detail.value ?? '';
+          placeholder="Filter"
+          .value=${this.filterText ?? ''}
+          @input=${(e: InputEvent) => {
+            const target = e.target as HTMLInputElement;
+            this.filterText = target.value;
           }}
         >
-          <input placeholder="Filter" />
-        </iron-input>
+        </md-outlined-text-field>
       </header>
       ${this.renderLoading()}
       ${this.computeGroups().map(group => this.renderGroup(group))}
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
index 3851255..fce2e2c6 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
@@ -7,9 +7,9 @@
 import './gr-included-in-dialog';
 import {GrIncludedInDialog} from './gr-included-in-dialog';
 import {BranchName, IncludedInInfo, TagName} from '../../../types/common';
-import {IronInputElement} from '@polymer/iron-input';
 import {queryAndAssert} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-included-in-dialog', () => {
   let element: GrIncludedInDialog;
@@ -37,9 +37,8 @@
               Close
             </gr-button>
           </span>
-          <iron-input id="filterInput">
-            <input placeholder="Filter" />
-          </iron-input>
+          <md-outlined-text-field id="filterInput" placeholder="Filter">
+          </md-outlined-text-field>
         </header>
         <div>Loading...</div>
       `
@@ -92,8 +91,14 @@
   });
 
   test('computeGroups with .bindValue', async () => {
-    queryAndAssert<IronInputElement>(element, '#filterInput')!.bindValue =
-      'stable-3.2';
+    const filterInput = queryAndAssert<MdOutlinedTextField>(
+      element,
+      '#filterInput'
+    );
+    filterInput.value = 'stable-3.2';
+    filterInput.dispatchEvent(
+      new Event('input', {bubbles: true, composed: true})
+    );
     element.includedIn = {branches: [], tags: []} as IncludedInInfo;
     element.includedIn.branches.push(
       'master' as BranchName,
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 bfece63..50de8a7 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
@@ -3,25 +3,24 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-selector/iron-selector';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
-import {IronSelectorElement} from '@polymer/iron-selector/iron-selector';
 import {
+  DetailedLabelInfo,
   LabelNameToInfoMap,
   QuickLabelInfo,
-  DetailedLabelInfo,
 } from '../../../types/common';
 import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
 import {Label} from '../../../utils/label-util';
 import {LabelNameToValuesMap} from '../../../api/rest-api';
 import {fire} from '../../../utils/event-util';
 import {LabelsChangedDetail} from '../../../api/change-reply';
-
+import '../../shared/gr-selector/gr-selector';
+import {GrSelector} from '../../shared/gr-selector/gr-selector';
 declare global {
   interface HTMLElementTagNameMap {
     'gr-label-score-row': GrLabelScoreRow;
@@ -34,7 +33,7 @@
 @customElement('gr-label-score-row')
 export class GrLabelScoreRow extends LitElement {
   @query('#labelSelector')
-  labelSelector?: IronSelectorElement;
+  labelSelector?: GrSelector;
 
   @property({type: Object})
   label: Label | undefined | null;
@@ -88,10 +87,10 @@
           box-sizing: border-box;
           --vote-text-color: var(--vote-chip-unselected-text-color);
         }
-        gr-button.iron-selected {
+        gr-button.selected {
           --vote-text-color: var(--vote-chip-selected-text-color);
         }
-        gr-button::part(paper-button) {
+        gr-button::part(md-text-button) {
           padding: 0 var(--spacing-m);
           background-color: var(
             --button-background-color,
@@ -99,25 +98,25 @@
           );
           border-color: var(--vote-chip-unselected-outline-color);
         }
-        gr-button.iron-selected::part(paper-button) {
+        gr-button.selected::part(md-text-button) {
           border-color: transparent;
         }
         gr-button {
           --button-background-color: var(--vote-chip-unselected-color);
         }
-        gr-button[data-vote='max'].iron-selected {
+        gr-button[data-vote='max'].selected {
           --button-background-color: var(--vote-chip-selected-positive-color);
         }
-        gr-button[data-vote='positive'].iron-selected {
+        gr-button[data-vote='positive'].selected {
           --button-background-color: var(--vote-chip-selected-positive-color);
         }
-        gr-button[data-vote='neutral'].iron-selected {
+        gr-button[data-vote='neutral'].selected {
           --button-background-color: var(--vote-chip-selected-neutral-color);
         }
-        gr-button[data-vote='negative'].iron-selected {
+        gr-button[data-vote='negative'].selected {
           --button-background-color: var(--vote-chip-selected-negative-color);
         }
-        gr-button[data-vote='min'].iron-selected {
+        gr-button[data-vote='min'].selected {
           --button-background-color: var(--vote-chip-selected-negative-color);
         }
         gr-button > gr-tooltip-content {
@@ -181,16 +180,16 @@
 
   private renderLabelSelector() {
     return html`
-      <iron-selector
+      <gr-selector
         id="labelSelector"
         .attrForSelected=${'data-value'}
         selected=${ifDefined(this._computeLabelValue())}
-        @selected-item-changed=${this.setSelectedValueText}
+        @select=${this.setSelectedValueText}
         role="radiogroup"
         aria-labelledby="labelName"
       >
         ${this.renderPermittedLabels()}
-      </iron-selector>
+      </gr-selector>
     `;
   }
 
@@ -232,11 +231,11 @@
     `;
   }
 
-  get selectedItem(): IronSelectorElement | undefined {
+  get selectedItem(): GrSelector | undefined {
     if (!this.labelSelector) {
       return undefined;
     }
-    return this.labelSelector.selectedItem as IronSelectorElement;
+    return this.labelSelector.selectedItem as GrSelector;
   }
 
   get selectedValue() {
@@ -306,8 +305,8 @@
    * Private but used in tests.
    * Maps the label value to exactly one of: min, max, positive, negative,
    * neutral. Used for the 'data-vote' attribute, because we don't want to
-   * interfere with <iron-selector> using the 'class' attribute for setting
-   * 'iron-selected'.
+   * interfere with <gr-selector> using the 'class' attribute for setting
+   * 'selected'.
    */
   _computeVoteAttribute(value: number, index: number, totalItems: number) {
     if (value < 0 && index === 0) {
@@ -347,8 +346,7 @@
   private setSelectedValueText = (e: Event) => {
     // Needed because when the selected item changes, it first changes to
     // nothing and then to the new item.
-    const selectedItem = (e.target as IronSelectorElement)
-      .selectedItem as HTMLElement;
+    const selectedItem = (e.target as GrSelector).selectedItem as HTMLElement;
     if (!selectedItem) {
       return;
     }
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_screenshot_test.ts
new file mode 100644
index 0000000..39d681c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_screenshot_test.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-label-score-row';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrLabelScoreRow} from './gr-label-score-row';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+
+suite('gr-label-score-row screenshot tests', () => {
+  let element: GrLabelScoreRow;
+
+  setup(async () => {
+    element = await fixture<GrLabelScoreRow>(
+      html`<gr-label-score-row
+        .label=${{name: 'Code-Review', value: '+1'}}
+        .labels=${{
+          'Code-Review': {
+            values: {
+              '-2': 'This shall not be merged',
+              '-1': 'I would prefer this is not merged as is',
+              ' 0': 'No score',
+              '+1': 'Looks good to me, but someone else must approve',
+              '+2': 'This looks good to me, approved',
+            },
+          },
+        }}
+        .permittedLabels=${{
+          'Code-Review': ['-2', '-1', ' 0', '+1', '+2'],
+        }}
+        .orderedLabelValues=${[-2, -1, 0, 1, 2]}
+      ></gr-label-score-row>`
+    );
+    await element.updateComplete;
+  });
+
+  test('label score row screenshot', async () => {
+    // Create a container with a fixed width to stabilize the component's dimensions.
+    const container = document.createElement('div');
+
+    container.style.width = '392px';
+    container.style.display = 'inline-block';
+    container.appendChild(element);
+
+    document.body.appendChild(container);
+
+    await visualDiff(container, 'gr-label-score-row');
+    await visualDiffDarkTheme(container, 'gr-label-score-row');
+
+    document.body.removeChild(container);
+  });
+});
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 7129247..77961a8 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
@@ -9,7 +9,7 @@
 import {GrLabelScoreRow} from './gr-label-score-row';
 import {AccountId} from '../../../api/rest-api';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {waitEventLoop} from '../../../test/test-utils';
 
 suite('gr-label-row-score tests', () => {
@@ -75,9 +75,9 @@
     const items = element.labelSelector!.items;
     assert.ok(items);
     const selectedItem = element.selectedItem;
-    for (let i = 0; i < items!.length; i++) {
-      const item = items![i];
-      if (items![i] === selectedItem) {
+    for (let i = 0; i < items.length; i++) {
+      const item = items[i];
+      if (items[i] === selectedItem) {
         assert.isTrue(item.hasAttribute('aria-checked'), `item ${i}`);
         assert.equal(item.getAttribute('aria-checked'), 'true', `item ${i}`);
       } else {
@@ -95,6 +95,7 @@
     ) as GrButton;
     button.click();
     await element.updateComplete;
+    await element.updateComplete;
 
     assert.strictEqual(element.selectedValue, '-1');
     assert.strictEqual(element.selectedItem!.textContent!.trim(), '-1');
@@ -197,54 +198,13 @@
     assert.deepEqual(element.computeBlankItemsCount('start'), 0);
   });
 
-  test('changes in label score are reflected in the DOM', async () => {
-    element.labels = {
-      'Code-Review': {
-        values: {
-          '0': 'No score',
-          '+1': 'good',
-          '+2': 'excellent',
-          '-1': 'bad',
-          '-2': 'terrible',
-        },
-        default_value: 0,
-      },
-      Verified: {
-        values: {
-          ' 0': 'No score',
-          '+1': 'good',
-          '+2': 'excellent',
-          '-1': 'bad',
-          '-2': 'terrible',
-        },
-        default_value: 0,
-      },
-    };
-    // For some reason we need the element.labels to flush first before we
-    // change the element.label
-    await element.updateComplete;
-
-    element.label = {name: 'Verified', value: ' 0'};
-    await element.updateComplete;
-    // Wait for @selected-item-changed to fire
-    await waitEventLoop();
-
-    const selector = element.labelSelector;
-    assert.strictEqual(selector!.selected, ' 0');
-    const selectedValueLabel = element.shadowRoot!.querySelector(
-      '#selectedValueLabel'
-    );
-    assert.strictEqual(selectedValueLabel!.textContent!.trim(), 'Reset Vote');
-    checkAriaCheckedValid();
-  });
-
   test('asymmetrical labels', async () => {
     element.permittedLabels = {
       'Code-Review': ['-2', '-1', ' 0', '+1', '+2'],
       Verified: [' 0', '+1'],
     };
     await element.updateComplete;
-    assert.strictEqual(element.labelSelector!.items!.length, 2);
+    assert.strictEqual(element.labelSelector!.items.length, 2);
     assert.strictEqual(
       element.shadowRoot!.querySelectorAll('.placeholder').length,
       3
@@ -255,7 +215,7 @@
       Verified: ['-2', '-1', ' 0', '+1', '+2'],
     };
     await element.updateComplete;
-    assert.strictEqual(element.labelSelector!.items!.length, 5);
+    assert.strictEqual(element.labelSelector!.items.length, 5);
     assert.strictEqual(
       element.shadowRoot!.querySelectorAll('.placeholder').length,
       0
@@ -320,7 +280,7 @@
         </span>
         <div class="buttonsCell">
           <span class="placeholder" data-label="Verified"></span>
-          <iron-selector
+          <gr-selector
             aria-labelledby="labelName"
             id="labelSelector"
             role="radiogroup"
@@ -330,6 +290,7 @@
               aria-disabled="false"
               aria-label="-1"
               data-name="Verified"
+              data-selection-index="0"
               data-value="-1"
               role="button"
               tabindex="0"
@@ -346,6 +307,7 @@
               aria-disabled="false"
               aria-label=" 0"
               data-name="Verified"
+              data-selection-index="1"
               data-value=" 0"
               role="button"
               tabindex="0"
@@ -366,8 +328,9 @@
               aria-checked="true"
               aria-disabled="false"
               aria-label="+1"
-              class="iron-selected"
+              class="selected"
               data-name="Verified"
+              data-selection-index="2"
               data-value="+1"
               role="button"
               tabindex="0"
@@ -380,7 +343,7 @@
                 +1
               </gr-tooltip-content>
             </gr-button>
-          </iron-selector>
+          </gr-selector>
           <span class="placeholder" data-label="Verified"></span>
         </div>
         <div class="selectedValueCell ">
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 0ebda8b..8c3bd77 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -5,21 +5,21 @@
  */
 import '../gr-label-score-row/gr-label-score-row';
 import '../../../styles/shared-styles';
-import {LitElement, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {
-  ChangeInfo,
   AccountInfo,
+  ChangeInfo,
   LabelNameToValueMap,
 } from '../../../types/common';
 import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
 import {
-  getTriggerVotes,
   computeLabels,
-  Label,
   computeOrderedLabelValues,
-  getDefaultValue,
   getApplicableLabels,
+  getDefaultValue,
+  getTriggerVotes,
+  Label,
 } from '../../../utils/label-util';
 import {ChangeStatus} from '../../../constants/constants';
 import {fontStyles} from '../../../styles/gr-font-styles';
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 990a7e3..1de151e 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
@@ -20,7 +20,7 @@
 } from '../../../test/test-data-generators';
 import {ChangeStatus} from '../../../constants/constants';
 import {getVoteForAccount} from '../../../utils/label-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-label-scores tests', () => {
   const accountId = 123 as AccountId;
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
index 09ca036..2d76e47 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
@@ -4,26 +4,18 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../gr-trigger-vote/gr-trigger-vote';
-import {LitElement, css, html} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {ChangeInfo} from '../../../api/rest-api';
-import {
-  LabelExtreme,
-  PATCH_SET_PREFIX_PATTERN,
-} from '../../../utils/comment-util';
-import {hasOwnProperty} from '../../../utils/common-util';
+import '../../checks/gr-checks-chip-for-label';
+import {css, html, LitElement, nothing} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {ChangeInfo, PatchSetNumber} from '../../../api/rest-api';
+import {LabelExtreme} from '../../../utils/comment-util';
 import {getTriggerVotes} from '../../../utils/label-util';
 import {ChangeMessage} from '../../../types/common';
-
-const VOTE_RESET_TEXT = '0 (vote reset)';
-
-interface Score {
-  label?: string;
-  value?: string;
-}
-
-export const LABEL_TITLE_SCORE_PATTERN =
-  /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.:]?$/;
+import {CheckRun} from '../../../api/checks';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {changeModelToken} from '../../../models/change/change-model';
+import {getScores, Score, VOTE_RESET_TEXT} from '../../../utils/message-util';
 
 @customElement('gr-message-scores')
 export class GrMessageScores extends LitElement {
@@ -36,6 +28,10 @@
   @property({type: Object})
   change?: ChangeInfo;
 
+  @state() runs: CheckRun[] = [];
+
+  @state() latestPatchNum?: PatchSetNumber;
+
   static override get styles() {
     return css`
       .score,
@@ -86,11 +82,28 @@
           min-width: 0px;
         }
       }
+
+      gr-checks-chip-for-label {
+        /* .checksChip has top: 2px, this is canceling it */
+        position: relative;
+        top: -2px;
+      }
     `;
   }
 
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getChangeModel().latestPatchNum$,
+      x => (this.latestPatchNum = x)
+    );
+  }
+
   override render() {
-    const scores = this._getScores(this.message, this.labelExtremes);
+    const scores = getScores(this.message, this.labelExtremes);
     const triggerVotes = getTriggerVotes(this.change);
     return scores.map(score => this.renderScore(score, triggerVotes));
   }
@@ -113,10 +126,22 @@
       </gr-trigger-vote>`;
     }
     return html`<span
-      class="score ${this._computeScoreClass(score, this.labelExtremes)}"
-    >
-      ${score.label} ${score.value}
-    </span>`;
+        class="score ${this._computeScoreClass(score, this.labelExtremes)}"
+      >
+        ${score.label} ${score.value} </span
+      >${this.renderChecks(score)}`;
+  }
+
+  renderChecks(score: Score) {
+    const labelName = score.label;
+    if (!labelName) return nothing;
+    if (Number(score.value) >= 0) return nothing;
+    if (this.latestPatchNum !== this.message?._revision_number) return nothing;
+
+    return html`<gr-checks-chip-for-label
+      .labels=${[labelName]}
+      .showRunning=${false}
+    ></gr-checks-chip-for-label>`;
   }
 
   _computeScoreClass(score?: Score, labelExtremes?: LabelExtreme) {
@@ -148,32 +173,6 @@
     }
     return classes.join(' ');
   }
-
-  _getScores(message?: ChangeMessage, labelExtremes?: LabelExtreme): Score[] {
-    if (!message || !message.message || !labelExtremes) {
-      return [];
-    }
-    const line = message.message.split('\n', 1)[0];
-    const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
-    if (!line.match(patchSetPrefix)) {
-      return [];
-    }
-    const scoresRaw = line.split(patchSetPrefix)[1];
-    if (!scoresRaw) {
-      return [];
-    }
-    return scoresRaw
-      .split(' ')
-      .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
-      .filter(
-        ms => ms && ms.length === 4 && hasOwnProperty(labelExtremes, ms[2])
-      )
-      .map(ms => {
-        const label = ms?.[2];
-        const value = ms?.[1] === '-' ? VOTE_RESET_TEXT : ms?.[3];
-        return {label, value};
-      });
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
index a757b37..48c3dfa 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
@@ -12,7 +12,8 @@
 } from '../../../test/test-data-generators';
 import {queryAll, stubFlags} from '../../../test/test-utils';
 import {GrMessageScores} from './gr-message-scores';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {PatchSetNumber} from '../../../api/rest-api';
 
 suite('gr-message-score tests', () => {
   let element: GrMessageScores;
@@ -40,6 +41,7 @@
       /* HTML */ `
         <span class="max positive score"> Verified +1 </span>
         <span class="min negative score"> Code-Review -2 </span>
+        <gr-checks-chip-for-label></gr-checks-chip-for-label>
         <span class="positive score"> Trybot-Label3 +1 </span>
       `
     );
@@ -192,4 +194,28 @@
       `
     );
   });
+
+  test('shows checks chip', async () => {
+    element.message = {
+      ...createChangeMessage(),
+      author: {},
+      expanded: false,
+      message: 'Patch Set 1: Verified-1',
+      _revision_number: 1 as PatchSetNumber,
+    };
+    element.labelExtremes = {
+      Verified: {max: 1, min: -1},
+    };
+    element.latestPatchNum = 1 as PatchSetNumber;
+
+    await element.updateComplete;
+
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <span class="min negative score"> Verified -1 </span
+        ><gr-checks-chip-for-label></gr-checks-chip-for-label>
+      `
+    );
+  });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index af063a5..e55631a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -15,25 +15,24 @@
 import {customElement, property, state} from 'lit/decorators.js';
 import {hasOwnProperty} from '../../../utils/common-util';
 import {
-  ChangeInfo,
-  ServerInfo,
-  ReviewInputTag,
-  NumericChangeId,
-  ChangeMessageId,
-  RevisionPatchSetNum,
   AccountInfo,
   BasePatchSetNum,
-  LabelNameToInfoMap,
-  CommentThread,
+  ChangeInfo,
   ChangeMessage,
+  ChangeMessageId,
+  CommentThread,
+  LabelNameToInfoMap,
+  NumericChangeId,
+  ReviewInputTag,
+  RevisionPatchSetNum,
+  ServerInfo,
 } from '../../../types/common';
 import {
   isFormattedReviewerUpdate,
+  isUnresolved,
   LabelExtreme,
   PATCH_SET_PREFIX_PATTERN,
-  isUnresolved,
 } from '../../../utils/comment-util';
-import {LABEL_TITLE_SCORE_PATTERN} from '../gr-message-scores/gr-message-scores';
 import {getAppContext} from '../../../services/app-context';
 import {pluralize} from '../../../utils/string-util';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -53,6 +52,7 @@
 import {configModelToken} from '../../../models/config/config-model';
 import {userModelToken} from '../../../models/user/user-model';
 import {subscribe} from '../../lit/subscription-controller';
+import {LABEL_TITLE_SCORE_PATTERN} from '../../../utils/message-util';
 
 const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
 const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index afd8ac2..23d2867 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -15,9 +15,9 @@
   createChange,
   createChangeMessage,
   createComment,
-  createRevisions,
-  createLabelInfo,
   createCommentThread,
+  createLabelInfo,
+  createRevisions,
 } from '../../../test/test-data-generators';
 import {
   mockPromise,
@@ -31,18 +31,18 @@
   ChangeMessageId,
   EmailAddress,
   NumericChangeId,
-  RevisionPatchSetNum,
   ReviewInputTag,
+  RevisionPatchSetNum,
+  SavingState,
   Timestamp,
   UrlEncodedCommentId,
-  SavingState,
 } from '../../../types/common';
 import {ChangeMessageDeletedEventDetail} from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {CommentSide} from '../../../constants/constants';
 import {SinonStubbedMember} from 'sinon';
 import {html} from 'lit';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-message tests', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index f8b5c02..03c79a8 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -3,32 +3,30 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-toggle-button/paper-toggle-button';
+import '@material/web/switch/switch';
 import '../../shared/gr-button/gr-button';
 import '../gr-message/gr-message';
-import '../../../styles/gr-paper-styles';
+import '../../../styles/gr-material-styles';
 import {parseDate} from '../../../utils/date-util';
 import {MessageTag} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
 import {customElement, property, state} from 'lit/decorators.js';
 import {
-  ChangeId,
   ChangeMessageId,
   ChangeMessageInfo,
   CommentThread,
   LabelNameToInfoMap,
   NumericChangeId,
   PatchSetNum,
-  VotingRangeInfo,
-  isRobot,
   PatchSetNumber,
+  VotingRangeInfo,
 } from '../../../types/common';
 import {GrMessage, MessageAnchorTapDetail} from '../gr-message/gr-message';
 import {getVotingRange} from '../../../utils/label-util';
 import {
   FormattedReviewerUpdateInfo,
-  ParsedChangeInfo,
   isPatchSetNumber,
+  ParsedChangeInfo,
 } from '../../../types/types';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {changeModelToken} from '../../../models/change/change-model';
@@ -37,7 +35,7 @@
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {subscribe} from '../../lit/subscription-controller';
-import {paperStyles} from '../../../styles/gr-paper-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {when} from 'lit/directives/when.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {
@@ -47,6 +45,7 @@
 } from '../../../services/shortcuts/shortcuts-service';
 import {GrFormattedText} from '../../shared/gr-formatted-text/gr-formatted-text';
 import {waitUntil} from '../../../utils/async-util';
+import {MdSwitch} from '@material/web/switch/switch';
 
 /**
  * The content of the enum is also used in the UI for the button text.
@@ -102,27 +101,18 @@
  * a message is initially hidden or not, see isImportant(). So we are applying
  * some "magic" rules here in order to hide exactly the right messages.
  *
- * 1. If a message does not have a tag, but is associated with robot comments,
- * then it gets a tag.
- *
- * 2. Use the same tag for some of Gerrit's standard events, if they should be
+ * 1. Use the same tag for some of Gerrit's standard events, if they should be
  * considered one group, e.g. normal and wip patchset uploads.
  *
- * 3. Everything beyond the ~ character is cut off from the tag. That gives
+ * 2. Everything beyond the ~ character is cut off from the tag. That gives
  * tools control over which messages will be hidden.
  *
- * 4. (Non-WIP) patchset uploads get a separate tag when they invalidate any
+ * 3. (Non-WIP) patchset uploads get a separate tag when they invalidate any
  * votes.
  */
 function computeTag(message: CombinedMessage) {
   if (!message.tag) {
-    const threads = message.commentThreads || [];
-    const messageId = getMessageId(message);
-    const comments = threads.map(t =>
-      t.comments.find(c => c.change_message_id === messageId)
-    );
-    const hasRobotComments = comments.some(isRobot);
-    return hasRobotComments ? 'autogenerated:has-robot-comments' : undefined;
+    return undefined;
   }
 
   if (message.tag === MessageTag.TAG_NEW_PATCHSET) {
@@ -261,7 +251,7 @@
   static override get styles() {
     return [
       sharedStyles,
-      paperStyles,
+      materialStyles,
       css`
         :host {
           display: flex;
@@ -312,7 +302,7 @@
   private change?: ParsedChangeInfo;
 
   @state()
-  private changeNum?: ChangeId | NumericChangeId;
+  private changeNum?: NumericChangeId;
 
   @state()
   private commentThreads: CommentThread[] = [];
@@ -385,10 +375,10 @@
           .change=${this.change}
           .changeNum=${this.changeNum}
           .message=${message}
-          .commentThreads=${message.commentThreads}
+          .commentThreads=${message.commentThreads ?? []}
           @message-anchor-tap=${this.handleAnchorClick}
           .labelExtremes=${labelExtremes}
-          data-message-id=${ifDefined(getMessageId(message) as String)}
+          data-message-id=${ifDefined(getMessageId(message) as string)}
         ></gr-message>`
       )}`;
   }
@@ -399,15 +389,13 @@
         ${when(
           this.combinedMessages.some(m => !m.isImportant),
           () => html`
-            <paper-toggle-button
+            <md-switch
               class="showAllActivityToggle"
-              ?checked=${this.showAllActivity}
+              ?selected=${this.showAllActivity}
               @change=${this.handleShowAllActivityChanged}
               aria-labelledby="showAllEntriesLabel"
               role="switch"
-              @click=${this.onTapShowAllActivityToggle}
-              @tap=${this.onTapShowAllActivityToggle}
-            ></paper-toggle-button>
+            ></md-switch>
             <div id="showAllEntriesLabel" aria-hidden="true">
               <span>Show all entries</span>
               <span class="hiddenEntries" ?hidden=${this.showAllActivity}>
@@ -463,7 +451,7 @@
   }
 
   private handleShowAllActivityChanged(e: Event) {
-    this.showAllActivity = (e.target as HTMLInputElement).checked ?? false;
+    this.showAllActivity = (e.target as MdSwitch).selected ?? false;
   }
 
   private refreshMessages() {
@@ -564,13 +552,6 @@
     }
     return extremes;
   }
-
-  /**
-   * Work around a issue on iOS when clicking turns into double tap
-   */
-  private onTapShowAllActivityToggle(e: Event) {
-    e.preventDefault();
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index d4c7b63..d5f4e52 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -30,9 +30,9 @@
 } from '../../../types/common';
 import {assertIsDefined, uuid} from '../../../utils/common-util';
 import {html} from 'lit';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
+import {MdSwitch} from '@material/web/switch/switch';
 import {testResolver} from '../../../test/common-test-setup';
 import {TEST_PROJECT_NAME} from '../../../test/test-data-generators';
 import {
@@ -137,7 +137,6 @@
     setup(async () => {
       stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       stubRestApi('getDiffComments').returns(Promise.resolve(comments));
-      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
       messages = generateRandomMessages(3);
@@ -384,21 +383,6 @@
       assert.equal(TEST_ONLY.computeTag(m), 'something');
     });
 
-    test('updateTag with robot comments', () => {
-      const m = randomMessage();
-      (m as any).commentThreads = [
-        {
-          comments: [
-            {
-              robot_id: 'id314',
-              change_message_id: m.id,
-            },
-          ],
-        },
-      ];
-      assert.notEqual(TEST_ONLY.computeTag(m), undefined);
-    });
-
     test('setRevisionNumber nothing to change', () => {
       const m1 = randomMessage();
       const m2 = randomMessage();
@@ -546,7 +530,6 @@
     setup(async () => {
       stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
-      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
       messages = [
@@ -589,7 +572,7 @@
     test('unimportant messages hidden after toggle', async () => {
       element.showAllActivity = true;
       await element.updateComplete;
-      const toggle = queryAndAssert<PaperToggleButtonElement>(
+      const toggle = queryAndAssert<MdSwitch>(
         element,
         '.showAllActivityToggle'
       );
@@ -603,7 +586,7 @@
     test('unimportant messages shown after toggle', async () => {
       element.showAllActivity = false;
       await element.updateComplete;
-      const toggle = queryAndAssert<PaperToggleButtonElement>(
+      const toggle = queryAndAssert<MdSwitch>(
         element,
         '.showAllActivityToggle'
       );
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index 244f5d1..8717059 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -3,13 +3,13 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {
   ChangeInfo,
-  RelatedChangeAndCommitInfo,
   CommitId,
+  RelatedChangeAndCommitInfo,
 } from '../../../types/common';
 import {ChangeStatus} from '../../../constants/constants';
 import {isChangeInfo} from '../../../utils/change-util';
@@ -69,6 +69,12 @@
         .indirectRelation {
           color: var(--indirect-relation-text-color);
         }
+        .workInProgress {
+          color: var(--status-wip);
+        }
+        .workInProgressLink {
+          color: var(--deemphasized-text-color);
+        }
         .submittableCheck {
           padding-left: var(--spacing-s);
           color: var(--positive-green-text-color);
@@ -133,6 +139,9 @@
     if (change.submittable) {
       statuses.push('submittable');
     }
+    if (change.work_in_progress) {
+      statuses.push('workInProgressLink');
+    }
     return statuses.join(' ');
   }
 
@@ -149,6 +158,8 @@
       classes.push('indirectRelation');
     } else if (change.submittable) {
       classes.push('submittable');
+    } else if (change.work_in_progress) {
+      classes.push('workInProgress');
     } else if (change.status === ChangeStatus.NEW) {
       classes.push('hidden');
     }
@@ -156,19 +167,21 @@
   }
 
   private computeChangeStatus(change: RelatedChangeAndCommitInfo | ChangeInfo) {
+    const isNotCurrent =
+      !isChangeInfo(change) &&
+      change._revision_number !== change._current_revision_number;
     switch (change.status) {
       case ChangeStatus.MERGED:
-        return 'Merged';
+        return isNotCurrent ? 'Merged, not current' : 'Merged';
       case ChangeStatus.ABANDONED:
-        return 'Abandoned';
+        return isNotCurrent ? 'Abandoned, not current' : 'Abandoned';
     }
-    if (
-      !isChangeInfo(change) &&
-      change._revision_number !== change._current_revision_number
-    ) {
+    if (isNotCurrent) {
       return 'Not current';
     } else if (!isChangeInfo(change) && this.isIndirectRelation(change)) {
       return 'Indirect relation';
+    } else if (change.work_in_progress) {
+      return 'WIP';
     } else if (change.submittable) {
       return 'Submittable';
     }
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 37a039f..19348be 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -10,7 +10,7 @@
 import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
 import '../../shared/gr-icon/gr-icon';
 import {classMap} from 'lit/directives/class-map.js';
-import {LitElement, css, html, TemplateResult} from 'lit';
+import {css, html, LitElement, TemplateResult} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index 1ecad6b..8e799cf 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -3,7 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {PluginApi} from '../../../api/plugin';
 import '../../../test/common-test-setup';
 import {testResolver} from '../../../test/common-test-setup';
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts
index 8c2f459..727353d 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts
@@ -3,7 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, css, html, nothing, TemplateResult} from 'lit';
+import {css, html, LitElement, nothing, TemplateResult} from 'lit';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import {customElement, property} from 'lit/decorators.js';
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
index 504c8b5..2dcccc8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
@@ -14,7 +14,7 @@
 } from '../../../test/test-utils';
 
 import {GrReplyDialog} from './gr-reply-dialog';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {
   AccountId,
   NumericChangeId,
@@ -147,7 +147,7 @@
     );
     const selectedBtn = queryAndAssert(
       labelScoreRows,
-      'gr-button[data-value="+1"].iron-selected'
+      'gr-button[data-value="+1"].selected'
     );
     assert.isOk(selectedBtn);
   });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 3d63802..04c8618 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -42,11 +42,13 @@
   AccountInfo,
   AttentionSetInput,
   ChangeInfo,
+  ChangeViewChangeInfo,
   CommentThread,
   DraftInfo,
   GroupInfo,
   isAccount,
   isDetailedLabelInfo,
+  isDraft,
   isReviewerAccountSuggestion,
   isReviewerGroupSuggestion,
   ReviewerInput,
@@ -56,8 +58,6 @@
   SuggestedReviewerGroupInfo,
   Suggestion,
   UserId,
-  isDraft,
-  ChangeViewChangeInfo,
 } from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrLabelScores} from '../gr-label-scores/gr-label-scores';
@@ -82,13 +82,13 @@
 } from '../../../utils/label-util';
 import {pluralize} from '../../../utils/string-util';
 import {
+  fire,
   fireAlert,
   fireError,
-  fire,
-  fireNoBubble,
   fireIronAnnounce,
-  fireServerError,
+  fireNoBubble,
   fireReload,
+  fireServerError,
 } from '../../../utils/event-util';
 import {ErrorCallback} from '../../../api/rest';
 import {DelayedTask} from '../../../utils/async-util';
@@ -101,7 +101,7 @@
 import {resolve} from '../../../models/dependency';
 import {changeModelToken} from '../../../models/change/change-model';
 import {LabelNameToValuesMap, PatchSetNumber} from '../../../api/rest-api';
-import {css, html, PropertyValues, LitElement, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {when} from 'lit/directives/when.js';
 import {classMap} from 'lit/directives/class-map.js';
@@ -110,7 +110,7 @@
   RemoveReviewerEvent,
   ValueChangedEvent,
 } from '../../../types/events';
-import {customElement, property, state, query} from 'lit/decorators.js';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
 import {hasHumanReviewer} from '../../../utils/change-util';
@@ -126,7 +126,10 @@
 import {accountsModelToken} from '../../../models/accounts/accounts-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {modalStyles} from '../../../styles/gr-modal-styles';
-import {ironAnnouncerRequestAvailability} from '../../polymer-util';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import {grAnnouncerRequestAvailability} from '../../lit-util';
 import {GrReviewerUpdatesParser} from '../../shared/gr-rest-api-interface/gr-reviewer-updates-parser';
 import {formStyles} from '../../../styles/form-styles';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -228,6 +231,9 @@
   knownLatestState?: LatestPatchState;
 
   @state()
+  isChangeMerged = false;
+
+  @state()
   underReview = true;
 
   @state()
@@ -346,6 +352,12 @@
   @state()
   isOwner = false;
 
+  @state()
+  private draggedAccount: AccountInfo | null = null;
+
+  @state()
+  private draggedFrom: GrAccountList | null = null;
+
   private readonly restApiService: RestApiService =
     getAppContext().restApiService;
 
@@ -370,7 +382,12 @@
       formStyles,
       sharedStyles,
       modalStyles,
+      materialStyles,
       css`
+        gr-account-list.drag-over {
+          background-color: var(--table-header-background-color);
+          outline: 2px dashed var(--border-color);
+        }
         :host {
           background-color: var(--dialog-background-color);
           display: block;
@@ -462,7 +479,7 @@
         gr-endpoint-decorator[name='reply-text'] {
           flex-direction: column;
         }
-        #checkingStatusLabel,
+        #changeIsMergedLabel #checkingStatusLabel,
         #notLatestLabel {
           margin-left: var(--spacing-l);
         }
@@ -470,6 +487,7 @@
           color: var(--deemphasized-text-color);
           font-style: italic;
         }
+        #changeIsMergedLabel,
         #notLatestLabel,
         #savingLabel {
           color: var(--error-text-color);
@@ -584,6 +602,22 @@
           margin-right: var(--spacing-m);
           color: var(--info-foreground);
         }
+        .rightActions {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-s);
+        }
+        .rightActions a {
+          display: flex;
+          align-items: center;
+        }
+        .rightActions gr-icon {
+          margin: 0;
+        }
+        md-checkbox {
+          --md-checkbox-container-size: 15px;
+          --md-checkbox-icon-size: 15px;
+        }
       `,
     ];
   }
@@ -678,7 +712,7 @@
 
   override connectedCallback() {
     super.connectedCallback();
-    ironAnnouncerRequestAvailability();
+    grAnnouncerRequestAvailability();
 
     this.getPluginLoader().jsApiService.addElement(
       TargetElement.REPLY_DIALOG,
@@ -810,8 +844,13 @@
           .accounts=${[...this.reviewers]}
           .change=${this.change}
           .reviewerState=${ReviewerState.REVIEWER}
+          .chipDraggable=${true}
           @account-added=${this.handleAccountAdded}
           @accounts-changed=${this.handleReviewersChanged}
+          @account-drag-start=${this.handleAccountDragStart}
+          @dragover=${this.handleDragOver}
+          @dragleave=${this.handleDragLeave}
+          @drop=${this.handleDrop}
           .removableValues=${this.change?.removable_reviewers}
           .filter=${this.filterReviewerSuggestion}
           .pendingConfirmation=${this.reviewerPendingConfirmation}
@@ -838,8 +877,13 @@
           .accounts=${[...this.ccs]}
           .change=${this.change}
           .reviewerState=${ReviewerState.CC}
+          .chipDraggable=${true}
           @account-added=${this.handleAccountAdded}
           @accounts-changed=${this.handleCcsChanged}
+          @account-drag-start=${this.handleAccountDragStart}
+          @dragover=${this.handleDragOver}
+          @dragleave=${this.handleDragLeave}
+          @drop=${this.handleDrop}
           .filter=${this.filterCCSuggestion}
           .pendingConfirmation=${this.ccPendingConfirmation}
           @pending-confirmation-changed=${this.handleCcsConfirmationChanged}
@@ -932,7 +976,7 @@
           // See `addReplyTextChangedCallback` in `ChangeReplyPluginApi`.
           fire(e.currentTarget as HTMLElement, 'value-changed', e.detail);
         }}
-        .messagePlaceholder=${this.messagePlaceholder}
+        .messagePlaceholder=${this.messagePlaceholder ?? ''}
         hide-header
         permanent-editing-mode
       ></gr-comment>
@@ -965,15 +1009,12 @@
     return html`
       <section class="draftsContainer">
         <div class="includeComments">
-          <input
-            type="checkbox"
+          <md-checkbox
             id="includeComments"
             @change=${this.handleIncludeCommentsChanged}
             ?checked=${this.includeComments}
-          />
-          <label for="includeComments"
-            >Publish ${this.computeDraftsTitle(threads)}</label
-          >
+          ></md-checkbox>
+          Publish ${this.computeDraftsTitle(threads)}
         </div>
         ${when(
           this.includeComments,
@@ -1020,7 +1061,7 @@
               `
             )}
           </div>
-          <div>
+          <div class="rightActions">
             ${this.renderModifyAttentionSetButton()}
             <a
               href=${getDocUrl(this.docsBaseUrl, 'user-attention-set.html')}
@@ -1065,9 +1106,7 @@
           <div>
             <span>Modify attention to</span>
           </div>
-
-          <div></div>
-          <div>
+          <div class="rightActions">
             ${this.renderModifyAttentionSetButton()}
             <a
               href=${getDocUrl(this.docsBaseUrl, 'user-attention-set.html')}
@@ -1185,7 +1224,17 @@
             `
           )}
           ${when(
-            this.knownLatestState === LatestPatchState.NOT_LATEST,
+            this.knownLatestState !== LatestPatchState.CHECKING &&
+              this.isChangeMerged,
+            () => html`
+              <span id="changeIsMergedLabel">
+                ${this.computeChangeMergedWarning()}
+              </span>
+            `
+          )}
+          ${when(
+            !this.isChangeMerged &&
+              this.knownLatestState === LatestPatchState.NOT_LATEST,
             () => html`
               <span id="notLatestLabel">
                 ${this.computePatchSetWarning()}
@@ -1230,7 +1279,7 @@
             <gr-button
               id="sendButton"
               primary
-              ?disabled=${this.isSendDisabled()}
+              ?disabled=${!!this.isSendDisabled()}
               class="action send"
               @click=${this.sendClickHandler}
               >${this.canBeStarted
@@ -1258,6 +1307,7 @@
         this.knownLatestState = result.isLatest
           ? LatestPatchState.LATEST
           : LatestPatchState.NOT_LATEST;
+        this.isChangeMerged = result.newStatus === ChangeStatus.MERGED;
       });
 
     this.focusOn(focusTarget);
@@ -1282,7 +1332,7 @@
   }
 
   private handleIncludeCommentsChanged(e: Event) {
-    if (!(e.target instanceof HTMLInputElement)) return;
+    if (!(e.target instanceof MdCheckbox)) return;
     this.includeComments = e.target.checked;
   }
 
@@ -1324,6 +1374,81 @@
     }
   }
 
+  private handleAccountDragStart(e: CustomEvent<{account: AccountInfo}>) {
+    const account = e.detail.account;
+    if (!account) return;
+
+    this.draggedAccount = account;
+    this.draggedFrom = e.currentTarget as GrAccountList;
+  }
+
+  private handleDragOver(e: DragEvent) {
+    e.preventDefault();
+    e.stopPropagation();
+    if (e.dataTransfer) {
+      e.dataTransfer.dropEffect = 'move';
+    }
+    const list = e.currentTarget as GrAccountList;
+    if (this.draggedFrom && this.draggedFrom !== list) {
+      list.classList.add('drag-over');
+    }
+  }
+
+  private handleDragLeave(e: DragEvent) {
+    e.preventDefault();
+    e.stopPropagation();
+    const list = e.currentTarget as GrAccountList;
+    list.classList.remove('drag-over');
+  }
+
+  private handleDrop(e: DragEvent) {
+    e.preventDefault();
+    e.stopPropagation();
+    const dropTarget = e.currentTarget as GrAccountList;
+    dropTarget.classList.remove('drag-over');
+
+    if (!this.draggedAccount || !this.draggedFrom) {
+      return;
+    }
+    if (this.draggedFrom === dropTarget) {
+      this.draggedAccount = null;
+      this.draggedFrom = null;
+      return;
+    }
+
+    const accountToMove = this.draggedAccount;
+    const targetIsCcs = dropTarget.id === 'ccs';
+
+    if (targetIsCcs) {
+      this.reviewers = this.reviewers.filter(
+        r => getUserId(r) !== getUserId(accountToMove)
+      );
+      if (!this.ccs.find(cc => getUserId(cc) === getUserId(accountToMove))) {
+        this.ccs = [...this.ccs, accountToMove];
+        this.reviewersMutated = true;
+      }
+    } else {
+      this.ccs = this.ccs.filter(
+        cc => getUserId(cc) !== getUserId(accountToMove)
+      );
+      if (
+        !this.reviewers.find(r => getUserId(r) === getUserId(accountToMove))
+      ) {
+        this.reviewers = [...this.reviewers, accountToMove];
+        this.reviewersMutated = true;
+      }
+    }
+
+    const moveFrom = targetIsCcs ? 'reviewers' : 'CCs';
+    const moveTo = targetIsCcs ? 'CCs' : 'reviewers';
+    const id = accountToMove.name || getUserId(accountToMove);
+    const message = `${id} moved from ${moveFrom} to ${moveTo}.`;
+    fireAlert(this, message);
+
+    this.draggedAccount = null;
+    this.draggedFrom = null;
+  }
+
   getUnresolvedPatchsetLevelClass(patchsetLevelDraftIsResolved: boolean) {
     return patchsetLevelDraftIsResolved ? 'resolved' : 'unresolved';
   }
@@ -1457,6 +1582,13 @@
     this.reportStartReview(reviewInput);
 
     const errFn = (r?: Response | null) => this.handle400Error(r);
+    if (
+      !(await this.getPluginLoader().jsApiService.handleBeforeReplySent(
+        this.change,
+        reviewInput
+      ))
+    )
+      return;
     this.getNavigation().blockNavigation('sending review');
     return this.saveReview(reviewInput, errFn)
       .then(result => {
@@ -1842,7 +1974,7 @@
     return [...this.newAttentionSet]
       .filter(id => !this.currentAttentionSet.has(id))
       .map(id => this.findAccountById(id))
-      .filter(account => !!account) as AccountInfo[];
+      .filter(account => !!account);
   }
 
   findAccountById(userId: UserId) {
@@ -1915,6 +2047,7 @@
 
   cancelTapHandler(e: Event) {
     e.preventDefault();
+    e.stopPropagation();
     this.cancel();
   }
 
@@ -1928,11 +2061,13 @@
 
   private saveClickHandler(e: Event) {
     e.preventDefault();
+    e.stopPropagation();
     this.submit(false);
   }
 
   private sendClickHandler(e: Event) {
     e.preventDefault();
+    e.stopPropagation();
     this.submit(this.canBeStarted);
   }
 
@@ -2110,6 +2245,10 @@
     );
   }
 
+  computeChangeMergedWarning() {
+    return 'Change has already been merged';
+  }
+
   computePatchSetWarning() {
     let str = `Patch ${this.latestPatchNum} is not latest.`;
     if (this.labelsChanged) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 3b76eda..52a4bd7 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -61,7 +61,7 @@
 import {GrAccountList} from '../../shared/gr-account-list/gr-account-list';
 import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
 import {GrLabelScores} from '../gr-label-scores/gr-label-scores';
-import {fixture, html, waitUntil, assert} from '@open-wc/testing';
+import {assert, fixture, html, waitUntil} from '@open-wc/testing';
 import {accountKey} from '../../../utils/account-util';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrAccountLabel} from '../../shared/gr-account-label/gr-account-label';
@@ -283,7 +283,7 @@
                   <div>
                     <span> No changes to the attention set. </span>
                   </div>
-                  <div>
+                  <div class="rightActions">
                     <gr-button
                       aria-disabled="false"
                       class="edit-attention-button"
@@ -2438,7 +2438,7 @@
   test('isSendDisabled_existingVote', async () => {
     const account = createAccountWithId();
     (
-      element.change!.labels![StandardLabels.CODE_REVIEW]! as DetailedLabelInfo
+      element.change!.labels![StandardLabels.CODE_REVIEW] as DetailedLabelInfo
     ).all = [account];
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2776,6 +2776,7 @@
     );
     el.setSelectedValue('-1');
     await element.updateComplete;
+    await element.updateComplete;
 
     queryAndAssert<GrButton>(element, '.send').click();
     await element.updateComplete;
@@ -2818,6 +2819,7 @@
     );
     el.setSelectedValue('-1');
     await element.updateComplete;
+    await element.updateComplete;
 
     queryAndAssert<GrButton>(element, '.send').click();
     await element.updateComplete;
@@ -2849,7 +2851,6 @@
       };
       commentsModel.setState({
         comments: {},
-        robotComments: {},
         drafts: {
           a: [draft],
         },
@@ -2884,7 +2885,6 @@
       };
       commentsModel.setState({
         comments: {},
-        robotComments: {},
         drafts: {
           a: [draft],
         },
@@ -2924,7 +2924,6 @@
       };
       commentsModel.setState({
         comments: {},
-        robotComments: {},
         drafts: {
           a: [draft],
         },
@@ -2984,7 +2983,6 @@
 
       commentsModel.setState({
         comments: {},
-        robotComments: {},
         drafts: {
           a: [
             {
@@ -3021,7 +3019,6 @@
       stubRestApi('getAccountDetails').returns(Promise.resolve(account));
       commentsModel.setState({
         comments: {},
-        robotComments: {},
         drafts: {
           a: [
             {
@@ -3058,4 +3055,17 @@
     await element.updateComplete;
     assert.equal(queryAndAssert(element, '#pluginMessage').textContent, 'foo');
   });
+
+  test('show warning when change is merged', async () => {
+    element.isChangeMerged = false;
+    await element.updateComplete;
+    let warning = query(element, '#changeIsMergedLabel');
+    assert.isNotOk(warning);
+
+    element.isChangeMerged = true;
+    await element.updateComplete;
+    warning = queryAndAssert(element, '#changeIsMergedLabel');
+    assert.isOk(warning);
+    assert.include(warning.textContent, 'Change has already been merged');
+  });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 5f4fcf3..4d62cbc 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -7,13 +7,13 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-vote-chip/gr-vote-chip';
-import {LitElement, html} from 'lit';
+import {html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 
 import {
+  AccountDetailInfo,
   AccountInfo,
   ApprovalInfo,
-  AccountDetailInfo,
   isDetailedLabelInfo,
   LabelInfo,
 } from '../../../types/common';
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
index 4df4bde..b64d220 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
@@ -15,7 +15,7 @@
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {AccountId, EmailAddress} from '../../../types/common';
 import './gr-reviewer-list';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {IdToAttentionSetMap} from '../../../api/rest-api';
 import {StandardLabels} from '../../../utils/label-util';
 
diff --git a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
index d3b46ff..3b5ba67 100644
--- a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
@@ -5,7 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import './gr-revision-parents';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrRevisionParents} from './gr-revision-parents';
 import {createRevision} from '../../../test/test-data-generators';
 import {
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard_test.ts
index f1ebd99..f8977f2 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-submit-requirement-dashboard-hovercard';
 import {GrSubmitRequirementDashboardHovercard} from './gr-submit-requirement-dashboard-hovercard';
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 950fb01..ce3c20e 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -128,6 +128,9 @@
           margin-top: var(--spacing-m);
           padding: var(--spacing-m) var(--spacing-xl) 0;
         }
+        gr-formatted-text {
+          overflow-wrap: anywhere;
+        }
       `,
     ];
   }
@@ -165,7 +168,7 @@
     return html`<gr-icon
       class=${icon.icon}
       icon=${icon.icon}
-      ?filled=${icon.filled}
+      ?filled=${!!icon.filled}
       role="img"
       aria-label=${requirement.status.toLowerCase()}
     ></gr-icon>`;
@@ -197,7 +200,10 @@
 
   private renderLabelSection() {
     if (!this.requirement) return;
-    const requirementLabels = extractAssociatedLabels(this.requirement);
+    const requirementLabels = extractAssociatedLabels(this.requirement, {
+      extractFromSubmittability: true,
+      extractFromOverride: 'onlyIfOverridden',
+    });
     const allLabels = this.change?.labels ?? {};
     const labels: string[] = [];
     for (const label of Object.keys(allLabels)) {
@@ -257,18 +263,18 @@
     if (!this.account) return;
     if (this.change?.status === ChangeStatus.MERGED) return;
 
-    const submittabilityLabels = extractAssociatedLabels(
-      this.requirement,
-      'onlySubmittability'
-    );
+    const submittabilityLabels = extractAssociatedLabels(this.requirement, {
+      extractFromSubmittability: true,
+      extractFromOverride: false,
+    });
     const submittabilityVotes = submittabilityLabels.map(labelName =>
       this.renderLabelVote(labelName, 'submittability')
     );
 
-    const overrideLabels = extractAssociatedLabels(
-      this.requirement,
-      'onlyOverride'
-    );
+    const overrideLabels = extractAssociatedLabels(this.requirement, {
+      extractFromSubmittability: false,
+      extractFromOverride: 'onlyIfOverridden',
+    });
     const overrideVotes = overrideLabels.map(labelName =>
       this.renderLabelVote(labelName, 'override')
     );
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 7cd4066..09c8b38 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-submit-requirement-hovercard';
 import {GrSubmitRequirementHovercard} from './gr-submit-requirement-hovercard';
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index d950941..377a649 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -10,9 +10,11 @@
 import '../gr-change-summary/gr-change-summary';
 import '../../shared/gr-limited-text/gr-limited-text';
 import '../../shared/gr-vote-chip/gr-vote-chip';
-import {LitElement, css, html, TemplateResult, nothing} from 'lit';
+import '../../checks/gr-checks-chip-for-label';
+import {css, html, LitElement, nothing, TemplateResult} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {ParsedChangeInfo} from '../../../types/types';
+import {repeat} from 'lit/directives/repeat.js';
 import {
   AccountInfo,
   isDetailedLabelInfo,
@@ -36,16 +38,17 @@
 } from '../../../utils/label-util';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {capitalizeFirstLetter, charsOnly} from '../../../utils/string-util';
-import {subscribe} from '../../lit/subscription-controller';
-import {CheckRun} from '../../../models/checks/checks-model';
-import {getResultsOf, hasResultsOf} from '../../../models/checks/checks-util';
-import {Category, RunStatus} from '../../../api/checks';
-import {fireShowTab} from '../../../utils/event-util';
-import {Tab} from '../../../constants/constants';
 import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
 import {resolve} from '../../../models/dependency';
-import {checksModelToken} from '../../../models/checks/checks-model';
+import {CheckRun, checksModelToken} from '../../../models/checks/checks-model';
 import {map} from 'lit/directives/map.js';
+import {
+  countErrorRunsForLabel,
+  countRunningRunsForLabel,
+} from '../../checks/gr-checks-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {when} from 'lit/directives/when.js';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 
 /**
  * @attr {Boolean} suppress-title - hide titles, currently for hovercard view
@@ -74,6 +77,7 @@
     return [
       fontStyles,
       submitRequirementsStyles,
+      spinnerStyles,
       css`
         :host([suppress-title]) .metadata-title {
           display: none;
@@ -125,10 +129,18 @@
         gr-vote-chip {
           margin-right: var(--spacing-s);
         }
-        gr-checks-chip {
+        gr-checks-chip-for-label {
           /* .checksChip has top: 2px, this is canceling it */
           margin-top: -2px;
         }
+        /* The basics of .loadingSpin are defined in shared styles. */
+        .loadingSpin {
+          width: calc(var(--line-height-normal) * 0.7);
+          height: calc(var(--line-height-normal) * 0.7);
+          display: inline-block;
+          vertical-align: middle;
+          margin-left: calc(var(--spacing-s));
+        }
       `,
     ];
   }
@@ -152,40 +164,59 @@
     const submit_requirements = orderSubmitRequirements(
       getRequirements(this.change)
     );
-    if (submit_requirements.length === 0) return nothing;
+
+    const requirementKey = (req: SubmitRequirementResultInfo, index: number) =>
+      `${index}-${req.name}`;
     return html` <h3
         class="metadata-title heading-3"
         id="submit-requirements-caption"
       >
         Submit Requirements
+        ${when(
+          submit_requirements.length === 0,
+          () =>
+            html`<span
+              class="loadingSpin"
+              title="Submit Requirements status is being updated"
+            ></span>`
+        )}
       </h3>
-      <table class="requirements" aria-labelledby="submit-requirements-caption">
-        <thead hidden>
-          <tr>
-            <th>Status</th>
-            <th>Name</th>
-            <th>Votes</th>
-          </tr>
-        </thead>
-        <tbody>
-          ${submit_requirements.map((requirement, index) =>
-            this.renderRequirement(requirement, index)
-          )}
-        </tbody>
-      </table>
-      ${this.disableHovercards
-        ? ''
-        : submit_requirements.map(
-            (requirement, index) => html`
-              <gr-submit-requirement-hovercard
-                for="requirement-${index}-${charsOnly(requirement.name)}"
-                .requirement=${requirement}
-                .change=${this.change}
-                .account=${this.account}
-                .mutable=${this.mutable ?? false}
-              ></gr-submit-requirement-hovercard>
-            `
-          )}`;
+      ${when(
+        submit_requirements.length !== 0,
+        () => html`<table
+            class="requirements"
+            aria-labelledby="submit-requirements-caption"
+          >
+            <thead hidden>
+              <tr>
+                <th>Status</th>
+                <th>Name</th>
+                <th>Votes</th>
+              </tr>
+            </thead>
+            <tbody>
+              ${repeat(
+                submit_requirements,
+                requirementKey,
+                (requirement, index) =>
+                  this.renderRequirement(requirement, index)
+              )}
+            </tbody>
+          </table>
+          ${this.disableHovercards
+            ? ''
+            : submit_requirements.map(
+                (requirement, index) => html`
+                  <gr-submit-requirement-hovercard
+                    for="requirement-${index}-${charsOnly(requirement.name)}"
+                    .requirement=${requirement}
+                    .change=${this.change}
+                    .account=${this.account}
+                    .mutable=${this.mutable ?? false}
+                  ></gr-submit-requirement-hovercard>
+                `
+              )}`
+      )}`;
   }
 
   private renderRequirement(
@@ -248,7 +279,7 @@
     const icon = iconForRequirement(requirement);
     return html`<gr-icon
       class=${icon.icon}
-      ?filled=${icon.filled}
+      ?filled=${!!icon.filled}
       .icon=${icon.icon}
       role="img"
       aria-label=${requirement.status.toLowerCase()}
@@ -260,7 +291,10 @@
       return html`<span class="error">Error</span>`;
     }
 
-    const requirementLabels = extractAssociatedLabels(requirement);
+    const requirementLabels = extractAssociatedLabels(requirement, {
+      extractFromSubmittability: true,
+      extractFromOverride: 'onlyIfOverridden',
+    });
     const allLabels = this.change?.labels ?? {};
     const associatedLabels = Object.keys(allLabels).filter(label =>
       requirementLabels.includes(label)
@@ -358,10 +392,10 @@
       requirement.status !== SubmitRequirementStatus.OVERRIDDEN
     )
       return;
-    const requirementLabels = extractAssociatedLabels(
-      requirement,
-      showForAllLabel ? 'all' : 'onlyOverride'
-    )
+    const requirementLabels = extractAssociatedLabels(requirement, {
+      extractFromSubmittability: showForAllLabel,
+      extractFromOverride: 'onlyIfOverridden',
+    })
       .filter(label => label === forLabel)
       .filter(label => {
         const allLabels = this.change?.labels ?? {};
@@ -373,73 +407,42 @@
   }
 
   renderChecks(requirement: SubmitRequirementResultInfo, labelName?: string) {
-    const requirementLabels = extractAssociatedLabels(requirement);
-    const errorRuns = this.runs
-      .filter(run => hasResultsOf(run, Category.ERROR))
-      .filter(run => {
-        if (labelName) {
-          return labelName === run.labelName;
-        } else {
-          return run.labelName && requirementLabels.includes(run.labelName);
-        }
-      });
-    const errorRunsCount = errorRuns.reduce(
-      (sum, run) => sum + getResultsOf(run, Category.ERROR).length,
-      0
+    // This method includes preliminary checks before rendering the
+    // <gr-checks-chip-for-label> component. These checks are necessary
+    // because:
+    // 1. We only want to display the checks chip for specific conditions
+    //    relevant to message scores (e.g., negative scores on the latest
+    //    patchset for which the message was posted).
+    // 2. It's programmatically difficult for a parent component to know if a
+    //    child Lit component (like <gr-checks-chip-for-label>) will internally
+    //    render 'nothing'. If the child component were rendered unconditionally
+    //    and then its own logic led it to render 'nothing', we might still
+    //    have an empty custom element in the DOM or face challenges in
+    //    conditionally rendering alternative UI elements that depend on the
+    //    child's visibility
+    // These upfront checks ensure that <gr-checks-chip-for-label> is only
+    // instantiated and rendered when it's genuinely appropriate and likely
+    // to display meaningful information, avoiding an empty or superfluous element.
+    const targetLabels = labelName
+      ? [labelName]
+      : requirement
+      ? extractAssociatedLabels(requirement)
+      : [];
+
+    // If there are no labels to filter by, then no checks can be associated.
+    if (targetLabels.length === 0) return undefined;
+
+    const {errorRunsCount} = countErrorRunsForLabel(this.runs, targetLabels);
+    const {runningRunsCount} = countRunningRunsForLabel(
+      this.runs,
+      targetLabels
     );
-    if (errorRunsCount > 0) {
-      return this.renderChecksCategoryChip(
-        errorRuns,
-        errorRunsCount,
-        Category.ERROR
-      );
-    }
-    const runningRuns = this.runs
-      .filter(r => r.isLatestAttempt)
-      .filter(
-        r => r.status === RunStatus.RUNNING || r.status === RunStatus.SCHEDULED
-      )
-      .filter(run => {
-        if (labelName) {
-          return labelName === run.labelName;
-        } else {
-          return run.labelName && requirementLabels.includes(run.labelName);
-        }
-      });
 
-    const runningRunsCount = runningRuns.length;
-    if (runningRunsCount > 0) {
-      return this.renderChecksCategoryChip(
-        runningRuns,
-        runningRunsCount,
-        RunStatus.RUNNING
-      );
-    }
-    return;
-  }
-
-  renderChecksCategoryChip(
-    runs: CheckRun[],
-    runsCount: Number,
-    category: Category | RunStatus
-  ) {
-    if (runsCount === 0) return;
-    const links = [];
-    if (runs.length === 1 && runs[0].statusLink) {
-      links.push(runs[0].statusLink);
-    }
-    return html`<gr-checks-chip
-      .text=${`${runsCount}`}
-      .links=${links}
-      .statusOrCategory=${category}
-      @click=${() => {
-        fireShowTab(this, Tab.CHECKS, false, {
-          checksTab: {
-            statusOrCategory: category,
-          },
-        });
-      }}
-    ></gr-checks-chip>`;
+    if (errorRunsCount <= 0 && runningRunsCount <= 0) return undefined;
+    return html`<gr-checks-chip-for-label
+      .labels=${targetLabels}
+      .showRunning=${true}
+    ></gr-checks-chip-for-label>`;
   }
 
   renderTriggerVotes() {
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index a5a336c..aee2017 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -4,43 +4,52 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-submit-requirements';
 import {GrSubmitRequirements} from './gr-submit-requirements';
 import {
   createAccountWithIdNameAndEmail,
   createApproval,
+  createCheckResult,
   createDetailedLabelInfo,
+  createNonApplicableSubmitRequirementResultInfo,
   createParsedChange,
+  createRun,
   createSubmitRequirementExpressionInfo,
   createSubmitRequirementResultInfo,
-  createNonApplicableSubmitRequirementResultInfo,
-  createCheckResult,
-  createRun,
 } from '../../../test/test-data-generators';
 import {
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
 } from '../../../api/rest-api';
-import {ParsedChangeInfo} from '../../../types/types';
+import {LoadingStatus, ParsedChangeInfo} from '../../../types/types';
 import {RunStatus} from '../../../api/checks';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+  ChangeModel,
+  changeModelToken,
+} from '../../../models/change/change-model';
 
 suite('gr-submit-requirements tests', () => {
   let element: GrSubmitRequirements;
   let change: ParsedChangeInfo;
+  let changeModel: ChangeModel;
+
   setup(async () => {
     const submitRequirement: SubmitRequirementResultInfo = {
       ...createSubmitRequirementResultInfo(),
       description: 'Test Description',
       submittability_expression_result: createSubmitRequirementExpressionInfo(),
     };
+    const submitRequirements: SubmitRequirementResultInfo[] = [
+      submitRequirement,
+      createNonApplicableSubmitRequirementResultInfo(),
+    ];
     change = {
       ...createParsedChange(),
-      submit_requirements: [
-        submitRequirement,
-        createNonApplicableSubmitRequirementResultInfo(),
-      ],
+      submittable: false,
+      submit_requirements: submitRequirements,
       labels: {
         Verified: {
           ...createDetailedLabelInfo(),
@@ -54,12 +63,23 @@
       },
     };
     const account = createAccountWithIdNameAndEmail();
+    changeModel = testResolver(changeModelToken);
+    changeModel.setState({
+      change,
+      submittabilityInfo: {
+        changeNum: change._number,
+        submitRequirements,
+        submittable: false,
+      },
+      loadingStatus: LoadingStatus.LOADED,
+    });
     element = await fixture<GrSubmitRequirements>(
       html`<gr-submit-requirements
         .change=${change}
         .account=${account}
       ></gr-submit-requirements>`
     );
+    await element.updateComplete;
   });
 
   test('renders', () => {
@@ -157,7 +177,7 @@
 
     test('without label to vote on', async () => {
       const modifiedChange = {...change};
-      modifiedChange.submit_requirements![0]!.submittability_expression_result.expression =
+      modifiedChange.submit_requirements![0].submittability_expression_result.expression =
         'hasfooter:"Release-Notes"';
       element.change = modifiedChange;
       await element.updateComplete;
@@ -185,7 +205,7 @@
             <div class="votes">
               <div class="votes-line">
                 <gr-vote-chip> </gr-vote-chip>
-                <gr-checks-chip> </gr-checks-chip>
+                <gr-checks-chip-for-label> </gr-checks-chip-for-label>
               </div>
             </div>
           </div>
@@ -211,7 +231,7 @@
             <div class="votes">
               <div class="votes-line">
                 <gr-vote-chip> </gr-vote-chip>
-                <gr-checks-chip> </gr-checks-chip>
+                <gr-checks-chip-for-label> </gr-checks-chip-for-label>
               </div>
             </div>
           </div>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 86bf1b2..95271f5 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -11,18 +11,16 @@
   AccountDetailInfo,
   AccountInfo,
   CommentThread,
+  isDraft,
   NumericChangeId,
   UrlEncodedCommentId,
-  isDraft,
 } from '../../../types/common';
 import {ChangeMessageId} from '../../../api/rest-api';
 import {
   getCommentAuthors,
   getMentionedThreads,
-  hasHumanReply,
   isDraftThread,
   isMentionedThread,
-  isRobotThread,
   isUnresolved,
   lastUpdated,
 } from '../../../utils/comment-util';
@@ -142,8 +140,7 @@
    * ATTENTION! this.threads should never be used directly within the component.
    *
    * Either use getAllThreads(), which applies filters that are inherent to what
-   * the component is supposed to render,
-   * e.g. onlyShowRobotCommentsWithHumanReply.
+   * the component is supposed to render.
    *
    * Or use getDisplayedThreads(), which applies the currently selected filters
    * on top.
@@ -158,12 +155,6 @@
   @property({type: Boolean, attribute: 'unresolved-only'})
   unresolvedOnly = false;
 
-  @property({
-    type: Boolean,
-    attribute: 'only-show-robot-comments-with-human-reply',
-  })
-  onlyShowRobotCommentsWithHumanReply = false;
-
   @property({type: Boolean, attribute: 'hide-dropdown'})
   hideDropdown = false;
 
@@ -413,7 +404,7 @@
       <gr-comment-thread
         .thread=${thread}
         show-file-path
-        ?show-ported-comment=${thread.ported}
+        ?show-ported-comment=${!!thread.ported}
         ?show-comment-context=${this.showCommentContext}
         ?show-file-name=${isFirst}
         .messageId=${this.messageId}
@@ -522,12 +513,7 @@
    */
   // private, but visible for testing
   getAllThreads() {
-    return this.threads.filter(
-      t =>
-        !this.onlyShowRobotCommentsWithHumanReply ||
-        !isRobotThread(t) ||
-        hasHumanReply(t)
-    );
+    return this.threads;
   }
 
   /**
@@ -566,11 +552,6 @@
       if (!hasACommentFromASelectedAuthor) return false;
     }
 
-    // This is probably redundant, because getAllThreads() filters this out.
-    if (this.onlyShowRobotCommentsWithHumanReply) {
-      if (isRobotThread(thread) && !hasHumanReply(thread)) return false;
-    }
-
     if (this.mentionsOnly && !isMentionedThread(thread, this.account))
       return false;
 
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
index 3a06f8d..6dd4f0c 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
@@ -8,9 +8,9 @@
 import {CommentSide, SpecialFilePath} from '../../../constants/constants';
 import {CommentTabState} from '../../../types/events';
 import {
+  __testOnly_SortDropdownState,
   compareThreads,
   GrThreadList,
-  __testOnly_SortDropdownState,
 } from './gr-thread-list';
 import {queryAll, stubFlags} from '../../../test/test-utils';
 import {getUserId} from '../../../utils/account-util';
@@ -29,17 +29,16 @@
   Timestamp,
 } from '../../../api/rest-api';
 import {
-  RobotId,
-  UrlEncodedCommentId,
-  RevisionPatchSetNum,
   CommentThread,
   isDraft,
+  RevisionPatchSetNum,
   SavingState,
+  UrlEncodedCommentId,
 } from '../../../types/common';
 import {query, queryAndAssert} from '../../../utils/common-util';
 import {GrAccountLabel} from '../../shared/gr-account-label/gr-account-label';
 import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
 import {FILE} from '../../../api/diff';
 
@@ -222,7 +221,6 @@
             updated: '2015-12-08 15:16:15.000000000' as Timestamp,
             message: 'test',
             unresolved: true,
-            robot_id: 'rc1' as RobotId,
           },
         ],
         patchNum: 4 as RevisionPatchSetNum,
@@ -247,7 +245,6 @@
             updated: '2015-12-09 15:16:15.000000000' as Timestamp,
             message: 'test',
             unresolved: true,
-            robot_id: 'rc2' as RobotId,
           },
           {
             path: '/COMMIT_MSG',
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard_test.ts
index 305ddd3..6a2273a 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-trigger-vote-hovercard';
 import {GrTriggerVoteHovercard} from './gr-trigger-vote-hovercard';
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
index c6493b3..e400584 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
@@ -6,7 +6,7 @@
 import '../../shared/gr-label-info/gr-label-info';
 import '../../shared/gr-vote-chip/gr-vote-chip';
 import '../gr-trigger-vote-hovercard/gr-trigger-vote-hovercard';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {ParsedChangeInfo} from '../../../types/types';
 import {
@@ -91,7 +91,7 @@
   }
 
   private renderHovercard() {
-    if (this.disableHovercards) return;
+    if (this.disableHovercards || !this.label) return;
     return html`<gr-trigger-vote-hovercard
       .labelName=${this.label}
       .labelInfo=${this.labelInfo}
@@ -100,7 +100,7 @@
         slot="label-info"
         .change=${this.change}
         .account=${this.account}
-        .mutable=${this.mutable}
+        .mutable=${!!this.mutable}
         .label=${this.label}
         .labelInfo=${this.labelInfo}
         .showAllReviewers=${false}
@@ -127,7 +127,7 @@
         ></gr-vote-chip>`
       );
     } else if (isQuickLabelInfo(labelInfo)) {
-      return [html`<gr-vote-chip .label=${this.labelInfo}></gr-vote-chip>`];
+      return [html`<gr-vote-chip .label=${labelInfo}></gr-vote-chip>`];
     } else {
       return html``;
     }
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
index 6247472..d9f3b86 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-trigger-vote';
 import {GrTriggerVote} from './gr-trigger-vote';
@@ -12,10 +12,10 @@
   createAccountWithIdNameAndEmail,
   createApproval,
   createDetailedLabelInfo,
+  createNonApplicableSubmitRequirementResultInfo,
   createParsedChange,
   createSubmitRequirementExpressionInfo,
   createSubmitRequirementResultInfo,
-  createNonApplicableSubmitRequirementResultInfo,
 } from '../../../test/test-data-generators';
 import {SubmitRequirementResultInfo} from '../../../api/rest-api';
 import {ParsedChangeInfo} from '../../../types/types';
@@ -67,8 +67,8 @@
         <gr-trigger-vote-hovercard>
           <gr-label-info slot="label-info"></gr-label-info>
         </gr-trigger-vote-hovercard>
-        <span class="label"> Verified </span>
-        <gr-vote-chip> </gr-vote-chip>
+        <span class="label">Verified</span>
+        <gr-vote-chip></gr-vote-chip>
       </div>`
     );
   });
diff --git a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
index 4694d25..6fdb74e 100644
--- a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
+++ b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options.ts
@@ -4,11 +4,13 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {customElement, property} from 'lit/decorators.js';
 import {
-  ValidationOptionsInfo,
   ValidationOptionInfo,
+  ValidationOptionsInfo,
 } from '../../../api/rest-api';
 import {repeat} from 'lit/directives/repeat.js';
 import {capitalizeFirstLetter} from '../../../utils/string-util';
@@ -21,10 +23,19 @@
 
   static override get styles() {
     return [
+      materialStyles,
       css`
-        .selectionLabel {
+        :host {
           display: block;
-          margin-left: -4px;
+        }
+        div.checkbox-container {
+          display: flex;
+          align-items: center;
+          gap: var(--spacing-m);
+          margin-top: var(--spacing-l);
+        }
+        label {
+          cursor: pointer;
         }
       `,
     ];
@@ -51,14 +62,17 @@
 
   private renderValidationOption(option: ValidationOptionInfo) {
     return html`
-      <label class="selectionLabel">
-        <input
-          type="checkbox"
-          .checked=${!!this.isOptionSelected.get(option.name)}
-          @click=${() => this.toggleCheckbox(option)}
-        />
-        ${capitalizeFirstLetter(option.description)}
-      </label>
+      <div class="checkbox-container">
+        <md-checkbox
+          class="selectionLabel"
+          id=${option.name}
+          ?checked=${!!this.isOptionSelected.get(option.name)}
+          @change=${() => this.toggleCheckbox(option)}
+        ></md-checkbox>
+        <label for=${option.name}
+          >${capitalizeFirstLetter(option.description)}</label
+        >
+      </div>
     `;
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options_test.ts b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options_test.ts
index 0f5a48e..1a1fb0c 100644
--- a/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-validation-options/gr-validation-options_test.ts
@@ -4,14 +4,15 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-validation-options';
 import {GrValidationOptions} from './gr-validation-options';
 import {ValidationOptionsInfo} from '../../../api/rest-api';
 import {queryAll} from '../../../test/test-utils';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
 
-suite('gr-trigger-vote tests', () => {
+suite('gr-validation-options tests', () => {
   let element: GrValidationOptions;
   setup(async () => {
     const validationOptions: ValidationOptionsInfo = {
@@ -31,34 +32,32 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <label class="selectionLabel">
-          <input type="checkbox" />
-          Option 1
-        </label>
-        <label class="selectionLabel">
-          <input type="checkbox" />
-          Option 2
-        </label>
+        <div class="checkbox-container">
+          <md-checkbox class="selectionLabel" id="o1"> </md-checkbox>
+          <label for="o1"> Option 1 </label>
+        </div>
+        <div class="checkbox-container">
+          <md-checkbox class="selectionLabel" id="o2"> </md-checkbox>
+          <label for="o2"> Option 2 </label>
+        </div>
       `
     );
   });
 
-  test('selects and unselects options', () => {
-    const checkboxes = queryAll<HTMLInputElement>(
-      element,
-      'input[type="checkbox"]'
-    );
-    element.validationOptions?.validation_options;
+  test('selects and unselects options', async () => {
+    const checkboxes = queryAll<MdCheckbox>(element, 'md-checkbox');
 
     assert.deepEqual(element.getSelectedOptions(), []);
 
     checkboxes[0].click();
+    await element.updateComplete;
 
     assert.deepEqual(element.getSelectedOptions(), [
       {name: 'o1', description: 'option 1'},
     ]);
 
     checkboxes[1].click();
+    await element.updateComplete;
 
     assert.deepEqual(element.getSelectedOptions(), [
       {name: 'o1', description: 'option 1'},
@@ -66,12 +65,14 @@
     ]);
 
     checkboxes[0].click();
+    await element.updateComplete;
 
     assert.deepEqual(element.getSelectedOptions(), [
       {name: 'o2', description: 'option 2'},
     ]);
 
     checkboxes[1].click();
+    await element.updateComplete;
 
     assert.deepEqual(element.getSelectedOptions(), []);
   });
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index abd8a23..1143fe8 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -3,12 +3,18 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, PropertyValues, css, html} from 'lit';
+import '../shared/gr-tooltip-content/gr-tooltip-content';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {Action, NOT_USEFUL, USEFUL} from '../../api/checks';
 import {assertIsDefined} from '../../utils/common-util';
 import {resolve} from '../../models/dependency';
-import {checksModelToken} from '../../models/checks/checks-model';
+import {
+  CheckRun,
+  checksModelToken,
+  RunResult,
+} from '../../models/checks/checks-model';
+
 @customElement('gr-checks-action')
 export class GrChecksAction extends LitElement {
   @property({type: Object})
@@ -17,6 +23,10 @@
   @property({type: Object})
   eventTarget: HTMLElement | null = null;
 
+  /** Just for reporting. */
+  @property({type: Object})
+  runOrResult?: CheckRun | RunResult;
+
   /** In what context is <gr-checks-action> rendered? Just for reporting. */
   @property({type: String})
   context = 'unknown';
@@ -48,11 +58,10 @@
         gr-button {
           --gr-button-padding: var(--spacing-s) var(--spacing-m);
         }
-        paper-tooltip {
+        gr-tooltip-content {
           text-transform: none;
           text-align: center;
           white-space: normal;
-          max-width: 200px;
         }
       `,
     ];
@@ -69,16 +78,19 @@
   }
 
   override render() {
+    if (!this.action.tooltip) {
+      return this.renderButton();
+    }
+
     return html`
-      <gr-button
-        link
-        ?disabled=${this.action.disabled}
-        class="action"
-        @click=${(e: Event) => this.handleClick(e)}
+      <gr-tooltip-content
+        has-tooltip
+        ?position-below=${true}
+        .maxWidth=${'200px'}
+        title=${this.action.tooltip}
       >
-        ${this.renderName()}
-      </gr-button>
-      ${this.renderTooltip()}
+        ${this.renderButton()}
+      </gr-tooltip-content>
     `;
   }
 
@@ -89,12 +101,16 @@
     `;
   }
 
-  private renderTooltip() {
-    if (!this.action.tooltip) return;
+  private renderButton() {
     return html`
-      <paper-tooltip offset="5" ?fitToVisibleBounds=${true}>
-        ${this.action.tooltip}
-      </paper-tooltip>
+      <gr-button
+        link
+        ?disabled=${!!this.action.disabled}
+        class="action"
+        @click=${(e: Event) => this.handleClick(e)}
+      >
+        ${this.renderName()}
+      </gr-button>
     `;
   }
 
@@ -106,7 +122,11 @@
       // the "Was this helpful?" label.
       e.stopPropagation();
     }
-    this.getChecksModel().triggerAction(this.action, undefined, this.context);
+    this.getChecksModel().triggerAction(
+      this.action,
+      this.runOrResult,
+      this.context
+    );
   }
 }
 
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
index 8c0143c..9677185 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
@@ -3,7 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {CheckRun} from '../../models/checks/checks-model';
 import {ordinal} from '../../utils/string-util';
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts
new file mode 100644
index 0000000..ddf37ab
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../change/gr-change-summary/gr-checks-chip';
+import {html, LitElement, nothing} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {Category, RunStatus} from '../../api/checks';
+import {Tab} from '../../constants/constants';
+import {fireShowTab} from '../../utils/event-util';
+import {CheckRun, checksModelToken} from '../../models/checks/checks-model';
+import {resolve} from '../../models/dependency';
+import {subscribe} from '../lit/subscription-controller';
+import {
+  countErrorRunsForLabel,
+  countRunningRunsForLabel,
+} from './gr-checks-util';
+
+@customElement('gr-checks-chip-for-label')
+export class GrChecksChipForLabel extends LitElement {
+  @property({type: Array})
+  labels: string[] = [];
+
+  @property({type: Boolean})
+  showRunning = false;
+
+  @state() runs: CheckRun[] = [];
+
+  private readonly getChecksModel = resolve(this, checksModelToken);
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getChecksModel().allRunsLatestPatchsetLatestAttempt$,
+      x => (this.runs = x)
+    );
+  }
+
+  override render() {
+    const {errorRuns, errorRunsCount} = countErrorRunsForLabel(
+      this.runs,
+      this.labels
+    );
+    if (errorRunsCount > 0) {
+      return this.renderChecksCategoryChip(
+        errorRuns,
+        errorRunsCount,
+        Category.ERROR
+      );
+    }
+    if (!this.showRunning) return nothing;
+    const {runningRuns, runningRunsCount} = countRunningRunsForLabel(
+      this.runs,
+      this.labels
+    );
+    if (runningRunsCount > 0) {
+      return this.renderChecksCategoryChip(
+        runningRuns,
+        runningRunsCount,
+        RunStatus.RUNNING
+      );
+    }
+    return nothing;
+  }
+
+  private renderChecksCategoryChip(
+    runs: CheckRun[],
+    runsCount: number,
+    category: Category | RunStatus
+  ) {
+    if (runsCount === 0) return;
+    const links = [];
+    if (runs.length === 1 && runs[0].statusLink) {
+      links.push(runs[0].statusLink);
+    }
+    return html`<gr-checks-chip
+      .text=${`${runsCount}`}
+      .links=${links}
+      .statusOrCategory=${category}
+      @click=${() => {
+        fireShowTab(this, Tab.CHECKS, false, {
+          checksTab: {
+            statusOrCategory: category,
+          },
+        });
+      }}
+    ></gr-checks-chip>`;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-checks-chip-for-label': GrChecksChipForLabel;
+  }
+}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts
new file mode 100644
index 0000000..cac448a
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {Category} from '../../api/checks';
+import '../../test/common-test-setup';
+import {createCheckResult, createRun} from '../../test/test-data-generators';
+import {queryAndAssert} from '../../utils/common-util';
+import {GrChecksChip} from '../change/gr-change-summary/gr-checks-chip';
+import './gr-checks-chip-for-label';
+import {GrChecksChipForLabel} from './gr-checks-chip-for-label';
+import {assert, fixture} from '@open-wc/testing';
+import {html} from 'lit';
+
+suite('gr-checks-chip-for-label test', () => {
+  let element: GrChecksChipForLabel;
+
+  setup(async () => {
+    element = await fixture<GrChecksChipForLabel>(
+      html`<gr-checks-chip-for-label></gr-checks-chip-for-label>`
+    );
+    element.runs = [
+      createRun({
+        labelName: 'Verified',
+
+        results: [
+          createCheckResult({
+            category: Category.ERROR,
+          }),
+        ],
+      }),
+    ];
+    await element.updateComplete;
+
+    element.labels = ['Verified'];
+    await element.updateComplete;
+  });
+
+  test('renders loading', async () => {
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ '<gr-checks-chip></gr-checks-chip>'
+    );
+
+    const checksChip = queryAndAssert<GrChecksChip>(element, 'gr-checks-chip');
+    assert.shadowDom.equal(
+      checksChip,
+      /* HTML */ `<div
+        aria-label="1 error result"
+        class="checksChip error font-small"
+        role="link"
+        tabindex="0"
+      >
+        <gr-icon filled="" icon="error"> </gr-icon>
+        <div class="text">1</div>
+      </div>`
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
index 56a99a9..7c26e92 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
@@ -6,7 +6,7 @@
 import '../shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 import {GrSuggestionDiffPreview} from '../shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 import {css, html, LitElement, nothing} from 'lit';
-import {customElement, query, property, state} from 'lit/decorators.js';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {BasePatchSetNum, RepoName} from '../../types/common';
 import {resolve} from '../../models/dependency';
 import {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts
index 72116b7..c575565 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview_test.ts
@@ -8,7 +8,7 @@
 import './gr-checks-results';
 import './gr-checks-fix-preview';
 import {html} from 'lit';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {createCheckFix} from '../../test/test-data-generators';
 import {GrChecksFixPreview} from './gr-checks-fix-preview';
 import {rectifyFix} from '../../models/checks/checks-util';
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 6a72d41..a808c69 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -8,17 +8,17 @@
 import {repeat} from 'lit/directives/repeat.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {
-  LitElement,
   css,
   html,
+  LitElement,
+  nothing,
   PropertyValues,
   TemplateResult,
-  nothing,
 } from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import './gr-checks-action';
 import './gr-hovercard-run';
-import '@polymer/paper-tooltip/paper-tooltip';
+import '../shared/gr-tooltip-content/gr-tooltip-content';
 import {
   Action,
   Category,
@@ -33,27 +33,27 @@
 import {CheckRun, RunResult, runResult} from '../../models/checks/checks-model';
 import {
   ALL_ATTEMPTS,
+  allResults,
   AttemptChoice,
   attemptChoiceLabel,
-  isAttemptChoice,
-  LATEST_ATTEMPT,
-  sortAttemptChoices,
-  stringToAttemptChoice,
-  allResults,
+  computeIsExpandable,
   createFixAction,
+  createGetAiFixAction,
   firstPrimaryLink,
   hasCompletedWithoutResults,
   iconFor,
   iconForLink,
+  isAttemptChoice,
   isCategory,
+  LATEST_ATTEMPT,
   otherPrimaryLinks,
-  secondaryLinks,
-  tooltipForLink,
-  computeIsExpandable,
   rectifyFix,
-  createGetAiFixAction,
+  secondaryLinks,
+  sortAttemptChoices,
+  stringToAttemptChoice,
+  tooltipForLink,
 } from '../../models/checks/checks-util';
-import {assertIsDefined, assert, unique} from '../../utils/common-util';
+import {assert, assertIsDefined, unique} from '../../utils/common-util';
 import {modifierPressed, whenVisible} from '../../utils/dom-util';
 import {durationString} from '../../utils/date-util';
 import {charsOnly} from '../../utils/string-util';
@@ -87,7 +87,6 @@
 import {changeViewModelToken} from '../../models/views/change';
 import {formStyles} from '../../styles/form-styles';
 import {isDefined} from '../../types/types';
-import {KnownExperimentId} from '../../services/flags/flags';
 import {
   ReportSource,
   suggestionsServiceToken,
@@ -155,8 +154,6 @@
     suggestionsServiceToken
   );
 
-  private readonly flagsService = getAppContext().flagsService;
-
   constructor() {
     super();
     this.addEventListener('get-ai-fix-for-check-result', () => {
@@ -560,12 +557,15 @@
     if (value === 0) return;
     const valueStr = valueString(value);
     return html`
-      <div class="label ${status}">
-        <span>${label} ${valueStr}</span>
-        <paper-tooltip offset="5" .fitToVisibleBounds=${true}>
-          The check result has (probably) influenced this label vote.
-        </paper-tooltip>
-      </div>
+      <gr-tooltip-content
+        has-tooltip
+        ?position-below=${true}
+        title="The check result has (probably) influenced this label vote."
+      >
+        <div class="label ${status}">
+          <span>${label} ${valueStr}</span>
+        </div>
+      </gr-tooltip-content>
     `;
   }
 
@@ -590,19 +590,25 @@
     if (!link) return;
     const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
     const icon = iconForLink(link.icon);
-    return html`<a
-      href=${link.url}
-      class="link"
-      target="_blank"
-      rel="noopener noreferrer"
-      ><gr-icon
-        icon=${icon.name}
-        ?filled=${icon.filled}
-        aria-label="external link to details"
+    return html`<gr-tooltip-content
+      has-tooltip
+      ?position-below=${true}
+      title=${tooltipText}
+    >
+      <a
+        href=${link.url}
         class="link"
-      ></gr-icon
-      ><paper-tooltip offset="5">${tooltipText}</paper-tooltip></a
-    >`;
+        target="_blank"
+        rel="noopener noreferrer"
+      >
+        <gr-icon
+          icon=${icon.name}
+          ?filled=${!!icon.filled}
+          aria-label="external link to details"
+          class="link"
+        ></gr-icon>
+      </a>
+    </gr-tooltip-content>`;
   }
 
   private renderActions() {
@@ -619,13 +625,17 @@
     }
 
     if (
-      this.flagsService.isEnabled(KnownExperimentId.GET_AI_FIX) &&
       this.getSuggestionsService()?.isGeneratedSuggestedFixEnabled(
         this.result?.codePointers?.[0]?.path
       ) &&
       this.isOwner &&
       // without fixes
       !this.result?.fixes?.length &&
+      // disable on codepointer with range pointing to 0 line
+      this.result?.codePointers?.[0]?.range.start_line !== 0 &&
+      this.result?.codePointers?.[0]?.range.end_line !== 0 &&
+      this.result?.category !== Category.SUCCESS &&
+      this.result?.category !== Category.INFO &&
       !fixAction
     ) {
       getAiFixAction = createGetAiFixAction(this);
@@ -678,29 +688,33 @@
   private renderAction(action?: Action) {
     if (!action) return;
     return html`<gr-checks-action
+      .runOrResult=${this.result}
       context="result-row"
       .action=${action}
     ></gr-checks-action>`;
   }
 
   renderTag(tag: Tag) {
-    return html`<button
-      class="tag ${tag.color}"
-      @click=${(e: MouseEvent) => this.tagClick(e, tag.name)}
+    return html`<gr-tooltip-content
+      has-tooltip
+      ?position-below=${true}
+      title=${tag.tooltip ??
+      'A category tag for this check result. Click to filter.'}
     >
-      <span>${tag.name}</span>
-      <paper-tooltip offset="5" .fitToVisibleBounds=${true}>
-        ${tag.tooltip ??
-        'A category tag for this check result. Click to filter.'}
-      </paper-tooltip>
-    </button>`;
+      <button
+        class="tag ${tag.color}"
+        @click=${(e: MouseEvent) => this.tagClick(e, tag.name)}
+      >
+        <span>${tag.name}</span>
+      </button>
+    </gr-tooltip-content>`;
   }
 
   private renderSuggestionPreview() {
     if (!this.suggestion) return nothing;
     return html`<gr-checks-fix-preview
       .fixSuggestionInfos=${[this.suggestion]}
-      .patchSet=${this.result?.patchset}
+      .patchSet=${this.result?.patchset as PatchSetNumber | undefined}
     ></gr-checks-fix-preview>`;
   }
 
@@ -725,6 +739,8 @@
       fireAlert(this, 'No suitable AI fix could be found');
       return;
     }
+    suggestion.description =
+      ReportSource.GET_AI_FIX_FOR_CHECK + ' ' + suggestion.description;
     this.suggestion = suggestion;
     this.toggleExpanded(/* setExpanded= */ true);
   }
@@ -869,7 +885,7 @@
     return html`
       <gr-checks-fix-preview
         .fixSuggestionInfos=${fixSuggestionInfos}
-        .patchSet=${this.result?.patchset}
+        .patchSet=${this.result?.patchset as PatchSetNumber | undefined}
       ></gr-checks-fix-preview>
     `;
   }
@@ -883,10 +899,12 @@
       <div class="useful">
         <div class="title">${this.notUsefulLabel}</div>
         <gr-checks-action
+          .runOrResult=${this.result}
           @click=${() => (this.notUsefulLabel = 'Thanks!')}
           .action=${useful}
         ></gr-checks-action>
         <gr-checks-action
+          .runOrResult=${this.result}
           @click=${() => (this.notUsefulLabel = 'Sorry about that')}
           .action=${notUseful}
         ></gr-checks-action>
@@ -904,7 +922,11 @@
       target=${ifDefined(target)}
       rel="noopener noreferrer"
     >
-      <gr-icon icon=${icon.name} class="link" ?filled=${icon.filled}></gr-icon>
+      <gr-icon
+        icon=${icon.name}
+        class="link"
+        ?filled=${!!icon.filled}
+      ></gr-icon>
       <span>${text}</span>
     </a>`;
   }
@@ -1344,7 +1366,7 @@
       )
       .slice(0, 4);
     const overflowLinks = links.filter(a => !primaryLinks.includes(a));
-    const overflowLinkItems = overflowLinks.map(link => {
+    const overflowLinkItems: DropdownLink[] = overflowLinks.map(link => {
       return {
         ...link,
         id: link.tooltip,
@@ -1381,15 +1403,20 @@
     if (!link) return;
     const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
     const icon = iconForLink(link.icon);
-    return html`<a href=${link.url} target="_blank" rel="noopener noreferrer"
-      ><gr-icon
-        icon=${icon.name}
-        aria-label=${tooltipText}
-        class="link"
-        ?filled=${icon.filled}
-      ></gr-icon>
-      <paper-tooltip offset="5">${tooltipText}</paper-tooltip></a
-    >`;
+    return html`<gr-tooltip-content
+      has-tooltip
+      ?position-below=${true}
+      title=${tooltipText}
+    >
+      <a href=${link.url} target="_blank" rel="noopener noreferrer">
+        <gr-icon
+          icon=${icon.name}
+          aria-label=${tooltipText}
+          class="link"
+          ?filled=${!!icon.filled}
+        ></gr-icon>
+      </a>
+    </gr-tooltip-content>`;
   }
 
   private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
@@ -1584,18 +1611,21 @@
               class="expandIcon"
               icon=${expanded ? 'expand_less' : 'expand_more'}
             ></gr-icon>
-            <div class="statusIconWrapper">
-              <gr-icon
-                icon=${icon.name}
-                ?filled=${icon.filled}
-                class="statusIcon ${catString}"
-              ></gr-icon>
-              <span class="title">${catString}</span>
-              <span class="count">${this.renderCount(all, filtered)}</span>
-              <paper-tooltip offset="5"
-                >${CATEGORY_TOOLTIPS.get(category)}</paper-tooltip
-              >
-            </div>
+            <gr-tooltip-content
+              has-tooltip
+              ?position-below=${true}
+              title=${ifDefined(CATEGORY_TOOLTIPS.get(category))}
+            >
+              <div class="statusIconWrapper">
+                <gr-icon
+                  icon=${icon.name}
+                  ?filled=${!!icon.filled}
+                  class="statusIcon ${catString}"
+                ></gr-icon>
+                <span class="title">${catString}</span>
+                <span class="count">${this.renderCount(all, filtered)}</span>
+              </div>
+            </gr-tooltip-content>
           </h3>
           <div class="right">
             <gr-button
@@ -1765,7 +1795,7 @@
       const end = run.finishedTimestamp;
       let duration = '';
       if (start && end) {
-        duration = ` in ${durationString(start, end, true)}`;
+        duration = ` ${durationString(start, end, 'in')}`;
       }
       adaptedRun.message = `Completed without results${duration}.`;
     }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results_screenshot_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-results_screenshot_test.ts
new file mode 100644
index 0000000..9838604
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results_screenshot_test.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import '../../test/common-test-setup';
+import './gr-checks-results';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {checksModelToken} from '../../models/checks/checks-model';
+import {setAllFakeRuns} from '../../models/checks/checks-fakes';
+import {resolve} from '../../models/dependency';
+import {GrChecksResults} from './gr-checks-results';
+import {visualDiffDarkTheme} from '../../test/test-utils';
+
+suite('gr-checks-results screenshot', () => {
+  let element: GrChecksResults;
+
+  setup(async () => {
+    element = await fixture<GrChecksResults>(
+      html`<gr-checks-results></gr-checks-results>`
+    );
+    const getChecksModel = resolve(element, checksModelToken);
+    getChecksModel().allRunsSelectedPatchset$.subscribe(
+      runs => (element.runs = runs)
+    );
+    setAllFakeRuns(getChecksModel());
+    await element.updateComplete;
+  });
+
+  test('screenshot', async () => {
+    await visualDiff(element, 'gr-checks-results');
+    await visualDiffDarkTheme(element, 'gr-checks-results');
+  });
+});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
index 1e42a4c..16595d9 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
@@ -11,7 +11,7 @@
   GrResultRow,
 } from './gr-checks-results';
 import {html} from 'lit';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {checksModelToken, RunResult} from '../../models/checks/checks-model';
 import {
   fakeRun0,
@@ -20,7 +20,7 @@
 } from '../../models/checks/checks-fakes';
 import {resolve} from '../../models/dependency';
 import {createLabelInfo} from '../../test/test-data-generators';
-import {queryAndAssert, query, assertIsDefined} from '../../utils/common-util';
+import {assertIsDefined, query, queryAndAssert} from '../../utils/common-util';
 import {PatchSetNumber} from '../../api/rest-api';
 import {GrDropdownList} from '../shared/gr-dropdown-list/gr-dropdown-list';
 
@@ -53,9 +53,6 @@
       /* HTML */ `
         <div class="approved label">
           <span> test-label +1 </span>
-          <paper-tooltip offset="5" role="tooltip" tabindex="-1">
-            The check result has (probably) influenced this label vote.
-          </paper-tooltip>
         </div>
       `
     );
@@ -66,27 +63,33 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-      <div class="flex">
-        <gr-hovercard-run> </gr-hovercard-run>
-        <div class="name" role="button" tabindex="0">
-          FAKE Error Finder Finder Finder Finder Finder Finder Finder
+        <div class="flex">
+          <gr-hovercard-run> </gr-hovercard-run>
+          <div class="name" role="button" tabindex="0">
+            FAKE Error Finder Finder Finder Finder Finder Finder Finder
+          </div>
+          <div class="space"></div>
         </div>
-        <div class="space"></div>
-      </div>
         <div class="summary-cell">
-          <a class="link"
-             href="https://www.google.com"
-             target="_blank"
-             rel="noopener noreferrer">
-            <gr-icon
-              icon="open_in_new"
-              aria-label="external link to details"
+          <gr-tooltip-content
+            has-tooltip=""
+            position-below=""
+            title="Link to details"
+          >
+            <a
               class="link"
-            ></gr-icon>
-            <paper-tooltip offset="5" role="tooltip" tabindex="-1">
-              Link to details
-            </paper-tooltip>
-          </a>
+              href="https://www.google.com"
+              rel="noopener noreferrer"
+              target="_blank"
+            >
+              <gr-icon
+                aria-label="external link to details"
+                class="link"
+                icon="open_in_new"
+              >
+              </gr-icon>
+            </a>
+          </gr-tooltip-content>
           <div
             class="summary"
             title="I would like to point out this error: 1 is not equal to 2!"
@@ -95,40 +98,37 @@
           </div>
           <div class="message"></div>
           <div class="tags">
-            <button class="tag">
-              <span> OBSOLETE </span>
-              <paper-tooltip
-                offset="5"
-                role="tooltip"
-                tabindex="-1"
-              >
-                A category tag for this check result. Click to filter.
-              </paper-tooltip>
-            </button>
-            <button class="tag">
-              <span> E2E </span>
-              <paper-tooltip
-                offset="5"
-                role="tooltip"
-                tabindex="-1"
-              >
-                A category tag for this check result. Click to filter.
-              </paper-tooltip>
-            </button>
+            <gr-tooltip-content
+              has-tooltip=""
+              position-below=""
+              title="A category tag for this check result. Click to filter."
+            >
+              <button class="tag">
+                <span> OBSOLETE </span>
+              </button>
+            </gr-tooltip-content>
+            <gr-tooltip-content
+              has-tooltip=""
+              position-below=""
+              title="A category tag for this check result. Click to filter."
+            >
+              <button class="tag">
+                <span> E2E </span>
+              </button>
+            </gr-tooltip-content>
           </div>
         </div>
         <div
           aria-checked="false"
           aria-label="Expand result row"
           class="show-hide"
-          hidden
+          hidden=""
           role="switch"
           tabindex="0"
         >
-          <gr-icon icon="expand_more"></gr-icon>
+          <gr-icon icon="expand_more"> </gr-icon>
         </div>
-      </div>
-    `
+      `
     );
   });
 
@@ -352,53 +352,76 @@
               </div>
             </div>
             <div class="right">
-              <a
-                href="https://www.google.com"
-                target="_blank"
-                rel="noopener noreferrer"
+              <gr-tooltip-content
+                has-tooltip=""
+                position-below=""
+                title="Fake Bug Report 1"
               >
-                <gr-icon
-                  icon="bug_report"
-                  filled
-                  aria-label="Fake Bug Report 1"
-                  class="link"
-                ></gr-icon>
-                <paper-tooltip offset="5"> </paper-tooltip>
-              </a>
-              <a
-                href="https://www.google.com"
-                target="_blank"
-                rel="noopener noreferrer"
+                <a
+                  href="https://www.google.com"
+                  rel="noopener noreferrer"
+                  target="_blank"
+                >
+                  <gr-icon
+                    aria-label="Fake Bug Report 1"
+                    class="link"
+                    filled=""
+                    icon="bug_report"
+                  >
+                  </gr-icon>
+                </a>
+              </gr-tooltip-content>
+              <gr-tooltip-content
+                has-tooltip=""
+                position-below=""
+                title="Fake Link 1"
               >
-                <gr-icon
-                  icon="open_in_new"
-                  aria-label="Fake Link 1"
-                  class="link"
-                ></gr-icon>
-                <paper-tooltip offset="5"> </paper-tooltip>
-              </a>
-              <a
-                href="https://www.google.com"
-                target="_blank"
-                rel="noopener noreferrer"
+                <a
+                  href="https://www.google.com"
+                  rel="noopener noreferrer"
+                  target="_blank"
+                >
+                  <gr-icon
+                    aria-label="Fake Link 1"
+                    class="link"
+                    icon="open_in_new"
+                  >
+                  </gr-icon>
+                </a>
+              </gr-tooltip-content>
+              <gr-tooltip-content
+                has-tooltip=""
+                position-below=""
+                title="Fake Code Link"
               >
-                <gr-icon icon="code" aria-label="Fake Code Link" class="link">
-                </gr-icon>
-                <paper-tooltip offset="5"> </paper-tooltip>
-              </a>
-              <a
-                href="https://www.google.com"
-                target="_blank"
-                rel="noopener noreferrer"
+                <a
+                  href="https://www.google.com"
+                  rel="noopener noreferrer"
+                  target="_blank"
+                >
+                  <gr-icon aria-label="Fake Code Link" class="link" icon="code">
+                  </gr-icon>
+                </a>
+              </gr-tooltip-content>
+              <gr-tooltip-content
+                has-tooltip=""
+                position-below=""
+                title="Fake Image Link"
               >
-                <gr-icon
-                  icon="image"
-                  filled
-                  aria-label="Fake Image Link"
-                  class="link"
-                ></gr-icon>
-                <paper-tooltip offset="5"> </paper-tooltip>
-              </a>
+                <a
+                  href="https://www.google.com"
+                  rel="noopener noreferrer"
+                  target="_blank"
+                >
+                  <gr-icon
+                    aria-label="Fake Image Link"
+                    class="link"
+                    filled=""
+                    icon="image"
+                  >
+                  </gr-icon>
+                </a>
+              </gr-tooltip-content>
               <div class="space"></div>
               <gr-checks-action context="results"> </gr-checks-action>
               <gr-dropdown
@@ -407,10 +430,8 @@
                 link=""
                 vertical-offset="32"
               >
-                <gr-icon
-                  icon="more_vert"
-                  aria-labelledby="moreMessage"
-                ></gr-icon>
+                <gr-icon aria-labelledby="moreMessage" icon="more_vert">
+                </gr-icon>
                 <span id="moreMessage"> More </span>
               </gr-dropdown>
             </div>
@@ -419,18 +440,20 @@
         <div class="body">
           <div class="error expanded">
             <div class="categoryHeader error">
-              <h3 class="left heading-3">
-                <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
-                <div class="statusIconWrapper">
-                  <gr-icon
-                    icon="error"
-                    filled
-                    class="error statusIcon"
-                  ></gr-icon>
-                  <span class="title"> error </span>
-                  <span class="count"> (3) </span>
-                  <paper-tooltip offset="5"> </paper-tooltip>
-                </div>
+              <h3 class="heading-3 left">
+                <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
+                <gr-tooltip-content
+                  has-tooltip=""
+                  position-below=""
+                  title="Must be fixed and is blocking submit"
+                >
+                  <div class="statusIconWrapper">
+                    <gr-icon class="error statusIcon" filled="" icon="error">
+                    </gr-icon>
+                    <span class="title"> error </span>
+                    <span class="count"> (3) </span>
+                  </div>
+                </gr-tooltip-content>
               </h3>
               <div class="right">
                 <gr-button link=""> Expand All </gr-button>
@@ -441,11 +464,12 @@
             >
             </gr-result-row>
             <gr-result-row
-              isexpandable
               class="FAKEErrorFinderFinderFinderFinderFinderFinderFinder"
+              isexpandable=""
             >
             </gr-result-row>
-            <gr-result-row isexpandable class="FAKESuperCheck"> </gr-result-row>
+            <gr-result-row class="FAKESuperCheck" isexpandable="">
+            </gr-result-row>
             <table class="resultsTable">
               <thead>
                 <tr class="headerRow">
@@ -459,21 +483,31 @@
           </div>
           <div class="expanded warning">
             <div class="categoryHeader warning">
-              <h3 class="left heading-3">
-                <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
-                <div class="statusIconWrapper">
-                  <gr-icon icon="warning" filled class="warning statusIcon">
-                  </gr-icon>
-                  <span class="title"> warning </span>
-                  <span class="count"> (1) </span>
-                  <paper-tooltip offset="5"> </paper-tooltip>
-                </div>
+              <h3 class="heading-3 left">
+                <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
+                <gr-tooltip-content
+                  has-tooltip=""
+                  position-below=""
+                  title="Should be checked but is not blocking submit"
+                >
+                  <div class="statusIconWrapper">
+                    <gr-icon
+                      class="statusIcon warning"
+                      filled=""
+                      icon="warning"
+                    >
+                    </gr-icon>
+                    <span class="title"> warning </span>
+                    <span class="count"> (1) </span>
+                  </div>
+                </gr-tooltip-content>
               </h3>
               <div class="right">
                 <gr-button link=""> Expand All </gr-button>
               </div>
             </div>
-            <gr-result-row class="FAKESuperCheck" isexpandable> </gr-result-row>
+            <gr-result-row class="FAKESuperCheck" isexpandable="">
+            </gr-result-row>
             <table class="resultsTable">
               <thead>
                 <tr class="headerRow">
@@ -487,14 +521,19 @@
           </div>
           <div class="collapsed info">
             <div class="categoryHeader info">
-              <h3 class="left heading-3">
-                <gr-icon icon="expand_more" class="expandIcon"></gr-icon>
-                <div class="statusIconWrapper">
-                  <gr-icon icon="info" class="info statusIcon"></gr-icon>
-                  <span class="title"> info </span>
-                  <span class="count"> (3) </span>
-                  <paper-tooltip offset="5"> </paper-tooltip>
-                </div>
+              <h3 class="heading-3 left">
+                <gr-icon class="expandIcon" icon="expand_more"> </gr-icon>
+                <gr-tooltip-content
+                  has-tooltip=""
+                  position-below=""
+                  title="Does not have to be checked, for your information only"
+                >
+                  <div class="statusIconWrapper">
+                    <gr-icon class="info statusIcon" icon="info"> </gr-icon>
+                    <span class="title"> info </span>
+                    <span class="count"> (3) </span>
+                  </div>
+                </gr-tooltip-content>
               </h3>
               <div class="right">
                 <gr-button hidden="" link=""> Expand All </gr-button>
@@ -503,15 +542,20 @@
           </div>
           <div class="collapsed success">
             <div class="categoryHeader empty success">
-              <h3 class="left heading-3">
-                <gr-icon icon="expand_more" class="expandIcon"></gr-icon>
-                <div class="statusIconWrapper">
-                  <gr-icon icon="check_circle" class="statusIcon success">
-                  </gr-icon>
-                  <span class="title"> success </span>
-                  <span class="count"> (0) </span>
-                  <paper-tooltip offset="5"> </paper-tooltip>
-                </div>
+              <h3 class="heading-3 left">
+                <gr-icon class="expandIcon" icon="expand_more"> </gr-icon>
+                <gr-tooltip-content
+                  has-tooltip=""
+                  position-below=""
+                  title="Successful runs without results and individual successful results"
+                >
+                  <div class="statusIconWrapper">
+                    <gr-icon class="statusIcon success" icon="check_circle">
+                    </gr-icon>
+                    <span class="title"> success </span>
+                    <span class="count"> (0) </span>
+                  </div>
+                </gr-tooltip-content>
               </h3>
               <div class="right">
                 <gr-button hidden="" link=""> Expand All </gr-button>
@@ -521,7 +565,6 @@
         </div>
       `,
       {
-        ignoreChildren: ['paper-tooltip'],
         ignoreAttributes: ['tabindex', 'aria-disabled', 'role'],
       }
     );
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 662c560..bc4398f 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../shared/gr-icon/gr-icon';
+import '../shared/gr-tooltip-content/gr-tooltip-content';
 import {classMap} from 'lit/directives/class-map.js';
 import './gr-hovercard-run';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
@@ -15,12 +16,12 @@
   ALL_ATTEMPTS,
   AttemptChoice,
   attemptChoiceLabel,
-  LATEST_ATTEMPT,
   AttemptDetail,
   compareByWorstCategory,
   headerForStatus,
   iconFor,
   iconForRun,
+  LATEST_ATTEMPT,
   PRIMARY_STATUS_ACTIONS,
   primaryRunAction,
 } from '../../models/checks/checks-util';
@@ -58,6 +59,8 @@
 import {when} from 'lit/directives/when.js';
 import {changeViewModelToken} from '../../models/views/change';
 import {formStyles} from '../../styles/form-styles';
+import '@material/web/radio/radio';
+import {materialStyles} from '../../styles/gr-material-styles';
 
 @customElement('gr-checks-run')
 export class GrChecksRun extends LitElement {
@@ -65,6 +68,7 @@
     return [
       formStyles,
       sharedStyles,
+      materialStyles,
       css`
         :host {
           display: block;
@@ -172,7 +176,7 @@
         .attemptDetail {
           /* This is thick-border (6) + spacing-m (8) + icon (20) + padding. */
           padding-left: 39px;
-          padding-top: var(--spacing-s);
+          padding-top: var(--spacing-l);
         }
         .attemptDetail input {
           width: 14px;
@@ -242,6 +246,7 @@
 
   override render() {
     if (!this.shouldRender) return html`<div class="chip">Loading ...</div>`;
+    if (!this.run) return nothing;
 
     const icon = iconForRun(this.run);
     const classes = {
@@ -265,7 +270,7 @@
           <gr-icon
             class=${icon.name}
             icon=${icon.name}
-            ?filled=${icon.filled}
+            ?filled=${!!icon.filled}
           ></gr-icon>
           ${this.renderAdditionalIcon()}
           <span class="name">${this.run.checkName}</span>
@@ -278,6 +283,7 @@
         <div class="right">
           ${action
             ? html`<gr-checks-action
+                .runOrResult=${this.run}
                 context="runs"
                 .action=${action}
               ></gr-checks-action>`
@@ -315,21 +321,21 @@
               r.checkName === this.run?.checkName &&
               r.pluginName === this.run?.pluginName
           )}
-          .attempt=${attempt}
+          .attempt=${attempt as number}
         ></gr-hovercard-run>`
       )}
-      <input
-        type="radio"
+      <md-radio
         id=${id}
         name=${`${checkNameId}-attempt-choice`}
-        .checked=${selected}
+        ?checked=${selected}
         ?disabled=${!selected && wasNotRun}
         @change=${() => this.handleAttemptChange(attempt)}
-      />
+      >
+      </md-radio>
       <gr-icon
         icon=${icon.name}
         class=${icon.name}
-        ?filled=${icon.filled}
+        ?filled=${!!icon.filled}
       ></gr-icon>
       <label for=${id}>
         ${attemptChoiceLabel(attempt)}${wasNotRun ? ' (not run)' : ''}
@@ -346,7 +352,7 @@
     if (!this.run.finishedTimestamp) return;
     const now = new Date();
     if (this.run.finishedTimestamp.getTime() < now.getTime()) return;
-    const eta = durationString(new Date(), this.run.finishedTimestamp, true);
+    const eta = durationString(new Date(), this.run.finishedTimestamp);
     return html`<span class="eta">ETA: ${eta}</span>`;
   }
 
@@ -354,18 +360,23 @@
     const link = this.run.statusLink;
     if (!link) return;
     return html`
-      <a
-        href=${link}
-        target="_blank"
-        rel="noopener noreferrer"
-        @click=${this.onLinkClick}
-        ><gr-icon
-          icon="open_in_new"
-          class="statusLinkIcon"
-          aria-label="external link to run status details"
-        ></gr-icon>
-        <paper-tooltip offset="5">Link to run status details</paper-tooltip>
-      </a>
+      <gr-tooltip-content
+        has-tooltip
+        ?position-below=${true}
+        title="Link to run status details"
+      >
+        <a
+          href=${link}
+          target="_blank"
+          rel="noopener noreferrer"
+          @click=${this.onLinkClick}
+          ><gr-icon
+            icon="open_in_new"
+            class="statusLinkIcon"
+            aria-label="external link to run status details"
+          ></gr-icon>
+        </a>
+      </gr-tooltip-content>
     `;
   }
 
@@ -396,7 +407,7 @@
       <gr-icon
         icon=${icon.name}
         class=${icon.name}
-        ?filled=${icon.filled}
+        ?filled=${!!icon.filled}
       ></gr-icon>
     `;
   }
@@ -799,8 +810,10 @@
     links: Link[] = [],
     summaryMessage: string | undefined = undefined
   ) {
+    const checksModel = this.getChecksModel();
+    checksModel.updateStateSetProvider(plugin, ChecksPatchset.LATEST);
     const newRuns = this.runs.includes(runs[0]) ? [] : runs;
-    this.getChecksModel().updateStateSetResults(
+    checksModel.updateStateSetResults(
       plugin,
       newRuns,
       actions,
@@ -903,7 +916,7 @@
         <gr-button link @click=${() => this.toggle('f3', [fakeRun3])}
           >3</gr-button
         >
-        <gr-button link @click="${() => this.toggle('f4', fakeRun4Att)}}"
+        <gr-button link @click=${() => this.toggle('f4', fakeRun4Att)}
           >4</gr-button
         >
         <gr-button link @click=${() => this.toggle('f5', [fakeRun5])}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
index a858e4d..871a598 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs_test.ts
@@ -7,7 +7,7 @@
 import './gr-checks-runs';
 import {GrChecksRun, GrChecksRuns} from './gr-checks-runs';
 import {html} from 'lit';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {checksModelToken} from '../../models/checks/checks-model';
 import {fakeRun0, setAllFakeRuns} from '../../models/checks/checks-fakes';
 import {resolve} from '../../models/dependency';
@@ -53,7 +53,7 @@
               role="switch"
             >
               <div>
-                <gr-icon icon="chevron_left" class="expandIcon"></gr-icon>
+                <gr-icon class="expandIcon" icon="chevron_left"> </gr-icon>
               </div>
             </gr-button>
           </gr-tooltip-content>
@@ -64,7 +64,8 @@
           </div>
           <div class="right">
             <div class="message">
-              Error while fetching results for test-plugin-name: <br />
+              Error while fetching results for test-plugin-name:
+              <br />
               test-error-message
             </div>
           </div>
@@ -76,32 +77,32 @@
         />
         <div class="expanded running">
           <div class="sectionHeader">
-            <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
+            <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
             <h3 class="heading-3">Running / Scheduled (2)</h3>
           </div>
           <div class="sectionRuns">
-            <gr-checks-run></gr-checks-run>
-            <gr-checks-run></gr-checks-run>
+            <gr-checks-run> </gr-checks-run>
+            <gr-checks-run> </gr-checks-run>
           </div>
         </div>
         <div class="completed expanded">
           <div class="sectionHeader">
-            <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
+            <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
             <h3 class="heading-3">Completed (3)</h3>
           </div>
           <div class="sectionRuns">
-            <gr-checks-run></gr-checks-run>
-            <gr-checks-run></gr-checks-run>
-            <gr-checks-run></gr-checks-run>
+            <gr-checks-run> </gr-checks-run>
+            <gr-checks-run> </gr-checks-run>
+            <gr-checks-run> </gr-checks-run>
           </div>
         </div>
         <div class="expanded runnable">
           <div class="sectionHeader">
-            <gr-icon icon="expand_less" class="expandIcon"></gr-icon>
+            <gr-icon class="expandIcon" icon="expand_less"> </gr-icon>
             <h3 class="heading-3">Not run (1)</h3>
           </div>
           <div class="sectionRuns">
-            <gr-checks-run></gr-checks-run>
+            <gr-checks-run> </gr-checks-run>
           </div>
         </div>
       `,
@@ -216,26 +217,28 @@
             <gr-checks-attempt> </gr-checks-attempt>
           </div>
           <div class="right"></div>
+        </div>
+        <div class="attemptDetails" hidden="">
+          <div class="attemptDetail">
+            <md-radio
+              checked=""
+              id="attempt-latest"
+              name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
+              tabindex="0"
+            >
+            </md-radio>
+            <gr-icon icon=""> </gr-icon>
+            <label for="attempt-latest"> Latest Attempt </label>
           </div>
-          <div class="attemptDetails" hidden="">
-            <div class="attemptDetail">
-              <input
-                id="attempt-latest"
-                name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
-                type="radio"
-              />
-              <gr-icon icon=""> </gr-icon>
-              <label for="attempt-latest"> Latest Attempt </label>
-            </div>
-            <div class="attemptDetail">
-              <input
-                id="attempt-all"
-                name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
-                type="radio"
-              />
-              <gr-icon icon=""> </gr-icon>
-              <label for="attempt-all"> All Attempts </label>
-            </div>
+          <div class="attemptDetail">
+            <md-radio
+              id="attempt-all"
+              name="fakeerrorfinderfinderfinderfinderfinderfinderfinder-attempt-choice"
+              tabindex="-1"
+            >
+            </md-radio>
+            <gr-icon icon=""> </gr-icon>
+            <label for="attempt-all"> All Attempts </label>
           </div>
         </div>
       `
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index c1bcdc1..673f279 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -3,7 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
   CheckResult,
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
index f8e4e69..b0f48cd 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab_test.ts
@@ -7,7 +7,7 @@
 import {html} from 'lit';
 import './gr-checks-tab';
 import {GrChecksTab} from './gr-checks-tab';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {checksModelToken} from '../../models/checks/checks-model';
 import {setAllFakeRuns} from '../../models/checks/checks-fakes';
 import {resolve} from '../../models/dependency';
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util.ts b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
index f1a3fb9..0f844ae 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-util.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
@@ -3,10 +3,13 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import {Category, RunStatus} from '../../api/checks';
 import {CheckRun, RunResult} from '../../models/checks/checks-model';
 import {
   ALL_ATTEMPTS,
   AttemptChoice,
+  getResultsOf,
+  hasResultsOf,
   LATEST_ATTEMPT,
 } from '../../models/checks/checks-util';
 import {fire} from '../../utils/event-util';
@@ -44,3 +47,32 @@
     regExp.test(result.message ?? '')
   );
 }
+
+export function countErrorRunsForLabel(
+  runs: CheckRun[],
+  labels: string[]
+): {errorRuns: CheckRun[]; errorRunsCount: number} {
+  const errorRuns = runs
+    .filter(run => hasResultsOf(run, Category.ERROR))
+    .filter(run => run.labelName && labels.includes(run.labelName));
+  const errorRunsCount = errorRuns.reduce(
+    (sum, run) => sum + getResultsOf(run, Category.ERROR).length,
+    0
+  );
+  return {errorRuns, errorRunsCount};
+}
+
+export function countRunningRunsForLabel(
+  runs: CheckRun[],
+  labels: string[]
+): {runningRuns: CheckRun[]; runningRunsCount: number} {
+  const runningRuns = runs
+    .filter(r => r.isLatestAttempt)
+    .filter(
+      r => r.status === RunStatus.RUNNING || r.status === RunStatus.SCHEDULED
+    )
+    .filter(run => run.labelName && labels.includes(run.labelName));
+
+  const runningRunsCount = runningRuns.length;
+  return {runningRuns, runningRunsCount};
+}
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
index adfab0c..aa6c957 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
@@ -3,9 +3,8 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-tooltip/paper-tooltip';
 import '../shared/gr-icon/gr-icon';
-import {LitElement, css, html, PropertyValues, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {RunResult} from '../../models/checks/checks-model';
 import {
@@ -18,7 +17,7 @@
 import './gr-checks-results';
 import './gr-hovercard-run';
 import {fontStyles} from '../../styles/gr-font-styles';
-import {Action} from '../../api/checks';
+import {Action, Category} from '../../api/checks';
 import {assertIsDefined} from '../../utils/common-util';
 import {resolve} from '../../models/dependency';
 import {commentsModelToken} from '../../models/comments/comments-model';
@@ -26,12 +25,15 @@
 import {changeModelToken} from '../../models/change/change-model';
 import {getAppContext} from '../../services/app-context';
 import {Interaction} from '../../constants/reporting';
-import {KnownExperimentId} from '../../services/flags/flags';
 import {
   ReportSource,
   suggestionsServiceToken,
 } from '../../services/suggestions/suggestions-service';
-import {FixSuggestionInfo, RevisionPatchSetNum} from '../../api/rest-api';
+import {
+  FixSuggestionInfo,
+  PatchSetNumber,
+  RevisionPatchSetNum,
+} from '../../api/rest-api';
 import {when} from 'lit/directives/when.js';
 import {fireAlert} from '../../utils/event-util';
 
@@ -73,8 +75,6 @@
     suggestionsServiceToken
   );
 
-  private readonly flagsService = getAppContext().flagsService;
-
   static override get styles() {
     return [
       fontStyles,
@@ -194,7 +194,7 @@
       <div class="${cat} container font-normal">
         <div class="header" @click=${this.toggleExpandedClick}>
           <div class="icon">
-            <gr-icon icon=${icon.name} ?filled=${icon.filled}></gr-icon>
+            <gr-icon icon=${icon.name} ?filled=${!!icon.filled}></gr-icon>
           </div>
           <div class="name">
             <gr-hovercard-run .run=${this.result}></gr-hovercard-run>
@@ -257,7 +257,7 @@
     if (!this.suggestion) return nothing;
     return html`<gr-checks-fix-preview
       .fixSuggestionInfos=${[this.suggestion]}
-      .patchSet=${this.result?.patchset}
+      .patchSet=${this.result?.patchset as PatchSetNumber | undefined}
     ></gr-checks-fix-preview>`;
   }
 
@@ -285,9 +285,6 @@
   }
 
   private shouldShowAIFixButton() {
-    if (!this.flagsService.isEnabled(KnownExperimentId.GET_AI_FIX)) {
-      return false;
-    }
     if (
       !this.getSuggestionsService()?.isGeneratedSuggestedFixEnabled(
         this.result?.codePointers?.[0].path
@@ -298,11 +295,23 @@
     if (this.result?.fixes?.length) {
       return false;
     }
+    if (
+      this.result?.category === Category.SUCCESS ||
+      this.result?.category === Category.INFO
+    ) {
+      return false;
+    }
+    // disable on codepointer with range pointing to 0 line
+    if (
+      this.result?.codePointers?.[0]?.range.start_line === 0 ||
+      this.result?.codePointers?.[0]?.range.end_line === 0
+    ) {
+      return false;
+    }
     return this.isOwner;
   }
 
   private renderPleaseFixButton() {
-    if (this.isOwner) return nothing;
     const action: Action = {
       name: 'Please Fix',
       callback: () => {
@@ -315,6 +324,7 @@
       <gr-checks-action
         id="please-fix"
         context="diff-fix"
+        .runOrResult=${this.result}
         .action=${action}
       ></gr-checks-action>
     `;
@@ -328,6 +338,7 @@
       <gr-checks-action
         id="show-fix"
         context="diff-fix"
+        .runOrResult=${this.result}
         .action=${action}
       ></gr-checks-action>
     `;
@@ -383,6 +394,8 @@
       fireAlert(this, 'No suitable AI fix could be found');
       return;
     }
+    suggestion.description =
+      ReportSource.GET_AI_FIX_FOR_CHECK + ' ' + suggestion.description;
     this.suggestion = suggestion;
     this.isExpanded = true;
   }
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result_screenshot_test.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result_screenshot_test.ts
new file mode 100644
index 0000000..0e6e9e2
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result_screenshot_test.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../test/common-test-setup';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {visualDiffDarkTheme} from '../../test/test-utils';
+import {GrDiffCheckResult} from './gr-diff-check-result';
+import './gr-diff-check-result';
+import {fakeRun1} from '../../models/checks/checks-fakes';
+import {RunResult} from '../../models/checks/checks-model';
+
+suite('gr-diff-check-result screenshot tests', () => {
+  let element: GrDiffCheckResult;
+
+  setup(async () => {
+    element = await fixture(
+      html`<gr-diff-check-result></gr-diff-check-result>`
+    );
+  });
+
+  test('collapsed', async () => {
+    element.result = {...fakeRun1, ...fakeRun1.results?.[0]} as RunResult;
+    await element.updateComplete;
+
+    await visualDiff(element, 'gr-diff-check-result-collapsed');
+    await visualDiffDarkTheme(element, 'gr-diff-check-result-collapsed');
+  });
+
+  test('expanded', async () => {
+    element.result = {...fakeRun1, ...fakeRun1.results?.[2]} as RunResult;
+    element.isExpanded = true;
+    await element.updateComplete;
+
+    await visualDiff(element, 'gr-diff-check-result-expanded');
+    await visualDiffDarkTheme(element, 'gr-diff-check-result-expanded');
+  });
+});
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
index 49c3744..42d17e1 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
@@ -10,28 +10,17 @@
 import {queryAndAssert} from '../../utils/common-util';
 import './gr-diff-check-result';
 import {GrDiffCheckResult} from './gr-diff-check-result';
-import {getAppContext} from '../../services/app-context';
-import {KnownExperimentId} from '../../services/flags/flags';
 import {GrButton} from '../shared/gr-button/gr-button';
 import {suggestionsServiceToken} from '../../services/suggestions/suggestions-service';
 import {testResolver} from '../../test/common-test-setup';
 
 suite('gr-diff-check-result tests', () => {
   let element: GrDiffCheckResult;
-  let flagsService: any;
   let suggestionsService: any;
 
   setup(async () => {
-    flagsService = getAppContext().flagsService;
     suggestionsService = testResolver(suggestionsServiceToken);
 
-    // Enable AI fix feature flag
-    sinon
-      .stub(flagsService, 'isEnabled')
-      .callsFake(
-        (id: KnownExperimentId) => id === KnownExperimentId.GET_AI_FIX
-      );
-
     sinon
       .stub(suggestionsService, 'isGeneratedSuggestedFixEnabled')
       .returns(true);
@@ -127,6 +116,15 @@
       `
     );
   });
+
+  test('shows please-fix button for author', async () => {
+    element.result = {...fakeRun1, ...fakeRun1.results?.[0]} as RunResult;
+    element.isOwner = true;
+    await element.updateComplete;
+    const button = queryAndAssert(element, '#please-fix');
+    assert.isOk(button);
+  });
+
   suite('AI fix button', () => {
     setup(async () => {
       element.result = {
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index b38a30e..f1c2023 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -133,7 +133,7 @@
             <div class="chip">
               <gr-icon
                 icon=${chipIcon.name}
-                ?filled=${chipIcon.filled}
+                ?filled=${!!chipIcon.filled}
               ></gr-icon>
               <span>${this.run.status}</span>
             </div>
@@ -144,7 +144,7 @@
             <gr-icon
               icon=${icon.name}
               class=${icon.name}
-              ?filled=${icon.filled}
+              ?filled=${!!icon.filled}
             ></gr-icon>
           </div>
           <div class="sectionContent">
@@ -229,7 +229,11 @@
     return html`
       <div>
         <div class="attemptIcon">
-          <gr-icon class=${icon.name} icon=${icon.name} ?filled=${icon.filled}>
+          <gr-icon
+            class=${icon.name}
+            icon=${icon.name}
+            ?filled=${!!icon.filled}
+          >
           </gr-icon>
         </div>
         <div class="attemptNumber">${ordinal(attemptNumber)}</div>
@@ -278,8 +282,7 @@
             <div>
               ${durationString(
                 this.run.startedTimestamp,
-                this.run.finishedTimestamp,
-                true
+                this.run.finishedTimestamp
               )}
             </div>
           </div>`
@@ -289,9 +292,7 @@
       this.run.finishedTimestamp && this.run.status === RunStatus.RUNNING
         ? html`<div class="row">
             <div class="title">ETA</div>
-            <div>
-              ${durationString(new Date(), this.run.finishedTimestamp, true)}
-            </div>
+            <div>${durationString(new Date(), this.run.finishedTimestamp)}</div>
           </div>`
         : '';
 
@@ -358,6 +359,7 @@
             <gr-checks-action
               context="hovercard"
               .eventTarget=${this._target}
+              .runOrResult=${this.run}
               .action=${action}
             ></gr-checks-action>
           </div>
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
index 2a7d98d..e25d0d2 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -6,9 +6,9 @@
 import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import './gr-hovercard-run';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrHovercardRun} from './gr-hovercard-run';
-import {fakeRun4Att, fakeRun4_4} from '../../models/checks/checks-fakes';
+import {fakeRun4_4, fakeRun4Att} from '../../models/checks/checks-fakes';
 import {createAttemptMap} from '../../models/checks/checks-util';
 import {CheckRun} from '../../models/checks/checks-model';
 
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index 644871a4..70780cd 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -10,11 +10,12 @@
 import {fire} from '../../../utils/event-util';
 import {DropdownContent} from '../../shared/gr-dropdown/gr-dropdown';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
 import {subscribe} from '../../lit/subscription-controller';
+import '@material/web/icon/icon';
 
 const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
 
@@ -101,6 +102,9 @@
           width: 2em;
           vertical-align: middle;
         }
+        md-icon[filled] {
+          font-variation-settings: 'FILL' 1;
+        }
       `,
     ];
   }
@@ -112,9 +116,10 @@
       .topContent=${this.topContent}
       @tap-item-shortcuts=${this.handleShortcutsTap}
       .horizontalAlign=${'right'}
+      .verticalOffset=${40}
     >
       ${this.showMobile && !this.hasAvatars
-        ? html`<gr-icon icon="account_circle" filled></gr-icon>`
+        ? html`<md-icon filled>account_circle</md-icon>`
         : html`<span ?hidden=${this.hasAvatars}
             >${this.accountName(this.account)}</span
           >`}
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts
index 06dc3f8..9bb5f6e 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts
@@ -5,7 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import './gr-account-dropdown';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrAccountDropdown} from './gr-account-dropdown';
 import {AccountInfo} from '../../../types/common';
 import {createServerInfo} from '../../../test/test-data-generators';
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
index 779aecf..51dc146 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
@@ -4,13 +4,15 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../shared/gr-dialog/gr-dialog';
+import '../../shared/gr-html-renderer/gr-html-renderer';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {fireNoBubbleNoCompose} from '../../../utils/event-util';
 import {configModelToken} from '../../../models/config/config-model';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
+import {when} from 'lit/directives/when.js';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -33,6 +35,9 @@
   @property({type: String})
   text?: string;
 
+  @property({attribute: false})
+  htmlContent?: string;
+
   @state() loginUrl = '';
 
   @state() loginText = '';
@@ -75,6 +80,11 @@
         .signInLink {
           text-decoration: none;
         }
+        .plaintext {
+          font: inherit;
+          white-space: var(--linked-text-white-space, pre-wrap);
+          word-wrap: var(--linked-text-word-wrap, break-word);
+        }
       `,
     ];
   }
@@ -91,7 +101,16 @@
         confirm-on-enter=""
       >
         <div class="header" slot="header">An error occurred</div>
-        <div class="main" slot="main">${this.text}</div>
+        ${when(
+          this.htmlContent,
+          () =>
+            html`<div class="main" slot="main">
+              <gr-html-renderer
+                .htmlContent=${this.htmlContent}
+              ></gr-html-renderer>
+            </div>`,
+          () => html`<div class="main" slot="main">${this.text}</div>`
+        )}
         ${this.renderSignButton()}
       </gr-dialog>
     `;
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
index 148b5c4..8185f11 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
@@ -3,7 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {mockPromise, queryAndAssert} from '../../../test/test-utils';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index fc59980..846d4c6 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -21,12 +21,12 @@
 import {windowLocationReload} from '../../../utils/dom-util';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {fireIronAnnounce} from '../../../utils/event-util';
-import {LitElement, html} from 'lit';
+import {html, LitElement} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {authServiceToken} from '../../../services/gr-auth/gr-auth';
 import {resolve} from '../../../models/dependency';
 import {modalStyles} from '../../../styles/gr-modal-styles';
-import {ironAnnouncerRequestAvailability} from '../../polymer-util';
+import {grAnnouncerRequestAvailability} from '../../lit-util';
 
 const HIDE_ALERT_TIMEOUT_MS = 10 * 1000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -129,7 +129,7 @@
     document.addEventListener('show-auth-required', this.handleAuthRequired);
     document.addEventListener('auth-error', this.handleAuthError);
 
-    ironAnnouncerRequestAvailability();
+    grAnnouncerRequestAvailability();
   }
 
   override disconnectedCallback() {
@@ -239,15 +239,28 @@
         } else if (response.status === 429) {
           this.showQuotaExceeded({status, statusText});
         } else {
-          this.showErrorDialog(
-            constructServerErrorMsg({
-              status,
-              statusText,
-              errorText,
-              url,
-              trace,
-            })
-          );
+          const contentType = response.headers?.get('Content-Type') ?? '';
+          if (contentType.includes('text/html')) {
+            this.showErrorDialog(
+              constructServerErrorMsg({
+                status,
+                statusText,
+                url,
+                trace,
+              }),
+              {htmlContent: errorText}
+            );
+          } else {
+            this.showErrorDialog(
+              constructServerErrorMsg({
+                status,
+                statusText,
+                errorText,
+                url,
+                trace,
+              })
+            );
+          }
         }
       }
       this.reporting.error('Server error', new Error(errorText));
@@ -510,9 +523,13 @@
   };
 
   // private but used in tests
-  showErrorDialog(message: string, options?: {showSignInButton?: boolean}) {
+  showErrorDialog(
+    message: string,
+    options?: {showSignInButton?: boolean; htmlContent?: string}
+  ) {
     this.reporting.reportErrorDialog(message);
     this.errorDialog.text = message;
+    this.errorDialog.htmlContent = options?.htmlContent;
     this.errorDialog.showSignInButton = !!options && !!options.showSignInButton;
     if (this.errorModal.hasAttribute('open')) {
       this.errorModal.close();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index 4f3b0fb..bfb6ac5 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -7,9 +7,9 @@
 import '../../../test/common-test-setup';
 import './gr-error-manager';
 import {
+  __testOnly_ErrorType,
   constructServerErrorMsg,
   GrErrorManager,
-  __testOnly_ErrorType,
 } from './gr-error-manager';
 import {
   stubReporting,
@@ -23,7 +23,7 @@
 } from '../../../test/test-data-generators';
 import {AccountId} from '../../../types/common';
 import {waitUntil} from '../../../test/test-utils';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import {testResolver} from '../../../test/common-test-setup';
 import {authServiceToken} from '../../../services/gr-auth/gr-auth';
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
index 6fc8c2d..92d3d7f 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
@@ -3,7 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 
 declare global {
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts
index 31256f8..900c104 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts
@@ -3,7 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import './gr-key-binding-display';
 import {GrKeyBindingDisplay} from './gr-key-binding-display';
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
index a359072..43fbdbb 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -7,11 +7,11 @@
 import '../gr-key-binding-display/gr-key-binding-display';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {
-  ShortcutSection,
   SectionView,
+  ShortcutSection,
   shortcutsServiceToken,
   ShortcutViewListener,
 } from '../../../services/shortcuts/shortcuts-service';
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
index 5208359..e2c2388 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
@@ -10,7 +10,7 @@
   SectionView,
   ShortcutSection,
 } from '../../../services/shortcuts/shortcuts-service';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {waitEventLoop} from '../../../test/test-utils';
 
 const x = ['x'];
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index d6fe4a3..5c9145a 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -5,7 +5,6 @@
  */
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../shared/gr-dropdown/gr-dropdown';
-import '../../shared/gr-icon/gr-icon';
 import '../gr-account-dropdown/gr-account-dropdown';
 import '../gr-smart-search/gr-smart-search';
 import {getBaseUrl, getDocUrl} from '../../../utils/url-util';
@@ -21,15 +20,22 @@
 import {AuthType} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
-import {fire} from '../../../utils/event-util';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
 import {userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {subscribe} from '../../lit/subscription-controller';
 import {ifDefined} from 'lit/directives/if-defined.js';
+import '@material/web/divider/divider';
+import '@material/web/list/list';
+import '@material/web/list/list-item';
+import '@material/web/labs/item/item';
+import '@material/web/icon/icon';
+import '@material/web/iconbutton/icon-button';
+import {when} from 'lit/directives/when.js';
+import {isElementTarget} from '../../../utils/dom-util';
 
 type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
 
@@ -87,7 +93,10 @@
 ];
 
 // visible for testing
-export function getDocLinks(docBaseUrl: string, docLinks: MainHeaderLink[]) {
+export function getDocLinks(
+  docBaseUrl: string,
+  docLinks: MainHeaderLink[]
+): MainHeaderLink[] {
   if (!docBaseUrl) return [];
   return docLinks.map(link => {
     return {
@@ -112,9 +121,6 @@
   interface HTMLElementTagNameMap {
     'gr-main-header': GrMainHeader;
   }
-  interface HTMLElementEventMap {
-    'mobile-search': CustomEvent<{}>;
-  }
 }
 
 @customElement('gr-main-header')
@@ -129,9 +135,6 @@
 
   @state() loginText = '';
 
-  @property({type: Boolean})
-  mobileSearchHidden = false;
-
   // private but used in test
   @state() account?: AccountDetailInfo;
 
@@ -155,13 +158,12 @@
 
   @state() hamburgerClose? = false;
 
+  @state() mobileSearch? = false;
+
   @query('.nav-sidebar') navSidebar?: HTMLDivElement;
 
   @query('.modelBackground') modelBackground?: HTMLDivElement;
 
-  @query('.has-collapsible.active')
-  hasCollapsibleActive?: HTMLLIElement;
-
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly getPluginLoader = resolve(this, pluginLoaderToken);
@@ -205,9 +207,27 @@
 
   override connectedCallback() {
     super.connectedCallback();
+    document.addEventListener('click', this.handleBodyClick);
     this.loadAccount();
   }
 
+  override disconnectedCallback() {
+    document.removeEventListener('click', this.handleBodyClick);
+    super.disconnectedCallback();
+  }
+
+  private readonly handleBodyClick = (e: Event) => {
+    const eventPath = e.composedPath();
+    if (!eventPath) return;
+
+    const headerEl = eventPath.find(
+      el => isElementTarget(el) && el.tagName.toUpperCase() === 'GR-MAIN-HEADER'
+    ) as HTMLAnchorElement | undefined;
+    if (headerEl) return;
+
+    this.mobileSearch = false;
+  };
+
   static override get styles() {
     return [
       sharedStyles,
@@ -218,7 +238,6 @@
         .hideOnDesktop {
           display: none;
         }
-
         nav.hideOnMobile {
           align-items: center;
           display: flex;
@@ -233,25 +252,38 @@
           padding: 0;
           position: relative;
         }
-
         .mobileTitle {
           display: none;
         }
-
         .bigTitle {
           color: var(--header-text-color);
           font-size: var(--header-title-font-size);
           line-height: calc(var(--header-title-font-size) * 1.2);
           text-decoration: none;
+          display: block;
+          flex: 1 1 auto;
+          flex-grow: 0;
+          justify-content: center;
+          overflow: hidden;
+          max-width: 30%;
+          text-decoration: none;
         }
         .bigTitle:hover {
           text-decoration: underline;
         }
-        .titleText,
-        .mobileTitleText {
-          /* Vertical alignment of icons and text with just block/inline display is too troublesome. */
+        .mobileTitleWrapper {
+          position: absolute;
+          left: 100px;
+          right: 100px;
+          overflow: hidden;
+          display: flex;
+          justify-content: center;
+        }
+        .titleText {
           display: flex;
           align-items: center;
+          min-width: 0;
+          overflow: hidden;
         }
         .titleText::before {
           --icon-width: var(--header-icon-width, var(--header-icon-size, 0));
@@ -261,34 +293,7 @@
           background-repeat: no-repeat;
           content: '';
           /* Any direct child of a flex element implicitly has 'display: block', but let's make that explicit here. */
-          display: block;
-          width: var(--icon-width);
-          height: var(--icon-height);
-          /* If size or height are set, then use 'spacing-m', 0px otherwise. */
-          margin-right: clamp(0px, var(--icon-height), var(--spacing-m));
-        }
-        .titleText::after {
-          /* The height will be determined by the line-height of the .bigTitle element. */
-          content: var(--header-title-content);
-          white-space: nowrap;
-        }
-
-        .mobileTitleText::before {
-          --icon-width: var(
-            --header-icon-width,
-            var(--header-mobile-icon-size, var(--header-icon-size, 0))
-          );
-          --icon-height: var(
-            --header-icon-height,
-            var(--header-mobile-icon-size, var(--header-icon-size, 0))
-          );
-          background-image: var(--header-mobile-icon, var(--header-icon));
-          background-size: var(--mobile-icon-width, var(--icon-width))
-            var(--mobile-icon-height, var(--icon-height));
-          background-repeat: no-repeat;
-          content: '';
-          /* Any direct child of a flex element implicitly has 'display: block', but let's make that explicit here. */
-          display: block;
+          display: inline-block;
           width: var(--mobile-icon-width, var(--icon-width));
           height: var(--mobile-icon-height, var(--icon-height));
           /* If size or height are set, then use 'spacing-m', 0px otherwise. */
@@ -297,17 +302,15 @@
             var(--mobile-icon-height, var(--icon-height)),
             var(--spacing-m)
           );
+          flex-shrink: 0;
         }
-        .mobileTitleText::after {
+        .titleText::after {
           /* The height will be determined by the line-height of the .bigTitle element. */
-          content: var(
-            --header-mobile-title-content,
-            var(--header-title-content)
-          );
+          content: var(--header-title-content);
           white-space: nowrap;
+          overflow: hidden;
           text-overflow: ellipsis;
           flex: 1;
-          overflow: hidden;
           min-width: 0;
         }
 
@@ -329,19 +332,15 @@
         .mobileRightItems {
           align-items: center;
           justify-content: flex-end;
-
-          display: inline-block;
+          display: inline-flex;
           vertical-align: middle;
-          cursor: pointer;
           position: relative;
           top: 0px;
           right: 0px;
           margin-right: 0;
           margin-left: auto;
           min-height: 50px;
-          padding-top: 12px;
         }
-
         .rightItems gr-endpoint-decorator:not(:empty),
         .mobileRightItems gr-endpoint-decorator:not(:empty) {
           margin-left: var(--spacing-s);
@@ -351,6 +350,8 @@
           margin: 0 var(--spacing-m);
           max-width: 500px;
           min-width: 150px;
+          height: 36px;
+          --gr-autocomplete-height: 36px;
         }
         gr-dropdown,
         .browse {
@@ -359,12 +360,6 @@
         gr-dropdown {
           --gr-dropdown-item-color: var(--primary-text-color);
         }
-        .settingsButton {
-          margin-left: var(--spacing-m);
-        }
-        .feedbackButton {
-          margin-left: var(--spacing-s);
-        }
         .browse {
           color: var(--header-text-color);
           /* Same as gr-button */
@@ -372,8 +367,8 @@
           text-decoration: none;
         }
         .invisible,
-        .settingsButton,
-        gr-account-dropdown {
+        gr-account-dropdown,
+        .settingsButton {
           display: none;
         }
         :host([loading]) .accountContainer,
@@ -382,7 +377,9 @@
         :host([loggedIn]) .moreMenu {
           display: none;
         }
-        :host([loggedIn]) .settingsButton,
+        :host([loggedIn]) .settingsButton {
+          display: flex;
+        }
         :host([loggedIn]) gr-account-dropdown {
           display: inline;
         }
@@ -394,7 +391,6 @@
           align-items: center;
           display: flex;
           margin: 0 calc(0 - var(--spacing-m)) 0 var(--spacing-m);
-          overflow: hidden;
           text-overflow: ellipsis;
           white-space: nowrap;
         }
@@ -410,14 +406,13 @@
           box-shadow: var(--elevation-level-2);
         }
         /*
-           * We are not using :host to do this, because :host has a lowest css priority
-           * compared to others. This means that using :host to do this would break styles.
-           */
+          * We are not using :host to do this, because :host has a lowest css priority
+          * compared to others. This means that using :host to do this would break styles.
+          */
         .linksTitle,
         .bigTitle,
         .loginButton,
         .registerButton,
-        gr-icon,
         gr-dropdown,
         gr-account-dropdown {
           --gr-button-text-color: var(--header-text-color);
@@ -427,25 +422,56 @@
           --gr-button-text-color: var(--primary-text-color);
           color: var(--primary-text-color);
         }
-        #mobileSearch {
-          display: none;
+        md-icon-button {
+          --md-sys-color-on-surface-variant: var(--header-text-color);
+        }
+        md-icon[filled] {
+          font-variation-settings: 'FILL' 1;
+        }
+        nav.hideOnMobile,
+        nav .nav-header {
+          border-bottom: var(--header-border-bottom);
+          border-image: var(--header-border-image);
+          box-shadow: var(--header-box-shadow);
+          padding: var(--header-padding);
+          min-height: 48px;
         }
         @media screen and (max-width: 50em) {
           .bigTitle {
             font-family: var(--header-font-family);
-            font-size: var(--font-size-h3);
+            font-size: 20px;
             font-weight: var(--font-weight-h3);
             line-height: var(--line-height-h3);
+            min-width: 0;
+            max-width: unset;
           }
-          gr-smart-search,
+          .titleText::before {
+            --icon-width: var(
+              --header-icon-width,
+              var(--header-mobile-icon-size, var(--header-icon-size, 0))
+            );
+            --icon-height: var(
+              --header-icon-height,
+              var(--header-mobile-icon-size, var(--header-icon-size, 0))
+            );
+            background-image: var(--header-mobile-icon, var(--header-icon));
+            background-size: var(--mobile-icon-width, var(--icon-width))
+              var(--mobile-icon-height, var(--icon-height));
+            width: var(--mobile-icon-width, var(--icon-width));
+            height: var(--mobile-icon-height, var(--icon-height));
+          }
+          .titleText::after {
+            /* The height will be determined by the line-height of the .bigTitle element. */
+            content: var(
+              --header-mobile-title-content,
+              var(--header-title-content)
+            );
+          }
           .browse,
           .rightItems .hideOnMobile,
           .links > li.hideOnMobile {
             display: none;
           }
-          #mobileSearch {
-            display: inline-flex;
-          }
           .accountContainer {
             margin-left: var(--spacing-m) !important;
           }
@@ -457,40 +483,25 @@
             width: 200px;
             height: 100%;
             display: block;
-            position: fixed;
+            position: absolute;
             left: -200px;
-            top: 0px;
             transition: left 0.25s ease;
-            margin: 0;
-            border: 0;
             overflow-y: auto;
             overflow-x: hidden;
-            height: 100%;
-            margin-bottom: 15px 0;
             box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
-            border-radius: 3px;
             z-index: 2;
+            padding-bottom: 56px;
           }
           .nav-sidebar.visible {
             left: 0px;
             transition: left 0.25s ease;
-            width: 80%;
-            z-index: 200;
-          }
-          .mobileTitle {
-            position: relative;
-            display: block;
-            top: 10px;
-            font-size: 20px;
-            left: 100px;
-            right: 100px;
-            text-align: center;
-            text-overflow: ellipsis;
-            overflow: hidden;
             width: 50%;
+            z-index: 200;
+            box-shadow: var(--header-box-shadow);
           }
           .nav-header {
             display: flex;
+            align-items: center;
           }
           .hamburger {
             display: inline-block;
@@ -504,32 +515,16 @@
             padding: 12px;
             z-index: 200;
           }
-          .nav-sidebar ul {
-            list-style-type: none;
-            margin: 0;
-            padding: 0;
-            display: block;
-            padding-top: 50px;
-          }
-          .nav-sidebar li {
-            list-style-type: none;
-            margin: 0;
-            padding: 0;
-            display: inline-block;
-            position: relative;
-            font-size: 14;
-            color: var(--primary-text-color);
-            display: block;
-          }
-          .cover {
+          .modelBackground {
             background: rgba(0, 0, 0, 0.5);
-            position: fixed;
-            top: 0;
-            bottom: 0;
-            left: 0;
-            right: 0;
+            position: absolute;
+            height: 100%;
             overflow: none;
             z-index: 199;
+            width: 100%;
+          }
+          .transparent {
+            background: rgba(0, 0, 0, 0);
           }
           .hideOnDesktop {
             display: block;
@@ -537,71 +532,53 @@
           nav.hideOnMobile {
             display: none;
           }
-          .nav-sidebar .menu ul {
-            list-style-type: none;
-            margin: 0;
-            padding: 0;
+          md-list {
+            --md-list-container-color: transparent;
             display: block;
-            padding-top: 50px;
+            margin-inline: 12px;
+            min-width: unset;
+            --md-sys-typescale-body-large-size: inherit;
+            --md-sys-color-on-surface: var(--primary-text-color);
           }
-          .nav-sidebar .menu li {
-            list-style-type: none;
-            margin: 0;
-            padding: 0;
-            display: inline-block;
-            position: relative;
-            font-size: 14;
-            color: var(--primary-text-color);
+          md-list-item {
+            font-size: 16px;
+            --md-list-item-label-text-font: inherit;
+          }
+          md-list a md-list-item::part(focus-ring) {
+            --md-focus-ring-shape: 28px;
+          }
+          md-list a md-list-item {
+            margin-block: 12px;
             display: block;
+            --md-focus-ring-shape: 28px;
+            border-radius: 28px;
           }
-          .nav-sidebar .menu li a {
-            padding: 15px 20px;
-            font-size: 14;
-            outline: 0;
-            display: block;
-            color: var(--primary-text-color);
-            font-weight: 600;
+          md-list md-item [slot='headline'] {
+            /* shadow root slot has overflow:hidden, it's cutting some text off */
+            padding-block: 2px;
           }
-          .nav-sidebar .menu li.active ul.dropdown {
-            display: block;
+          md-list md-item:first-of-type {
+            padding-block: 0;
           }
-          .nav-sidebar .menu li ul.dropdown {
-            position: absolute;
-            display: none;
-            width: 100%;
-            box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
-            padding-top: 0;
-            position: relative;
+          md-list md-item {
+            font-size: 24px;
+            padding-block-end: 0;
           }
-          .nav-sidebar .menu li ul.dropdown li {
-            display: block;
-            list-style-type: none;
+          md-list md-item + a md-list-item {
+            margin-block-start: 0;
           }
-          .nav-sidebar .menu li ul.dropdown li a {
-            padding: 15px 20px;
-            font-size: 15px;
-            display: block;
-            font-weight: 400;
-            border-bottom: none;
-            padding: 10px 10px 10px 30px;
-          }
-          .nav-sidebar .menu li ul.dropdown li:last-child a {
-            border-bottom: none;
-          }
-          .nav-sidebar .menu a {
+          .itemAction:link,
+          .itemAction:visited {
             text-decoration: none;
           }
-          .nav-sidebar .menu li.active:first-child a {
-            border-radius: 3px 0 0 3px;
-            border-radius: 0;
-          }
-          .nav-sidebar .menu li ul.dropdown li.active:first-child a {
-            border-radius: 0;
-          }
-          .arrow-down {
-            position: absolute;
-            top: 10px;
-            right: 10px;
+          @media (forced-colors: active) {
+            md-list a md-list-item[selected] {
+              border: 4px double CanvasText;
+            }
+            md-list a md-list-item {
+              border-radius: 28px;
+              border: 1px solid CanvasText;
+            }
           }
         }
       `,
@@ -632,17 +609,43 @@
             class="hideOnMobile"
             name="header-small-banner"
           ></gr-endpoint-decorator>
-          <gr-smart-search id="search"></gr-smart-search>
+          <gr-smart-search id="search" .verticalOffset=${40}></gr-smart-search>
           <gr-endpoint-decorator
             class="hideOnMobile"
             name="header-top-right"
           ></gr-endpoint-decorator>
+          <!--
+          Always render the fallback feedback button, but hide it with CSS if feedbackURL is empty.
+          We do this instead of using Lit's conditional rendering (e.g., ?hidden or if) inside
+          <gr-endpoint-decorator>, because the plugin system may replace or remove this content
+          outside of Lit's control. If Lit tries to update a node that was removed by the plugin
+          system, it will throw an error. By always rendering the node and only hiding it, we
+          avoid this issue and ensure plugin compatibility.
+          -->
           <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
-            ${this.renderFeedback()}
+            <md-icon-button
+              touch-target="none"
+              href=${this.feedbackURL}
+              title="File a bug"
+              aria-label="File a bug"
+              target="_blank"
+              ?hidden=${!this.feedbackURL}
+              @click=${(e: Event) => {
+                const path = e.composedPath();
+                const iconButton = path.find(
+                  (el): el is HTMLElement =>
+                    el instanceof HTMLElement &&
+                    el.tagName.toLowerCase() === 'md-icon-button'
+                );
+                const anchor = iconButton?.shadowRoot?.querySelector('a');
+                anchor?.setAttribute('rel', 'noopener noreferrer');
+              }}
+            >
+              <md-icon filled>bug_report</md-icon>
+            </md-icon-button>
           </gr-endpoint-decorator>
-          </div>
-          ${this.renderAccount()}
         </div>
+        ${this.renderAccount()}
       </nav>
     `;
   }
@@ -670,73 +673,120 @@
       });
     }
 
+    const linkGroups = this.computeLinks(
+      this.userLinks,
+      this.adminLinks,
+      this.topMenus
+    );
+
     return html`
       <nav class="hideOnDesktop">
-        <div class="nav-sidebar">
-          <ul class="menu">
-            ${this.computeLinks(
-              this.userLinks,
-              this.adminLinks,
-              this.topMenus
-            ).map(linkGroup => this.renderLinkGroupMobile(linkGroup))}
-          </ul>
-        </div>
-        <div class="nav-header">
-          <a
-            class="hamburger"
-            href=""
-            title="Hamburger"
-            aria-label="${!this.hamburgerClose ? 'Open' : 'Close'} hamburger"
-            role="button"
-            @click=${() => {
-              this.handleSidebar();
-            }}
-          >
-            ${!this.hamburgerClose
-              ? html`<gr-icon icon="menu" filled></gr-icon>`
-              : html`<gr-icon
-                  class="hamburger-open"
-                  icon="menu_open"
-                  filled
-                ></gr-icon>`}
-          </a>
-          <a
-            href=${`//${window.location.host}${getBaseUrl()}/`}
-            class="mobileTitle bigTitle"
-          >
-            <gr-endpoint-decorator name="header-mobile-title">
-              <div class="mobileTitleText"></div>
-            </gr-endpoint-decorator>
-          </a>
-          <div class="mobileRightItems">
-            <a
-              class="searchButton"
-              title="Search"
-              @click=${(e: Event) => {
-                this.onMobileSearchTap(e);
+        ${when(
+          this.mobileSearch,
+          () => html` <div class="nav-header">
+            <md-icon-button
+              touch-target="none"
+              aria-label="Back"
+              @click=${() => {
+                this.mobileSearch = false;
               }}
-              role="button"
-              aria-label=${this.mobileSearchHidden
-                ? 'Show Searchbar'
-                : 'Hide Searchbar'}
             >
-              <gr-icon icon="search" filled></gr-icon>
-            </a>
-            <gr-dropdown
-              class="moreMenu"
-              link=""
-              .items=${moreMenu}
-              horizontal-align="center"
-            >
-              <span class="linksTitle">
-                <gr-icon icon="more_horiz" filled></gr-icon>
-              </span>
-            </gr-dropdown>
-            ${this.renderAccountDropdown(true)}
-          </div>
-        </div>
+              <md-icon filled>arrow_back</md-icon>
+            </md-icon-button>
+            <gr-smart-search
+              id="search"
+              .verticalOffset=${40}
+            ></gr-smart-search>
+          </div>`
+        )}
+        ${when(
+          !this.mobileSearch,
+          () => html`
+            <div class="nav-header">
+              <md-icon-button
+                touch-target="none"
+                aria-label="${!this.hamburgerClose
+                  ? 'Open'
+                  : 'Close'} hamburger"
+                @click=${() => {
+                  this.handleSidebar();
+                }}
+              >
+                <md-icon filled
+                  >${!this.hamburgerClose ? 'menu' : 'menu_open'}</md-icon
+                >
+              </md-icon-button>
+              <div class="mobileTitleWrapper">
+                <a
+                  href=${`//${window.location.host}${getBaseUrl()}/`}
+                  class="bigTitle"
+                  @click=${() => {
+                    if (this.hamburgerClose) {
+                      this.handleSidebar();
+                    }
+                  }}
+                >
+                  <gr-endpoint-decorator name="header-mobile-title">
+                    <div class="titleText"></div>
+                  </gr-endpoint-decorator>
+                </a>
+              </div>
+              <div class="mobileRightItems">
+                <md-icon-button
+                  touch-target="none"
+                  title="Search"
+                  aria-label=${this.mobileSearch
+                    ? 'Hide Search Bar'
+                    : 'Show Search Bar'}
+                  @click=${() => {
+                    this.mobileSearch = !this.mobileSearch;
+
+                    if (this.hamburgerClose) {
+                      this.handleSidebar();
+                    }
+                  }}
+                >
+                  <md-icon filled>search</md-icon>
+                </md-icon-button>
+                <gr-dropdown
+                  class="moreMenu"
+                  link=""
+                  .items=${moreMenu}
+                  .horizontalAlign=${'right'}
+                  .verticalOffset=${40}
+                  @click=${() => {
+                    if (this.hamburgerClose) {
+                      this.handleSidebar();
+                    }
+                  }}
+                >
+                  <span class="linksTitle">
+                    <md-icon filled>more_horiz</md-icon>
+                  </span>
+                </gr-dropdown>
+                ${this.renderAccountDropdown(true)}
+              </div>
+            </div>
+            <div class="nav-sidebar">
+              <md-list aria-label="menu links">
+                ${linkGroups.map((linkGroup, index) =>
+                  this.renderLinkGroupMobile(
+                    linkGroup,
+                    index,
+                    linkGroups.length
+                  )
+                )}
+              </md-list>
+            </div>
+            ${this.hamburgerClose
+              ? html`<div
+                  class="modelBackground"
+                  @click=${() => this.handleSidebar()}
+                ></div>`
+              : nothing}
+          `
+        )}
       </nav>
-      <div class="modelBackground" @click=${() => this.handleSidebar()}></div>
     `;
   }
 
@@ -757,87 +807,53 @@
     `;
   }
 
-  private renderLinkGroupMobile(linkGroup: MainHeaderLinkGroup) {
+  private renderLinkGroupMobile(
+    linkGroup: MainHeaderLinkGroup,
+    groupsIndex: number,
+    totalGroups: number
+  ) {
     return html`
-      <li class="has-collapsible" @click=${this.handleCollapsible}>
-        <a class="main" href="" data-title=${linkGroup.title}
-          >${linkGroup.title}<gr-icon
-            icon="arrow_drop_down"
-            class="arrow-down"
-          ></gr-icon
-        ></a>
-        <ul class="dropdown">
-          ${linkGroup.links.map(link => this.renderLinkMobile(link))}
-        </ul>
-      </li>
+      <md-item><div slot="headline">${linkGroup.title}</div></md-item>
+      ${linkGroup.links.map(link => this.renderLinkMobile(link))}
+      ${groupsIndex < totalGroups - 1
+        ? html`<md-divider role="separator" tabindex="-1"></md-divider>`
+        : nothing}
     `;
   }
 
   private renderLinkMobile(link: DropdownLink) {
     return html`
-      <li tabindex="-1">
-        <span ?hidden=${!!link.url} tabindex="-1">${link.name}</span>
-        <a
-          class="itemAction"
-          href=${this.computeLinkURL(link)}
-          ?download=${!!link.download}
-          rel=${ifDefined(this.computeLinkRel(link) ?? undefined)}
-          target=${ifDefined(link.target ?? undefined)}
-          ?hidden=${!link.url}
-          tabindex="-1"
-          @click=${() => this.handleSidebar()}
-          >${link.name}</a
-        >
-      </li>
-    `;
-  }
-
-  private renderFeedback() {
-    if (!this.feedbackURL) return;
-
-    return html`
       <a
-        href=${this.feedbackURL}
-        title="File a bug"
-        aria-label="File a bug"
-        target="_blank"
-        rel="noopener noreferrer"
-        role="button"
+        class="itemAction"
+        href=${ifDefined(this.computeLinkURL(link))}
+        ?download=${!!link.download}
+        rel=${ifDefined(this.computeLinkRel(link) ?? undefined)}
+        target=${ifDefined(link.target ?? undefined)}
+        tabindex="-1"
       >
-        <gr-icon icon="bug_report" filled></gr-icon>
-      </a>
+        <md-list-item type="button" @click=${() => this.handleSidebar()}>
+          ${link.name}
+        </md-list-item></a
+      >
     `;
   }
 
   private renderAccount() {
     return html`
       <div class="accountContainer" id="accountContainer">
-        <div>
-          <gr-icon
-            id="mobileSearch"
-            icon="search"
-            @click=${(e: Event) => {
-              this.onMobileSearchTap(e);
-            }}
-            role="button"
-            aria-label=${this.mobileSearchHidden
-              ? 'Show Searchbar'
-              : 'Hide Searchbar'}
-          ></gr-icon>
-        </div>
         ${this.renderRegister()}
         <gr-endpoint-decorator name="auth-link">
           <a class="loginButton" href=${this.loginUrl}>${this.loginText}</a>
         </gr-endpoint-decorator>
-        <a
+        <md-icon-button
           class="settingsButton"
+          touch-target="none"
           href="${getBaseUrl()}/settings/"
           title="Settings"
           aria-label="Settings"
-          role="button"
         >
-          <gr-icon icon="settings" filled></gr-icon>
-        </a>
+          <md-icon filled>settings</md-icon>
+        </md-icon-button>
         ${this.renderAccountDropdown()}
       </div>
     `;
@@ -861,7 +877,12 @@
     return html`
       <gr-account-dropdown
         .account=${this.account}
-        ?showMobile=${showOnMobile}
+        ?showMobile=${!!showOnMobile}
+        @click=${() => {
+          if (this.hamburgerClose) {
+            this.handleSidebar();
+          }
+        }}
       ></gr-account-dropdown>
     `;
   }
@@ -988,17 +1009,9 @@
 
   // private but used in test
   createHeaderLink(linkObj: TopMenuItemInfo): MainHeaderLink {
-    // Delete target property due to complications of
-    // https://issues.gerritcodereview.com/issues/40006107
-    //
-    // The server tries to guess whether URL is a view within the UI.
-    // If not, it sets target='_blank' on the menu item. The server
-    // makes assumptions that work for the GWT UI, but not PolyGerrit,
-    // so we'll just disable it altogether for now.
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    const {target, ...headerLink} = {...linkObj};
+    const headerLink = {...linkObj};
 
-    // Normalize all urls to PolyGerrit style.
+    // Normalize all GWT style URLs to PolyGerrit style.
     if (headerLink.url.startsWith('#')) {
       headerLink.url = linkObj.url.slice(1);
     }
@@ -1006,12 +1019,6 @@
     return headerLink;
   }
 
-  private onMobileSearchTap(e: Event) {
-    e.preventDefault();
-    e.stopPropagation();
-    fire(this, 'mobile-search', {});
-  }
-
   /**
    * Build a URL for the given host and path. The base URL will be only added,
    * if it is not already included in the path.
@@ -1047,9 +1054,9 @@
    */
   private computeLinkURL(link: DropdownLink) {
     if (typeof link.url === 'undefined') {
-      return '';
+      return undefined;
     }
-    if (link.target || !link.url.startsWith('/')) {
+    if (!link.url.startsWith('/')) {
       return link.url;
     }
     return this.computeRelativeURL(link.url);
@@ -1074,23 +1081,9 @@
     return null;
   }
 
-  private handleCollapsible(e: MouseEvent) {
-    const target = e.target as HTMLSpanElement;
-    if (target.hasAttribute('data-title')) {
-      if (target.parentElement?.classList.contains('active')) {
-        target.parentElement.classList.remove('active');
-      } else {
-        if (this.hasCollapsibleActive) {
-          this.hasCollapsibleActive.classList.remove('active');
-        }
-        target.parentElement?.classList.toggle('active');
-      }
-    }
-  }
-
   private handleSidebar() {
     this.navSidebar?.classList.toggle('visible');
-    if (!this.modelBackground?.classList.contains('cover')) {
+    if (!this.modelBackground) {
       if (document.getElementsByTagName('html')) {
         document.getElementsByTagName('html')[0].style.overflow = 'hidden';
       }
@@ -1099,8 +1092,6 @@
         document.getElementsByTagName('html')[0].style.overflow = '';
       }
     }
-    this.modelBackground?.classList.toggle('cover');
-    this.hasCollapsibleActive?.classList.remove('active');
     this.hamburgerClose = !this.hamburgerClose;
   }
 }
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index b4a0600..04d4e84 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -11,16 +11,20 @@
   stubRestApi,
 } from '../../../test/test-utils';
 import './gr-main-header';
-import {GrMainHeader, getDocLinks} from './gr-main-header';
+import {getDocLinks, GrMainHeader} from './gr-main-header';
 import {
   createAccountDetailWithId,
   createGerritInfo,
   createServerInfo,
 } from '../../../test/test-data-generators';
 import {NavLink} from '../../../models/views/admin';
-import {ServerInfo, TopMenuItemInfo} from '../../../types/common';
+import {
+  ServerInfo,
+  TopMenuEntryInfo,
+  TopMenuItemInfo,
+} from '../../../types/common';
 import {AuthType} from '../../../constants/constants';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-main-header tests', () => {
   let element: GrMainHeader;
@@ -53,7 +57,9 @@
             </li>
             <li>
               <gr-dropdown down-arrow="" horizontal-align="left" link="">
-                <span class="linksTitle" id="Documentation">Documentation</span>
+                <span class="linksTitle" id="Documentation">
+                  Documentation
+                </span>
               </gr-dropdown>
             </li>
             <li>
@@ -75,195 +81,176 @@
               class="feedbackButton"
               name="header-feedback"
             >
+              <md-icon-button
+                data-aria-label="File a bug"
+                hidden=""
+                href=""
+                target="_blank"
+                title="File a bug"
+                touch-target="none"
+                value=""
+              >
+                <md-icon aria-hidden="true" filled=""> bug_report </md-icon>
+              </md-icon-button>
             </gr-endpoint-decorator>
           </div>
           <div class="accountContainer" id="accountContainer">
-            <div>
-              <gr-icon
-                aria-label="Hide Searchbar"
-                icon="search"
-                id="mobileSearch"
-                role="button"
-              >
-              </gr-icon>
-            </div>
             <gr-endpoint-decorator name="auth-link">
               <a class="loginButton" href="/login"> Sign in </a>
             </gr-endpoint-decorator>
-            <a
-              aria-label="Settings"
+            <md-icon-button
               class="settingsButton"
+              data-aria-label="Settings"
               href="/settings/"
-              role="button"
               title="Settings"
+              touch-target="none"
+              value=""
             >
-              <gr-icon icon="settings" filled></gr-icon>
-            </a>
+              <md-icon aria-hidden="true" filled=""> settings </md-icon>
+            </md-icon-button>
           </div>
         </nav>
         <nav class="hideOnDesktop">
-          <div class="nav-sidebar">
-            <ul class="menu">
-              <li class="has-collapsible">
-                <a class="main" data-title="Changes" href="">
-                  Changes
-                  <gr-icon class="arrow-down" icon="arrow_drop_down"> </gr-icon>
-                </a>
-                <ul class="dropdown">
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Open </span>
-                    <a
-                      class="itemAction"
-                      href="//localhost:9876/q/status:open+-is:wip"
-                      tabindex="-1"
-                    >
-                      Open
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Merged </span>
-                    <a
-                      class="itemAction"
-                      href="//localhost:9876/q/status:merged"
-                      tabindex="-1"
-                    >
-                      Merged
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Abandoned </span>
-                    <a
-                      class="itemAction"
-                      href="//localhost:9876/q/status:abandoned"
-                      tabindex="-1"
-                    >
-                      Abandoned
-                    </a>
-                  </li>
-                </ul>
-              </li>
-              <li class="has-collapsible">
-                <a class="main" data-title="Documentation" href="">
-                  Documentation
-                  <gr-icon class="arrow-down" icon="arrow_drop_down"> </gr-icon>
-                </a>
-                <ul class="dropdown">
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Table of Contents </span>
-                    <a
-                      class="itemAction"
-                      href="https://gerrit-review.googlesource.com/Documentation/index.html"
-                      rel="noopener"
-                      tabindex="-1"
-                      target="_blank"
-                    >
-                      Table of Contents
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Searching </span>
-                    <a
-                      class="itemAction"
-                      href="https://gerrit-review.googlesource.com/Documentation/user-search.html"
-                      rel="noopener"
-                      tabindex="-1"
-                      target="_blank"
-                    >
-                      Searching
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Uploading </span>
-                    <a
-                      class="itemAction"
-                      href="https://gerrit-review.googlesource.com/Documentation/user-upload.html"
-                      rel="noopener"
-                      tabindex="-1"
-                      target="_blank"
-                    >
-                      Uploading
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Access Control </span>
-                    <a
-                      class="itemAction"
-                      href="https://gerrit-review.googlesource.com/Documentation/access-control.html"
-                      rel="noopener"
-                      tabindex="-1"
-                      target="_blank"
-                    >
-                      Access Control
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> REST API </span>
-                    <a
-                      class="itemAction"
-                      href="https://gerrit-review.googlesource.com/Documentation/rest-api.html"
-                      rel="noopener"
-                      tabindex="-1"
-                      target="_blank"
-                    >
-                      REST API
-                    </a>
-                  </li>
-                  <li tabindex="-1">
-                    <span hidden="" tabindex="-1"> Project Owner Guide </span>
-                    <a
-                      class="itemAction"
-                      href="https://gerrit-review.googlesource.com/Documentation/intro-project-owner.html"
-                      rel="noopener"
-                      tabindex="-1"
-                      target="_blank"
-                    >
-                      Project Owner Guide
-                    </a>
-                  </li>
-                </ul>
-              </li>
-              <li class="has-collapsible">
-                <a class="main" data-title="Browse" href="">
-                  Browse
-                  <gr-icon class="arrow-down" icon="arrow_drop_down"> </gr-icon>
-                </a>
-                <ul class="dropdown"></ul>
-              </li>
-            </ul>
-          </div>
           <div class="nav-header">
-            <a
-              aria-label="Open hamburger"
-              class="hamburger"
-              href=""
-              role="button"
-              title="Hamburger"
+            <md-icon-button
+              data-aria-label="Open hamburger"
+              touch-target="none"
+              value=""
             >
-              <gr-icon filled="" icon="menu"> </gr-icon>
-            </a>
-            <a class="bigTitle mobileTitle" href="//localhost:9876/">
-              <gr-endpoint-decorator name="header-mobile-title">
-                <div class="mobileTitleText"></div>
-              </gr-endpoint-decorator>
-            </a>
-            <div class="mobileRightItems">
-              <a
-                aria-label="Hide Searchbar"
-                class="searchButton"
-                role="button"
-                title="Search"
-              >
-                <gr-icon filled="" icon="search"> </gr-icon>
+              <md-icon aria-hidden="true" filled=""> menu </md-icon>
+            </md-icon-button>
+            <div class="mobileTitleWrapper">
+              <a class="bigTitle" href="//localhost:9876/">
+                <gr-endpoint-decorator name="header-mobile-title">
+                  <div class="titleText"></div>
+                </gr-endpoint-decorator>
               </a>
-              <gr-dropdown class="moreMenu" horizontal-align="center" link="">
+            </div>
+            <div class="mobileRightItems">
+              <md-icon-button
+                data-aria-label="Show Search Bar"
+                title="Search"
+                touch-target="none"
+                value=""
+              >
+                <md-icon aria-hidden="true" filled=""> search </md-icon>
+              </md-icon-button>
+              <gr-dropdown class="moreMenu" link="">
                 <span class="linksTitle">
-                  <gr-icon filled="" icon="more_horiz"> </gr-icon>
+                  <md-icon aria-hidden="true" filled=""> more_horiz </md-icon>
                 </span>
               </gr-dropdown>
             </div>
           </div>
+          <div class="nav-sidebar">
+            <md-list aria-label="menu links">
+              <md-item>
+                <div slot="headline">Changes</div>
+              </md-item>
+              <a
+                class="itemAction"
+                href="//localhost:9876/q/status:open+-is:wip"
+                tabindex="-1"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Open
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="//localhost:9876/q/status:merged"
+                tabindex="-1"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Merged
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="//localhost:9876/q/status:abandoned"
+                tabindex="-1"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Abandoned
+                </md-list-item>
+              </a>
+              <md-divider role="separator" tabindex="-1"> </md-divider>
+              <md-item>
+                <div slot="headline">Documentation</div>
+              </md-item>
+              <a
+                class="itemAction"
+                href="https://gerrit-review.googlesource.com/Documentation/index.html"
+                rel="noopener"
+                tabindex="-1"
+                target="_blank"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Table of Contents
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="https://gerrit-review.googlesource.com/Documentation/user-search.html"
+                rel="noopener"
+                tabindex="-1"
+                target="_blank"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Searching
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="https://gerrit-review.googlesource.com/Documentation/user-upload.html"
+                rel="noopener"
+                tabindex="-1"
+                target="_blank"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Uploading
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="https://gerrit-review.googlesource.com/Documentation/access-control.html"
+                rel="noopener"
+                tabindex="-1"
+                target="_blank"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Access Control
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="https://gerrit-review.googlesource.com/Documentation/rest-api.html"
+                rel="noopener"
+                tabindex="-1"
+                target="_blank"
+              >
+                <md-list-item md-list-item="" type="button">
+                  REST API
+                </md-list-item>
+              </a>
+              <a
+                class="itemAction"
+                href="https://gerrit-review.googlesource.com/Documentation/intro-project-owner.html"
+                rel="noopener"
+                tabindex="-1"
+                target="_blank"
+              >
+                <md-list-item md-list-item="" type="button">
+                  Project Owner Guide
+                </md-list-item>
+              </a>
+              <md-divider role="separator" tabindex="-1"> </md-divider>
+              <md-item>
+                <div slot="headline">Browse</div>
+              </md-item>
+            </md-list>
+          </div>
         </nav>
-        <div class="modelBackground"></div>
       `
     );
   });
@@ -295,16 +282,14 @@
   });
 
   test('fix my menu item', () => {
-    assert.deepEqual(
-      [
-        {url: 'https://awesometown.com/#hashyhash', name: '', target: ''},
-        {url: 'url', name: '', target: '_blank'},
-      ].map(element.createHeaderLink),
-      [
-        {url: 'https://awesometown.com/#hashyhash', name: ''},
-        {url: 'url', name: ''},
-      ]
-    );
+    const menuLink: TopMenuItemInfo[] = [
+      {url: 'https://awesometown.com/#hashyhash', name: '', target: undefined},
+      {url: '#/q/is:nice', name: '', target: '_blank'},
+    ];
+    assert.deepEqual(menuLink.map(element.createHeaderLink), [
+      {url: 'https://awesometown.com/#hashyhash', name: '', target: undefined},
+      {url: '/q/is:nice', name: '', target: '_blank'},
+    ]);
   });
 
   test('user links', () => {
@@ -323,7 +308,7 @@
       {
         name: 'Facebook',
         url: 'https://facebook.com',
-        target: '',
+        target: undefined,
       },
     ];
     const adminLinks: NavLink[] = [
@@ -393,7 +378,7 @@
         view: undefined,
       },
     ];
-    const topMenus = [
+    const topMenus: TopMenuEntryInfo[] = [
       {
         name: 'Plugins',
         items: [
@@ -417,6 +402,7 @@
         links: [
           {
             name: 'Manage',
+            target: '_blank',
             url: 'https://gerrit/plugins/plugin-manager/static/index.html',
           },
         ],
@@ -438,12 +424,10 @@
         items: [
           {
             name: 'Project Settings',
-            target: '_blank',
             url: '/plugins/myplugin/${projectName}',
           },
           {
             name: 'Project List',
-            target: '_blank',
             url: '/plugins/myplugin/index.html',
           },
         ],
@@ -482,7 +466,6 @@
         items: [
           {
             name: 'Manage',
-            target: '_blank',
             url: 'https://gerrit/plugins/plugin-manager/static/index.html',
           },
         ],
@@ -492,7 +475,6 @@
         items: [
           {
             name: 'Create',
-            target: '_blank',
             url: 'https://gerrit/plugins/plugin-manager/static/create.html',
           },
         ],
@@ -539,7 +521,6 @@
         items: [
           {
             name: 'Manage',
-            target: '_blank',
             url: 'https://gerrit/plugins/plugin-manager/static/index.html',
           },
         ],
@@ -569,7 +550,7 @@
       {
         name: 'Facebook',
         url: 'https://facebook.com',
-        target: '',
+        target: undefined,
       },
     ];
     const topMenus = [
@@ -578,7 +559,6 @@
         items: [
           {
             name: 'Manage',
-            target: '_blank',
             url: 'https://gerrit/plugins/plugin-manager/static/index.html',
           },
         ],
@@ -597,7 +577,7 @@
           {
             name: 'Facebook',
             url: 'https://facebook.com',
-            target: '',
+            target: undefined,
           },
           {
             name: 'Manage',
@@ -616,7 +596,7 @@
         view: undefined,
       },
     ];
-    const topMenus = [
+    const topMenus: TopMenuEntryInfo[] = [
       {
         name: 'Browse',
         items: [
@@ -641,6 +621,7 @@
           adminLinks[0],
           {
             name: 'Manage',
+            target: '_blank',
             url: 'https://gerrit/plugins/plugin-manager/static/index.html',
           },
         ],
@@ -650,7 +631,12 @@
 
   test('shows feedback icon when URL provided', async () => {
     assert.isEmpty(element.feedbackURL);
-    assert.isNotOk(query(element, '.feedbackButton > a'));
+    const feedbackButton = query<HTMLAnchorElement>(
+      element,
+      '.feedbackButton > md-icon-button'
+    );
+    assert.ok(feedbackButton);
+    assert.isTrue(isHidden(feedbackButton));
 
     const url = 'report_bug_url';
     const config: ServerInfo = {
@@ -664,7 +650,12 @@
     await element.updateComplete;
 
     assert.equal(element.feedbackURL, url);
-    assert.ok(query(element, '.feedbackButton > a'));
+    const updatedFeedbackButton = query<HTMLAnchorElement>(
+      element,
+      '.feedbackButton > md-icon-button'
+    );
+    assert.ok(updatedFeedbackButton);
+    assert.equal(updatedFeedbackButton.style.display, '');
   });
 
   test('register URL', async () => {
diff --git a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts
index 6311660..857df48 100644
--- a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts
+++ b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt.ts
@@ -3,7 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, html, css, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {resolve} from '../../../models/dependency';
 import {serviceWorkerInstallerToken} from '../../../services/service-worker-installer';
diff --git a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts
index 8110ffc..1f3c00f 100644
--- a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts
@@ -7,7 +7,7 @@
 import '../../../test/common-test-setup';
 import './gr-notifications-prompt';
 import {GrNotificationsPrompt} from './gr-notifications-prompt';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {getAppContext} from '../../../services/app-context';
 import {testResolver} from '../../../test/common-test-setup';
 import {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
index 4e1f37d..68e8619 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
@@ -5,7 +5,7 @@
  */
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
-import {html, assert, fixture, waitUntil} from '@open-wc/testing';
+import {assert, fixture, html, waitUntil} from '@open-wc/testing';
 import './gr-router';
 import {Page, PageContext} from './gr-page';
 
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 1528374..2987d94 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -3,7 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {Page, PageOptions, PageContext, PageNextCallback} from './gr-page';
+import {Page, PageContext, PageNextCallback, PageOptions} from './gr-page';
 import {NavigationService} from '../gr-navigation/gr-navigation';
 import {getAppContext} from '../../../services/app-context';
 import {
@@ -14,14 +14,15 @@
 import {assert, assertIsDefined} from '../../../utils/common-util';
 import {
   BasePatchSetNum,
+  BranchName,
   GroupId,
   NumericChangeId,
-  RevisionPatchSetNum,
-  RepoName,
-  UrlEncodedCommentId,
   PARENT,
   PatchSetNumber,
-  BranchName,
+  RepoName,
+  RevisionPatchSetNum,
+  UrlEncodedCommentId,
+  UserId,
 } from '../../../types/common';
 import {AppElement, AppElementParams} from '../../gr-app-types';
 import {LocationChangeEventDetail} from '../../../types/events';
@@ -164,8 +165,14 @@
   // Matches /admin/repos/<repos>,access.
   REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
 
-  // Matches /admin/repos/<repos>,access.
-  REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
+  REPO_DASHBOARDS:
+    /^\/admin\/repos\/(.+),dashboards\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+
+  REPO_SUBMIT_REQUIREMENTS:
+    /^\/admin\/repos\/(.+),submit-requirements\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+
+  REPO_LABELS:
+    /^\/admin\/repos\/(.+),labels\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
 
   // Matches /admin/plugins with optional filter and offset.
   PLUGIN_LIST: /^\/admin\/plugins\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
@@ -221,7 +228,7 @@
   // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
   // TODO(kaspern): Migrate completely to project based URLs, with backwards
   // compatibility for change-only.
-  // eslint-disable-next-line max-len
+
   DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
 
   // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
@@ -229,7 +236,7 @@
 
   // Matches diff routes using @\d+ to specify a file name (whether or not
   // the project name is included).
-  // eslint-disable-next-line max-len
+
   DIFF_LEGACY_LINENUM:
     /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
 
@@ -786,6 +793,20 @@
       ctx => this.handleRepoDashboardsRoute(ctx)
     );
 
+    this.mapRoute(
+      RoutePattern.REPO_SUBMIT_REQUIREMENTS,
+      'handleRepoSubmitRequirementsRoute',
+      ctx => this.handleRepoSubmitRequirementsRoute(ctx),
+      true
+    );
+
+    this.mapRoute(
+      RoutePattern.REPO_LABELS,
+      'handleRepoLabelsRoute',
+      ctx => this.handleRepoLabelsRoute(ctx),
+      true
+    );
+
     this.mapRoute(RoutePattern.BRANCH_LIST, 'handleBranchListRoute', ctx =>
       this.handleBranchListRoute(ctx)
     );
@@ -1028,7 +1049,7 @@
         const state: DashboardViewState = {
           view: GerritView.DASHBOARD,
           type: DashboardType.USER,
-          user: ctx.params[0],
+          user: ctx.params[0] as UserId | 'self' | undefined,
         };
         // Note that router model view must be updated before view models.
         this.setState(state);
@@ -1208,6 +1229,38 @@
       view: GerritView.REPO,
       detail: RepoDetailView.DASHBOARDS,
       repo,
+      filter: ctx.params[1] ?? null,
+      offset: ctx.params[2] ?? '0',
+    };
+    // Note that router model view must be updated before view models.
+    this.setState(state);
+    this.repoViewModel.setState(state);
+    this.reporting.setRepoName(repo);
+  }
+
+  handleRepoSubmitRequirementsRoute(ctx: PageContext) {
+    const repo = ctx.params[0] as RepoName;
+    const state: RepoViewState = {
+      view: GerritView.REPO,
+      detail: RepoDetailView.SUBMIT_REQUIREMENTS,
+      repo,
+      filter: ctx.params[1] ?? null,
+      offset: ctx.params[2] ?? '0',
+    };
+    // Note that router model view must be updated before view models.
+    this.setState(state);
+    this.repoViewModel.setState(state);
+    this.reporting.setRepoName(repo);
+  }
+
+  handleRepoLabelsRoute(ctx: PageContext) {
+    const repo = ctx.params[0] as RepoName;
+    const state: RepoViewState = {
+      view: GerritView.REPO,
+      detail: RepoDetailView.LABELS,
+      repo,
+      filter: ctx.params[1] ?? null,
+      offset: ctx.params[2] ?? '0',
     };
     // Note that router model view must be updated before view models.
     this.setState(state);
@@ -1368,16 +1421,14 @@
     const commentId = ctx.params[2] as UrlEncodedCommentId;
 
     this.restApiService.addRepoNameToCache(changeNum, repo);
-    const [comments, robotComments, drafts, change] = await Promise.all([
+    const [comments, drafts, change] = await Promise.all([
       this.restApiService.getDiffComments(changeNum),
-      this.restApiService.getDiffRobotComments(changeNum),
       this.restApiService.getDiffDrafts(changeNum),
       this.restApiService.getChangeDetail(changeNum),
     ]);
 
     const comment =
       findComment(addPath(comments), commentId) ??
-      findComment(addPath(robotComments), commentId) ??
       findComment(addPath(drafts), commentId);
     const path = comment?.path;
     const patchsets = computeAllPatchSets(change);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 6d61f85..3ff3df2 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -8,12 +8,12 @@
 import './gr-router';
 import {Page, PageContext} from './gr-page';
 import {
+  addListenerForTest,
+  MockPromise,
+  mockPromise,
   stubBaseUrl,
   stubRestApi,
-  addListenerForTest,
   waitUntilCalled,
-  mockPromise,
-  MockPromise,
 } from '../../../test/test-utils';
 import {GrRouter, routerToken} from './gr-router';
 import {GerritView} from '../../../services/router/router-model';
@@ -107,18 +107,18 @@
     test('parses correctly', () => {
       let actual = router.parseLineAddress('1234');
       assert.isOk(actual);
-      assert.equal(actual!.lineNum, 1234);
-      assert.isFalse(actual!.leftSide);
+      assert.equal(actual.lineNum, 1234);
+      assert.isFalse(actual.leftSide);
 
       actual = router.parseLineAddress('a4');
       assert.isOk(actual);
-      assert.equal(actual!.lineNum, 4);
-      assert.isTrue(actual!.leftSide);
+      assert.equal(actual.lineNum, 4);
+      assert.isTrue(actual.leftSide);
 
       actual = router.parseLineAddress('b77');
       assert.isOk(actual);
-      assert.equal(actual!.lineNum, 77);
-      assert.isTrue(actual!.leftSide);
+      assert.equal(actual.lineNum, 77);
+      assert.isTrue(actual.leftSide);
     });
   });
 
@@ -162,6 +162,8 @@
       'handlePluginListRoute',
       'handleRepoCommandsRoute',
       'handleRepoEditFileRoute',
+      'handleRepoLabelsRoute',
+      'handleRepoSubmitRequirementsRoute',
       'handleServerInfoRoute',
       'handleSettingsLegacyRoute',
       'handleSettingsRoute',
@@ -727,6 +729,28 @@
         });
       });
 
+      test('REPO_SUBMIT_REQUIREMENTS', async () => {
+        // REPO_SUBMIT_REQUIREMENTS: /^\/admin\/repos\/(.+),submit-requirements$/,
+        await checkUrlToState('/admin/repos/4321,submit-requirements', {
+          ...createRepoViewState(),
+          detail: RepoDetailView.SUBMIT_REQUIREMENTS,
+          repo: '4321' as RepoName,
+          filter: '',
+          offset: '',
+        });
+      });
+
+      test('REPO_LABELS', async () => {
+        // REPO_LABELS: /^\/admin\/repos\/(.+),labels\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+        await checkUrlToState('/admin/repos/4321,labels', {
+          ...createRepoViewState(),
+          detail: RepoDetailView.LABELS,
+          repo: '4321' as RepoName,
+          filter: '',
+          offset: '',
+        });
+      });
+
       test('BRANCH_LIST', async () => {
         await checkUrlToState('/admin/repos/4321,branches', {
           ...createRepoBranchesViewState(),
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-autocomplete/gr-search-autocomplete.ts
similarity index 74%
rename from polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
rename to polygerrit-ui/app/elements/core/gr-search-autocomplete/gr-search-autocomplete.ts
index 7a34a04..b61c56e 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-autocomplete/gr-search-autocomplete.ts
@@ -5,32 +5,28 @@
  */
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-icon/gr-icon';
-import {ServerInfo} from '../../../types/common';
 import {
   AutocompleteQuery,
-  AutocompleteSuggestion,
   GrAutocomplete,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {MergeabilityComputationBehavior} from '../../../constants/constants';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {
   customElement,
   property,
-  state,
   query as queryDec,
+  state,
 } from 'lit/decorators.js';
-import {Shortcut, ShortcutController} from '../../lit/shortcut-controller';
 import {assertIsDefined} from '../../../utils/common-util';
 import {configModelToken} from '../../../models/config/config-model';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
-import {
-  AutocompleteCommitEvent,
-  ValueChangedEvent,
-} from '../../../types/events';
-import {fireNoBubbleNoCompose} from '../../../utils/event-util';
+import {ValueChangedEvent} from '../../../types/events';
 import {getDocUrl} from '../../../utils/url-util';
+import '@material/web/iconbutton/icon-button';
+import {when} from 'lit/directives/when.js';
 
 // Possible static search options for auto complete, without negations.
 const SEARCH_OPERATORS: ReadonlyArray<string> = [
@@ -66,6 +62,7 @@
   'has:edit',
   'has:star',
   'has:unresolved',
+  'hasfooter:',
   'hashtag:',
   'inhashtag:',
   'intopic:',
@@ -137,19 +134,12 @@
   expression: string
 ) => Promise<AutocompleteSuggestion[]>;
 
-export interface SearchBarHandleSearchDetail {
-  inputVal: string;
-}
+@customElement('gr-search-autocomplete')
+export class GrSearchAutocomplete extends LitElement {
+  @queryDec('#queryInput') protected queryInput?: GrAutocomplete;
 
-@customElement('gr-search-bar')
-export class GrSearchBar extends LitElement {
-  /**
-   * Fired when a search is committed
-   *
-   * @event handle-search
-   */
-
-  @queryDec('#searchInput') protected searchInput?: GrAutocomplete;
+  @property({type: String})
+  placeholder = 'Search for changes';
 
   @property({type: String})
   value = '';
@@ -164,9 +154,6 @@
   accountSuggestions: SuggestionProvider = () => Promise.resolve([]);
 
   @state()
-  serverConfig?: ServerInfo;
-
-  @state()
   mergeabilityComputationBehavior?: MergeabilityComputationBehavior;
 
   // private but used in test
@@ -177,16 +164,11 @@
 
   @state() private query: AutocompleteQuery;
 
-  @state() private threshold = 1;
-
-  private readonly shortcuts = new ShortcutController(this);
-
   private readonly getConfigModel = resolve(this, configModelToken);
 
   constructor() {
     super();
     this.query = (input: string) => this.getSearchSuggestions(input);
-    this.shortcuts.addAbstract(Shortcut.SEARCH, () => this.handleSearch());
     subscribe(
       this,
       () => this.getConfigModel().mergeabilityComputationBehavior$,
@@ -205,18 +187,23 @@
     return [
       sharedStyles,
       css`
-        gr-icon.searchIcon {
-          margin: 0 var(--spacing-xs);
-        }
         form {
           display: flex;
         }
         gr-autocomplete {
           background-color: var(--view-background-color);
-          border-radius: var(--border-radius);
+          border-radius: var(--gr-search-bar-border-radius, 0);
+          --gr-autocomplete-text-field-border-radius: var(
+            --gr-search-bar-border-radius,
+            0
+          );
           flex: 1;
           outline: none;
         }
+
+        md-icon-button {
+          --md-icon-button-icon-size: 20px;
+        }
       `,
     ];
   }
@@ -225,33 +212,53 @@
     return html`
       <form>
         <gr-autocomplete
-          id="searchInput"
-          label="Search for changes"
+          id="queryInput"
+          placeholder=${this.placeholder}
           .text=${this.inputVal}
           .query=${this.query}
           allow-non-suggested-values
           multi
-          .threshold=${this.threshold}
+          skip-commit-on-item-select
           tab-complete
-          .verticalOffset=${30}
-          @commit=${(e: AutocompleteCommitEvent) => {
-            this.handleInputCommit(e);
-          }}
           @text-changed=${(e: ValueChangedEvent) => {
-            this.handleSearchTextChanged(e);
+            this.handleQueryTextChanged(e);
           }}
         >
-          <gr-icon icon="search" class="searchIcon" slot="prefix"></gr-icon>
-          <a
-            class="help"
-            slot="suffix"
-            href=${getDocUrl(this.docsBaseUrl, 'user-search.html')}
-            target="_blank"
-            rel="noopener noreferrer"
-            tabindex="-1"
-          >
-            <gr-icon icon="help" title="read documentation"></gr-icon>
-          </a>
+          <div slot="leading-icon">
+            <slot name="leading-icon"></slot>
+          </div>
+          ${when(
+            this.inputVal?.length > 0,
+            () => html`
+              <md-icon-button
+                slot="trailing-icon"
+                touch-target="none"
+                @click=${(e: Event) => {
+                  e.preventDefault();
+                  this.queryInput?.clear();
+                }}
+              >
+                <gr-icon icon="close" title="Clear all text"></gr-icon>
+              </md-icon-button>
+            `
+          )}
+          ${when(
+            !this.inputVal?.length,
+            () => html`
+              <a
+                class="help"
+                slot="trailing-icon"
+                href=${getDocUrl(this.docsBaseUrl, 'user-search.html')}
+                target="_blank"
+                rel="noopener noreferrer"
+                tabindex="-1"
+              >
+                <md-icon-button touch-target="none" type="button">
+                  <gr-icon icon="help" title="read documentation"></gr-icon>
+                </md-icon-button>
+              </a>
+            `
+          )}
         </gr-autocomplete>
       </form>
     `;
@@ -267,6 +274,10 @@
     this.inputVal = this.value;
   }
 
+  getInput() {
+    return this.inputVal;
+  }
+
   private searchOperators() {
     const set = new Set(SEARCH_OPERATORS_WITH_NEGATIONS_SET);
     if (
@@ -281,34 +292,6 @@
     return set;
   }
 
-  private handleInputCommit(e: AutocompleteCommitEvent) {
-    this.preventDefaultAndNavigateToInputVal(e);
-  }
-
-  /**
-   * This function is called in a few different cases:
-   * - e.target is the search button
-   * - e.target is the gr-autocomplete widget (#searchInput)
-   * - e.target is the input element wrapped within #searchInput
-   */
-  private preventDefaultAndNavigateToInputVal(e: AutocompleteCommitEvent) {
-    e.preventDefault();
-    if (!this.inputVal) return;
-    const trimmedInput = this.inputVal.trim();
-    if (trimmedInput) {
-      const predefinedOpOnlyQuery = [...this.searchOperators()].some(
-        op => op.endsWith(':') && op === trimmedInput
-      );
-      if (predefinedOpOnlyQuery) {
-        return;
-      }
-      const detail: SearchBarHandleSearchDetail = {
-        inputVal: this.inputVal,
-      };
-      fireNoBubbleNoCompose(this, 'handle-search', detail);
-    }
-  }
-
   /**
    * Determine what array of possible suggestions should be provided
    * to getSearchSuggestions.
@@ -404,19 +387,19 @@
     });
   }
 
-  private handleSearch() {
-    assertIsDefined(this.searchInput, 'searchInput');
-    this.searchInput.focus();
-    this.searchInput.selectAll();
+  focusAndSelectAll() {
+    assertIsDefined(this.queryInput, 'queryInput');
+    this.queryInput.focus();
+    this.queryInput.selectAll();
   }
 
-  private handleSearchTextChanged(e: ValueChangedEvent) {
+  private handleQueryTextChanged(e: ValueChangedEvent) {
     this.inputVal = e.detail.value;
   }
 }
 
 declare global {
   interface HTMLElementTagNameMap {
-    'gr-search-bar': GrSearchBar;
+    'gr-search-autocomplete': GrSearchAutocomplete;
   }
 }
diff --git a/polygerrit-ui/app/elements/core/gr-search-autocomplete/gr-search-autocomplete_test.ts b/polygerrit-ui/app/elements/core/gr-search-autocomplete/gr-search-autocomplete_test.ts
new file mode 100644
index 0000000..80de410
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-search-autocomplete/gr-search-autocomplete_test.ts
@@ -0,0 +1,198 @@
+/**
+ * @license
+ * Copyright 2015 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-search-autocomplete';
+import {GrSearchAutocomplete} from './gr-search-autocomplete';
+import '../../../utils/async-util';
+import {stubRestApi, waitUntilObserved} from '../../../test/test-utils';
+import {
+  createChangeConfig,
+  createServerInfo,
+} from '../../../test/test-data-generators';
+import {MergeabilityComputationBehavior} from '../../../constants/constants';
+import {assert, fixture, html} from '@open-wc/testing';
+import {getAppContext} from '../../../services/app-context';
+import {changeModelToken} from '../../../models/change/change-model';
+import {
+  ConfigModel,
+  configModelToken,
+} from '../../../models/config/config-model';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {testResolver} from '../../../test/common-test-setup';
+
+suite('gr-search-autocomplete tests', () => {
+  let element: GrSearchAutocomplete;
+  let configModel: ConfigModel;
+
+  setup(async () => {
+    const serverConfig = createServerInfo();
+    serverConfig.gerrit.doc_url = 'https://mydocumentationurl.google.com/';
+    stubRestApi('getConfig').returns(Promise.resolve(serverConfig));
+    configModel = new ConfigModel(
+      testResolver(changeModelToken),
+      getAppContext().restApiService
+    );
+    configModel.updateServerConfig(serverConfig);
+    await waitUntilObserved(
+      configModel.docsBaseUrl$,
+      docsBaseUrl => docsBaseUrl === 'https://mydocumentationurl.google.com/'
+    );
+
+    element = (
+      await fixture(
+        wrapInProvider(
+          html`<gr-search-autocomplete></gr-search-autocomplete>`,
+          configModelToken,
+          configModel
+        )
+      )
+    ).querySelector('gr-search-autocomplete')!;
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <form>
+          <gr-autocomplete
+            allow-non-suggested-values=""
+            id="queryInput"
+            multi=""
+            placeholder="Search for changes"
+            skip-commit-on-item-select=""
+            tab-complete=""
+          >
+            <div slot="leading-icon">
+              <slot name="leading-icon"></slot>
+            </div>
+            <a
+              class="help"
+              href="https://mydocumentationurl.google.com/user-search.html"
+              rel="noopener noreferrer"
+              slot="trailing-icon"
+              tabindex="-1"
+              target="_blank"
+            >
+              <md-icon-button touch-target="none" type="button" value="">
+                <gr-icon icon="help" title="read documentation"> </gr-icon>
+              </md-icon-button>
+            </a>
+          </gr-autocomplete>
+        </form>
+      `
+    );
+  });
+
+  test('value is propagated to inputVal', async () => {
+    element.value = 'foo';
+    await element.updateComplete;
+    assert.equal(element.inputVal, 'foo');
+  });
+
+  suite('getSearchSuggestions', () => {
+    setup(async () => {
+      element = await fixture(
+        html`<gr-search-autocomplete></gr-search-autocomplete>`
+      );
+      element.mergeabilityComputationBehavior =
+        MergeabilityComputationBehavior.NEVER;
+      await element.updateComplete;
+    });
+
+    test('Autocompletes accounts', async () => {
+      element.accountSuggestions = () =>
+        Promise.resolve([{text: 'owner:fred@goog.co'}]);
+      await element.updateComplete;
+      const s = await element.getSearchSuggestions('owner:fr');
+      assert.equal(s[0].value, 'owner:fred@goog.co');
+    });
+
+    test('Autocompletes groups', async () => {
+      element.groupSuggestions = () =>
+        Promise.resolve([
+          {text: 'ownerin:Polygerrit'},
+          {text: 'ownerin:gerrit'},
+        ]);
+      await element.updateComplete;
+      const s = await element.getSearchSuggestions('ownerin:pol');
+      assert.equal(s[0].value, 'ownerin:Polygerrit');
+    });
+
+    test('Autocompletes projects', async () => {
+      element.projectSuggestions = () =>
+        Promise.resolve([
+          {text: 'project:Polygerrit'},
+          {text: 'project:gerrit'},
+          {text: 'project:gerrittest'},
+        ]);
+      await element.updateComplete;
+      const s = await element.getSearchSuggestions('project:pol');
+      assert.equal(s[0].value, 'project:Polygerrit');
+    });
+
+    test('Autocompletes simple searches', async () => {
+      const s = await element.getSearchSuggestions('is:o');
+      assert.equal(s[0].name, 'is:open');
+      assert.equal(s[0].value, 'is:open');
+      assert.equal(s[1].name, 'is:owner');
+      assert.equal(s[1].value, 'is:owner');
+    });
+
+    test('Does not autocomplete with no match', async () => {
+      const s = await element.getSearchSuggestions('asdasdasdasd');
+      assert.equal(s.length, 0);
+    });
+
+    test('Autocompletes without is:mergable when disabled', async () => {
+      const s = await element.getSearchSuggestions('is:mergeab');
+      assert.isEmpty(s);
+    });
+
+    test('Autocompletes correctly second condition', async () => {
+      const s = await element.getSearchSuggestions('is:open me');
+      assert.equal(s[0].value, 'mergedafter:');
+    });
+
+    test('Autocomplete handles space before expression correctly', async () => {
+      // This previously suggested "mergedafter" (incorrectly) due to the
+      // leading space.
+      const s = await element.getSearchSuggestions('author: me');
+      assert.isEmpty(s);
+    });
+  });
+
+  [
+    'API_REF_UPDATED_AND_CHANGE_REINDEX',
+    'REF_UPDATED_AND_CHANGE_REINDEX',
+  ].forEach(mergeability => {
+    suite(`mergeability as ${mergeability}`, () => {
+      setup(async () => {
+        element = await fixture(
+          html`<gr-search-autocomplete></gr-search-autocomplete>`
+        );
+        const serverConfig = {
+          ...createServerInfo(),
+          change: {
+            ...createChangeConfig(),
+            mergeability_computation_behavior:
+              mergeability as MergeabilityComputationBehavior,
+          },
+        };
+        configModel.updateServerConfig(serverConfig);
+        await element.updateComplete;
+      });
+
+      test('Autocompltes with is:mergable when enabled', async () => {
+        const s = await element.getSearchSuggestions('is:mergeab');
+        assert.equal(s.length, 2);
+        assert.equal(s[0].name, 'is:mergeable');
+        assert.equal(s[0].value, 'is:mergeable');
+        assert.equal(s[1].name, '-is:mergeable');
+        assert.equal(s[1].value, '-is:mergeable');
+      });
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
deleted file mode 100644
index 62ef654..0000000
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ /dev/null
@@ -1,302 +0,0 @@
-/**
- * @license
- * Copyright 2015 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import * as sinon from 'sinon';
-import '../../../test/common-test-setup';
-import './gr-search-bar';
-import {GrSearchBar} from './gr-search-bar';
-import '../../../utils/async-util';
-import {
-  mockPromise,
-  pressKey,
-  stubRestApi,
-  waitUntil,
-  waitUntilObserved,
-} from '../../../test/test-utils';
-import {
-  createChangeConfig,
-  createServerInfo,
-} from '../../../test/test-data-generators';
-import {MergeabilityComputationBehavior} from '../../../constants/constants';
-import {queryAndAssert} from '../../../test/test-utils';
-import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
-import {fixture, html, assert} from '@open-wc/testing';
-import {Key} from '../../../utils/dom-util';
-import {getAppContext} from '../../../services/app-context';
-import {changeModelToken} from '../../../models/change/change-model';
-import {
-  ConfigModel,
-  configModelToken,
-} from '../../../models/config/config-model';
-import {wrapInProvider} from '../../../models/di-provider-element';
-import {testResolver} from '../../../test/common-test-setup';
-
-suite('gr-search-bar tests', () => {
-  let element: GrSearchBar;
-  let configModel: ConfigModel;
-
-  setup(async () => {
-    const serverConfig = createServerInfo();
-    serverConfig.gerrit.doc_url = 'https://mydocumentationurl.google.com/';
-    stubRestApi('getConfig').returns(Promise.resolve(serverConfig));
-    configModel = new ConfigModel(
-      testResolver(changeModelToken),
-      getAppContext().restApiService
-    );
-    configModel.updateServerConfig(serverConfig);
-    await waitUntilObserved(
-      configModel.docsBaseUrl$,
-      docsBaseUrl => docsBaseUrl === 'https://mydocumentationurl.google.com/'
-    );
-
-    element = (
-      await fixture(
-        wrapInProvider(
-          html`<gr-search-bar></gr-search-bar>`,
-          configModelToken,
-          configModel
-        )
-      )
-    ).querySelector('gr-search-bar')!;
-  });
-
-  test('renders', () => {
-    assert.shadowDom.equal(
-      element,
-      /* HTML */ `
-        <form>
-          <gr-autocomplete
-            allow-non-suggested-values=""
-            id="searchInput"
-            label="Search for changes"
-            multi=""
-            tab-complete=""
-          >
-            <gr-icon icon="search" class="searchIcon" slot="prefix"></gr-icon>
-            <a
-              class="help"
-              href="https://mydocumentationurl.google.com/user-search.html"
-              slot="suffix"
-              tabindex="-1"
-              target="_blank"
-              rel="noopener noreferrer"
-            >
-              <gr-icon icon="help" title="read documentation"></gr-icon>
-            </a>
-          </gr-autocomplete>
-        </form>
-      `
-    );
-  });
-
-  test('value is propagated to inputVal', async () => {
-    element.value = 'foo';
-    await element.updateComplete;
-    assert.equal(element.inputVal, 'foo');
-  });
-
-  const getActiveElement = () =>
-    document.activeElement!.shadowRoot
-      ? document.activeElement!.shadowRoot.activeElement
-      : document.activeElement;
-
-  test('enter in search input fires event', async () => {
-    const promise = mockPromise();
-    element.addEventListener('handle-search', () => {
-      assert.notEqual(
-        getActiveElement(),
-        queryAndAssert<GrAutocomplete>(element, '#searchInput')
-      );
-      promise.resolve();
-    });
-    element.value = 'test';
-    await element.updateComplete;
-    const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
-    pressKey(
-      queryAndAssert<PaperInputElement>(searchInput, '#input'),
-      Key.ENTER
-    );
-    await promise;
-  });
-
-  test('empty search query does not trigger nav', async () => {
-    const searchSpy = sinon.spy();
-    element.addEventListener('handle-search', searchSpy);
-    element.value = '';
-    await element.updateComplete;
-    const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
-    pressKey(
-      queryAndAssert<PaperInputElement>(searchInput, '#input'),
-      Key.ENTER
-    );
-    assert.isFalse(searchSpy.called);
-  });
-
-  test('Predefined query op with no predication doesnt trigger nav', async () => {
-    const searchSpy = sinon.spy();
-    element.addEventListener('handle-search', searchSpy);
-    element.value = 'added:';
-    await element.updateComplete;
-    const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
-    pressKey(
-      queryAndAssert<PaperInputElement>(searchInput, '#input'),
-      Key.ENTER
-    );
-    assert.isFalse(searchSpy.called);
-  });
-
-  test('predefined predicate query triggers nav', async () => {
-    const searchSpy = sinon.spy();
-    element.addEventListener('handle-search', searchSpy);
-    element.value = 'age:1week';
-    await element.updateComplete;
-    const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
-    pressKey(
-      queryAndAssert<PaperInputElement>(searchInput, '#input'),
-      Key.ENTER
-    );
-    await waitUntil(() => searchSpy.called);
-  });
-
-  test('undefined predicate query triggers nav', async () => {
-    const searchSpy = sinon.spy();
-    element.addEventListener('handle-search', searchSpy);
-    element.value = 'random:1week';
-    await element.updateComplete;
-    const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
-    pressKey(
-      queryAndAssert<PaperInputElement>(searchInput, '#input'),
-      Key.ENTER
-    );
-    await waitUntil(() => searchSpy.called);
-  });
-
-  test('empty undefined predicate query triggers nav', async () => {
-    const searchSpy = sinon.spy();
-    element.addEventListener('handle-search', searchSpy);
-    element.value = 'random:';
-    await element.updateComplete;
-    const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
-    pressKey(
-      queryAndAssert<PaperInputElement>(searchInput, '#input'),
-      Key.ENTER
-    );
-    await waitUntil(() => searchSpy.called);
-  });
-
-  test('keyboard shortcuts', async () => {
-    const focusSpy = sinon.spy(
-      queryAndAssert<GrAutocomplete>(element, '#searchInput'),
-      'focus'
-    );
-    const selectAllSpy = sinon.spy(
-      queryAndAssert<GrAutocomplete>(element, '#searchInput'),
-      'selectAll'
-    );
-    pressKey(document.body, '/');
-    assert.isTrue(focusSpy.called);
-    assert.isTrue(selectAllSpy.called);
-  });
-
-  suite('getSearchSuggestions', () => {
-    setup(async () => {
-      element = await fixture(html`<gr-search-bar></gr-search-bar>`);
-      element.mergeabilityComputationBehavior =
-        MergeabilityComputationBehavior.NEVER;
-      await element.updateComplete;
-    });
-
-    test('Autocompletes accounts', async () => {
-      element.accountSuggestions = () =>
-        Promise.resolve([{text: 'owner:fred@goog.co'}]);
-      await element.updateComplete;
-      const s = await element.getSearchSuggestions('owner:fr');
-      assert.equal(s[0].value, 'owner:fred@goog.co');
-    });
-
-    test('Autocompletes groups', async () => {
-      element.groupSuggestions = () =>
-        Promise.resolve([
-          {text: 'ownerin:Polygerrit'},
-          {text: 'ownerin:gerrit'},
-        ]);
-      await element.updateComplete;
-      const s = await element.getSearchSuggestions('ownerin:pol');
-      assert.equal(s[0].value, 'ownerin:Polygerrit');
-    });
-
-    test('Autocompletes projects', async () => {
-      element.projectSuggestions = () =>
-        Promise.resolve([
-          {text: 'project:Polygerrit'},
-          {text: 'project:gerrit'},
-          {text: 'project:gerrittest'},
-        ]);
-      await element.updateComplete;
-      const s = await element.getSearchSuggestions('project:pol');
-      assert.equal(s[0].value, 'project:Polygerrit');
-    });
-
-    test('Autocompletes simple searches', async () => {
-      const s = await element.getSearchSuggestions('is:o');
-      assert.equal(s[0].name, 'is:open');
-      assert.equal(s[0].value, 'is:open');
-      assert.equal(s[1].name, 'is:owner');
-      assert.equal(s[1].value, 'is:owner');
-    });
-
-    test('Does not autocomplete with no match', async () => {
-      const s = await element.getSearchSuggestions('asdasdasdasd');
-      assert.equal(s.length, 0);
-    });
-
-    test('Autocompletes without is:mergable when disabled', async () => {
-      const s = await element.getSearchSuggestions('is:mergeab');
-      assert.isEmpty(s);
-    });
-
-    test('Autocompletes correctly second condition', async () => {
-      const s = await element.getSearchSuggestions('is:open me');
-      assert.equal(s[0].value, 'mergedafter:');
-    });
-
-    test('Autocomplete handles space before expression correctly', async () => {
-      // This previously suggested "mergedafter" (incorrectly) due to the
-      // leading space.
-      const s = await element.getSearchSuggestions('author: me');
-      assert.isEmpty(s);
-    });
-  });
-
-  [
-    'API_REF_UPDATED_AND_CHANGE_REINDEX',
-    'REF_UPDATED_AND_CHANGE_REINDEX',
-  ].forEach(mergeability => {
-    suite(`mergeability as ${mergeability}`, () => {
-      setup(async () => {
-        element = await fixture(html`<gr-search-bar></gr-search-bar>`);
-        element.serverConfig = {
-          ...createServerInfo(),
-          change: {
-            ...createChangeConfig(),
-            mergeability_computation_behavior:
-              mergeability as MergeabilityComputationBehavior,
-          },
-        };
-        await element.updateComplete;
-      });
-
-      test('Autocompltes with is:mergable when enabled', async () => {
-        const s = await element.getSearchSuggestions('is:mergeab');
-        assert.equal(s.length, 2);
-        assert.equal(s[0].name, 'is:mergeable');
-        assert.equal(s[0].value, 'is:mergeable');
-        assert.equal(s[1].name, '-is:mergeable');
-        assert.equal(s[1].value, '-is:mergeable');
-      });
-    });
-  });
-});
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index 48f5a05..3723eb7 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -3,18 +3,16 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '../gr-search-bar/gr-search-bar';
+import '../gr-search-autocomplete/gr-search-autocomplete';
 import {navigationToken} from '../gr-navigation/gr-navigation';
-import {getUserName} from '../../../utils/display-name-util';
-import {AccountInfo, ServerInfo} from '../../../types/common';
+import {ServerInfo} from '../../../types/common';
 import {
-  SearchBarHandleSearchDetail,
+  GrSearchAutocomplete,
   SuggestionProvider,
-} from '../gr-search-bar/gr-search-bar';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+} from '../gr-search-autocomplete/gr-search-autocomplete';
 import {getAppContext} from '../../../services/app-context';
-import {LitElement, html} from 'lit';
-import {customElement, state} from 'lit/decorators.js';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
@@ -23,15 +21,16 @@
   searchViewModelToken,
 } from '../../../models/views/search';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {AutocompleteCommitEvent} from '../../../types/events';
+import {Shortcut, ShortcutController} from '../../lit/shortcut-controller';
+import {
+  AutocompleteSuggestion,
+  fetchAccountSuggestions,
+} from '../../../utils/autocomplete-util';
 
 const MAX_AUTOCOMPLETE_RESULTS = 10;
-const SELF_EXPRESSION = 'self';
-const ME_EXPRESSION = 'me';
 
 declare global {
-  interface HTMLElementEventMap {
-    'handle-search': CustomEvent<SearchBarHandleSearchDetail>;
-  }
   interface HTMLElementTagNameMap {
     'gr-smart-search': GrSmartSearch;
   }
@@ -39,12 +38,18 @@
 
 @customElement('gr-smart-search')
 export class GrSmartSearch extends LitElement {
+  @property({type: Number})
+  verticalOffset = 31;
+
   @state()
   searchQuery = '';
 
   @state()
   serverConfig?: ServerInfo;
 
+  @query('gr-search-autocomplete')
+  searchBar?: GrSearchAutocomplete;
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly getConfigModel = resolve(this, configModelToken);
@@ -53,6 +58,8 @@
 
   private readonly getSearchViewModel = resolve(this, searchViewModelToken);
 
+  private readonly shortcuts = new ShortcutController(this);
+
   constructor() {
     super();
     subscribe(
@@ -65,29 +72,66 @@
       () => this.getSearchViewModel().query$,
       query => (this.searchQuery = query ?? '')
     );
+    this.shortcuts.addAbstract(Shortcut.SEARCH, () => this.focusAndSelectAll());
+  }
+
+  static override get styles() {
+    return css`
+      gr-search-autocomplete {
+        --gr-search-bar-border-radius: 50px;
+      }
+    `;
   }
 
   override render() {
-    const accountSuggestions: SuggestionProvider = (predicate, expression) =>
-      this.fetchAccounts(predicate, expression);
+    const accountSuggestions: SuggestionProvider = (predicate, expression) => {
+      const accountFetcher = (expr: string) =>
+        this.restApiService.queryAccounts(
+          expr,
+          MAX_AUTOCOMPLETE_RESULTS,
+          undefined,
+          undefined,
+          throwingErrorCallback
+        );
+      return fetchAccountSuggestions(
+        accountFetcher,
+        predicate,
+        expression,
+        this.serverConfig
+      );
+    };
     const groupSuggestions: SuggestionProvider = (predicate, expression) =>
       this.fetchGroups(predicate, expression);
     const projectSuggestions: SuggestionProvider = (predicate, expression) =>
       this.fetchProjects(predicate, expression);
     return html`
-      <gr-search-bar
+      <gr-search-autocomplete
         id="search"
         .value=${this.searchQuery}
         .projectSuggestions=${projectSuggestions}
         .groupSuggestions=${groupSuggestions}
         .accountSuggestions=${accountSuggestions}
-        @handle-search=${(e: CustomEvent<SearchBarHandleSearchDetail>) => {
-          this.handleSearch(e);
-        }}
-      ></gr-search-bar>
+        .verticalOffset=${this.verticalOffset}
+        @commit=${this.handleInputCommit}
+      >
+        <gr-icon icon="search" slot="leading-icon" aria-hidden="true"></gr-icon>
+      </gr-search-autocomplete>
     `;
   }
 
+  private focusAndSelectAll() {
+    this.searchBar?.focusAndSelectAll();
+  }
+
+  private handleInputCommit(e: CustomEvent<AutocompleteCommitEvent>) {
+    e.preventDefault();
+    if (!this.searchBar) return;
+    const trimmedInput = this.searchBar.getInput().trim();
+    if (trimmedInput) {
+      this.handleSearch(trimmedInput);
+    }
+  }
+
   /**
    * Fetch from the API the predicted projects.
    *
@@ -154,67 +198,7 @@
       });
   }
 
-  /**
-   * Fetch from the API the predicted accounts.
-   *
-   * @param predicate - The first part of the search term, e.g.
-   * 'owner'
-   * @param expression - The second part of the search term, e.g.
-   * 'kasp'
-   *
-   * private but used in test
-   */
-  fetchAccounts(
-    predicate: string,
-    expression: string
-  ): Promise<AutocompleteSuggestion[]> {
-    if (expression.length === 0) {
-      return Promise.resolve([]);
-    }
-    return this.restApiService
-      .queryAccounts(
-        expression,
-        MAX_AUTOCOMPLETE_RESULTS,
-        /* canSee=*/ undefined,
-        /* filterActive=*/ undefined,
-        throwingErrorCallback
-      )
-      .then(accounts => {
-        if (!accounts) {
-          return [];
-        }
-        return this.mapAccountsHelper(accounts, predicate);
-      })
-      .then(accounts => {
-        // When the expression supplied is a beginning substring of 'self',
-        // add it as an autocomplete option.
-        if (SELF_EXPRESSION.startsWith(expression)) {
-          return accounts.concat([{text: predicate + ':' + SELF_EXPRESSION}]);
-        } else if (ME_EXPRESSION.startsWith(expression)) {
-          return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
-        } else {
-          return accounts;
-        }
-      });
-  }
-
-  private mapAccountsHelper(
-    accounts: AccountInfo[],
-    predicate: string
-  ): AutocompleteSuggestion[] {
-    return accounts.map(account => {
-      const userName = getUserName(this.serverConfig, account);
-      return {
-        label: account.name || '',
-        text: account.email
-          ? `${predicate}:${account.email}`
-          : `${predicate}:"${userName}"`,
-      };
-    });
-  }
-
-  private handleSearch(e: CustomEvent<SearchBarHandleSearchDetail>) {
-    const query = e.detail.inputVal;
+  handleSearch(query: string) {
     if (!query) return;
     this.getNavigation().setUrl(createSearchUrl({query}));
   }
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
index 0551f23..3e9b15e 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -6,9 +6,17 @@
 import '../../../test/common-test-setup';
 import './gr-smart-search';
 import {GrSmartSearch} from './gr-smart-search';
-import {stubRestApi} from '../../../test/test-utils';
-import {EmailAddress, GroupId, UrlEncodedRepoName} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {
+  pressKey,
+  queryAndAssert,
+  stubRestApi,
+  waitUntil,
+} from '../../../test/test-utils';
+import {GroupId, UrlEncodedRepoName} from '../../../types/common';
+import {assert, fixture, html} from '@open-wc/testing';
+import {Key} from '../../../utils/dom-util';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 
 suite('gr-smart-search tests', () => {
   let element: GrSmartSearch;
@@ -20,64 +28,83 @@
   test('renders', () => {
     assert.shadowDom.equal(
       element,
-      /* HTML */ ' <gr-search-bar id="search"> </gr-search-bar> '
+      /* HTML */ ' <gr-search-autocomplete id="search"> <gr-icon icon="search" slot="leading-icon" aria-hidden="true"></gr-icon> </gr-search-autocomplete> '
     );
   });
 
-  test('Autocompletes accounts', () => {
-    stubRestApi('queryAccounts').callsFake(() =>
-      Promise.resolve([
-        {
-          name: 'fred',
-          email: 'fred@goog.co' as EmailAddress,
-        },
-      ])
+  test('empty search query does not trigger nav', async () => {
+    const searchSpy = sinon.spy(element, 'handleSearch');
+    element.searchBar!.value = '';
+    await element.updateComplete;
+    const searchInput = queryAndAssert<GrAutocomplete>(
+      element.searchBar,
+      '#queryInput'
     );
-    return element.fetchAccounts('owner', 'fr').then(s => {
-      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-    });
+    pressKey(
+      queryAndAssert<MdOutlinedTextField>(searchInput, '#input'),
+      Key.ENTER
+    );
+    assert.isFalse(searchSpy.called);
   });
 
-  test('Inserts self as option when valid', () => {
-    stubRestApi('queryAccounts').callsFake(() =>
-      Promise.resolve([
-        {
-          name: 'fred',
-          email: 'fred@goog.co' as EmailAddress,
-        },
-      ])
+  test('Predefined query op with no predication doesnt trigger nav', async () => {
+    const searchSpy = sinon.spy(element, 'handleSearch');
+    element.searchBar!.value = 'added:';
+    await element.updateComplete;
+    const searchInput = queryAndAssert<GrAutocomplete>(
+      element.searchBar,
+      '#queryInput'
     );
-    element
-      .fetchAccounts('owner', 's')
-      .then(s => {
-        assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-        assert.deepEqual(s[1], {text: 'owner:self'});
-      })
-      .then(() => element.fetchAccounts('owner', 'selfs'))
-      .then(s => {
-        assert.notEqual(s[0], {text: 'owner:self'});
-      });
+    pressKey(
+      queryAndAssert<MdOutlinedTextField>(searchInput, '#input'),
+      Key.ENTER
+    );
+    assert.isFalse(searchSpy.called);
   });
 
-  test('Inserts me as option when valid', () => {
-    stubRestApi('queryAccounts').callsFake(() =>
-      Promise.resolve([
-        {
-          name: 'fred',
-          email: 'fred@goog.co' as EmailAddress,
-        },
-      ])
+  test('predefined predicate query triggers nav', async () => {
+    const searchSpy = sinon.spy(element, 'handleSearch');
+    element.searchBar!.value = 'age:1week';
+    await element.updateComplete;
+    const searchInput = queryAndAssert<GrAutocomplete>(
+      element.searchBar,
+      '#queryInput'
     );
-    return element
-      .fetchAccounts('owner', 'm')
-      .then(s => {
-        assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
-        assert.deepEqual(s[1], {text: 'owner:me'});
-      })
-      .then(() => element.fetchAccounts('owner', 'meme'))
-      .then(s => {
-        assert.notEqual(s[0], {text: 'owner:me'});
-      });
+    pressKey(
+      queryAndAssert<MdOutlinedTextField>(searchInput, '#input'),
+      Key.ENTER
+    );
+    await waitUntil(() => searchSpy.called);
+  });
+
+  test('undefined predicate query triggers nav', async () => {
+    const searchSpy = sinon.spy(element, 'handleSearch');
+    element.searchBar!.value = 'random:1week';
+    await element.updateComplete;
+    const searchInput = queryAndAssert<GrAutocomplete>(
+      element.searchBar,
+      '#queryInput'
+    );
+    pressKey(
+      queryAndAssert<MdOutlinedTextField>(searchInput, '#input'),
+      Key.ENTER
+    );
+    await waitUntil(() => searchSpy.called);
+  });
+
+  test('empty undefined predicate query triggers nav', async () => {
+    const searchSpy = sinon.spy(element, 'handleSearch');
+    element.searchBar!.value = 'random:';
+    await element.updateComplete;
+    const searchInput = queryAndAssert<GrAutocomplete>(
+      element.searchBar,
+      '#queryInput'
+    );
+    pressKey(
+      queryAndAssert<MdOutlinedTextField>(searchInput, '#input'),
+      Key.ENTER
+    );
+    await waitUntil(() => searchSpy.called);
   });
 
   test('Autocompletes groups', () => {
@@ -116,22 +143,4 @@
       assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
     });
   });
-
-  test('Autocompletes accounts with no email', () => {
-    stubRestApi('queryAccounts').callsFake(() =>
-      Promise.resolve([{name: 'fred'}])
-    );
-    return element.fetchAccounts('owner', 'fr').then(s => {
-      assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
-    });
-  });
-
-  test('Autocompletes accounts with email', () => {
-    stubRestApi('queryAccounts').callsFake(() =>
-      Promise.resolve([{email: 'fred@goog.co' as EmailAddress}])
-    );
-    return element.fetchAccounts('owner', 'fr').then(s => {
-      assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
-    });
-  });
 });
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 34200a4..bfdde07 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -9,16 +9,15 @@
 import '../../../embed/diff/gr-diff/gr-diff';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
-  NumericChangeId,
-  EDIT,
-  FixSuggestionInfo,
-  PatchSetNum,
   BasePatchSetNum,
+  EDIT,
   FilePathToDiffInfoMap,
+  FixSuggestionInfo,
+  NumericChangeId,
+  PatchSetNum,
   PatchSetNumber,
 } from '../../../types/common';
 import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {getAppContext} from '../../../services/app-context';
 import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
@@ -348,21 +347,11 @@
     }
     let res: FilePathToDiffInfoMap | undefined;
     try {
-      if (fixSuggestion.fix_id === PROVIDED_FIX_ID) {
-        res = await this.restApiService.getFixPreview(
-          this.changeNum,
-          this.patchNum,
-          fixSuggestion.replacements
-        );
-      } else {
-        // TODO(b/227463363) Remove once Robot Comments are deprecated.
-        // We don't use this for user suggestions or comments.fix_suggestions.
-        res = await this.restApiService.getRobotCommentFixPreview(
-          this.changeNum,
-          this.patchNum,
-          fixSuggestion.fix_id
-        );
-      }
+      res = await this.restApiService.getFixPreview(
+        this.changeNum,
+        this.patchNum,
+        fixSuggestion.replacements
+      );
       if (res) {
         this.currentPreviews = Object.keys(res).map(key => {
           return {filepath: key, preview: res![key]};
@@ -460,50 +449,35 @@
     this.reporting.time(Timing.APPLY_FIX_LOAD);
     let res: Response | undefined = undefined;
     // Similar to gr-suggestion-diff-preview.ts:applyFix()
-    if (this.fixSuggestions?.[this.selectedFixIdx].fix_id === PROVIDED_FIX_ID) {
-      let errorText = '';
-      let status = '';
-      try {
-        res = await this.restApiService.applyFixSuggestion(
-          changeNum,
-          patchNum,
-          this.fixSuggestions[this.selectedFixIdx].replacements,
-          this.latestPatchNum
-        );
-      } catch (error) {
-        if (error instanceof Error) {
-          errorText = error.message;
-          status = errorText.match(/\b\d{3}\b/)?.[0] || '';
-        }
-        fireError(this, `Applying Fix failed.\n${errorText}`);
-      } finally {
-        this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
-          method: 'apply-fix-dialog',
-          description: this.fixSuggestions?.[0].description,
-          fileExtension: getFileExtension(
-            this.fixSuggestions?.[0]?.replacements?.[0].path ?? ''
-          ),
-          success: res?.ok ?? false,
-          status: res?.status ?? status,
-          errorText,
-        });
-      }
-      // Robot Comments are deprecated
-    } else {
-      res = await this.restApiService.applyRobotFixSuggestion(
+    const fixSuggestion = this.fixSuggestions?.[this.selectedFixIdx];
+    if (!fixSuggestion) {
+      throw new Error('No fix suggestion selected');
+    }
+    let errorText = '';
+    let status = '';
+    try {
+      res = await this.restApiService.applyFixSuggestion(
         changeNum,
         patchNum,
-        this.currentFix.fix_id
+        fixSuggestion.replacements,
+        this.latestPatchNum
       );
+    } catch (error) {
+      if (error instanceof Error) {
+        errorText = error.message;
+        status = errorText.match(/\b\d{3}\b/)?.[0] || '';
+      }
+      fireError(this, `Applying Fix failed.\n${errorText}`);
+    } finally {
       this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
         method: 'apply-fix-dialog',
         description: this.fixSuggestions?.[0].description,
-        isRobotComment: true,
         fileExtension: getFileExtension(
-          this.fixSuggestions?.[0].replacements?.[0].path ?? ''
+          this.fixSuggestions?.[0]?.replacements?.[0].path ?? ''
         ),
-        success: res.ok,
-        status: res.status,
+        success: res?.ok ?? false,
+        status: res?.status ?? status,
+        errorText,
       });
     }
     if (res?.ok) {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index 252a982..1d35430 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -21,10 +21,9 @@
   getCurrentRevision,
 } from '../../../test/test-data-generators';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
-import {DiffInfo} from '../../../types/diff';
 import {OpenFixPreviewEventDetail} from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {testResolver} from '../../../test/common-test-setup';
 import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
@@ -90,71 +89,13 @@
 
   suite('dialog open', () => {
     setup(() => {
-      const diffInfo1: DiffInfo = {
-        meta_a: {
-          name: 'f1',
-          content_type: 'text',
-          lines: 10,
-        },
-        meta_b: {
-          name: 'f1',
-          content_type: 'text',
-          lines: 12,
-        },
-        content: [
-          {
-            ab: ['loqlwkqll'],
-          },
-          {
-            b: ['qwqqsqw'],
-          },
-          {
-            ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
-          },
-        ],
-        change_type: 'MODIFIED',
-        intraline_status: 'OK',
-      };
-
-      const diffInfo2: DiffInfo = {
-        meta_a: {
-          name: 'f2',
-          content_type: 'text',
-          lines: 10,
-        },
-        meta_b: {
-          name: 'f2',
-          content_type: 'text',
-          lines: 12,
-        },
-        content: [
-          {
-            ab: ['eqweqweqwex'],
-          },
-          {
-            b: ['zassdasd'],
-          },
-          {
-            ab: ['zassdasd', 'dasdasda', 'asdasdad'],
-          },
-        ],
-        change_type: 'MODIFIED',
-        intraline_status: 'OK',
-      };
-
-      stubRestApi('getRobotCommentFixPreview').returns(
-        Promise.resolve({
-          f1: diffInfo1,
-          f2: diffInfo2,
-        })
-      );
       sinon.stub(element.applyFixModal!, 'showModal');
     });
 
     test('dialog opens fetch and sets previews', async () => {
       await open(TWO_FIXES);
       assert.equal(element.currentFix!.fix_id, 'fix_1');
-      assert.equal(element.currentPreviews.length, 2);
+      assert.equal(element.currentPreviews.length, 0);
       const button = getConfirmButton();
       assert.isFalse(button.hasAttribute('disabled'));
       assert.equal(button.getAttribute('title'), '');
@@ -206,8 +147,6 @@
   });
 
   test('next button state updated when suggestions changed', async () => {
-    stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
-
     await open(ONE_FIX);
     await element.updateComplete;
     assert.notOk(element.nextFix);
@@ -215,64 +154,7 @@
 
     await open(TWO_FIXES);
     assert.ok(element.nextFix);
-    assert.notOk(element.nextFix!.disabled);
-  });
-
-  test('preview endpoint throws error should reset dialog', async () => {
-    stubRestApi('getRobotCommentFixPreview').returns(
-      Promise.reject(new Error('backend error'))
-    );
-    try {
-      await open(TWO_FIXES);
-    } catch (error) {
-      // expected
-    }
-    assert.equal(element.currentFix, undefined);
-  });
-
-  test('apply fix button should call apply, navigate to change view and fire close', async () => {
-    const applyRobotFixSuggestionStub = stubRestApi(
-      'applyRobotFixSuggestion'
-    ).returns(Promise.resolve(new Response(null, {status: 200})));
-    element.currentFix = createFixSuggestionInfo('123');
-    element.hasEdit = true;
-
-    const closeFixPreviewEventSpy = sinon.spy();
-    element.onCloseFixPreviewCallbacks.push(closeFixPreviewEventSpy);
-
-    await element.handleApplyFix(new CustomEvent('confirm'));
-
-    sinon.assert.calledOnceWithExactly(
-      applyRobotFixSuggestionStub,
-      element.change!._number,
-      2 as PatchSetNum,
-      '123'
-    );
-    assert.isTrue(setUrlStub.called);
-    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/2..edit');
-
-    sinon.assert.calledOnceWithExactly(closeFixPreviewEventSpy, true);
-    // reset gr-apply-fix-dialog and close
-    assert.equal(element.currentFix, undefined);
-    assert.equal(element.currentPreviews.length, 0);
-  });
-
-  test('should not navigate to change view if incorect reponse', async () => {
-    const applyRobotFixSuggestionStub = stubRestApi(
-      'applyRobotFixSuggestion'
-    ).returns(Promise.resolve(new Response(null, {status: 500})));
-    element.currentFix = createFixSuggestionInfo('fix_123');
-
-    await element.handleApplyFix(new CustomEvent('confirm'));
-
-    sinon.assert.calledWithExactly(
-      applyRobotFixSuggestionStub,
-      element.change!._number,
-      2 as PatchSetNum,
-      'fix_123'
-    );
-    assert.isFalse(setUrlStub.called);
-    assert.equal(element.isApplyFixLoading, false);
+    assert.notOk(element.nextFix.disabled);
   });
 
   test('select fix forward and back of multiple suggested fixes', async () => {
@@ -285,24 +167,6 @@
     assert.equal(element.currentFix!.fix_id, 'fix_1');
   });
 
-  test('server-error should throw for failed apply call', async () => {
-    stubRestApi('applyRobotFixSuggestion').returns(
-      Promise.reject(new Error('backend error'))
-    );
-    element.currentFix = createFixSuggestionInfo('fix_123');
-
-    const closeFixPreviewEventSpy = sinon.spy();
-    element.onCloseFixPreviewCallbacks.push(closeFixPreviewEventSpy);
-
-    let expectedError;
-    await element.handleApplyFix(new CustomEvent('click')).catch(e => {
-      expectedError = e;
-    });
-    assert.isOk(expectedError);
-    assert.isFalse(setUrlStub.called);
-    sinon.assert.notCalled(closeFixPreviewEventSpy);
-  });
-
   test('onCancel fires close with correct parameters', () => {
     const closeFixPreviewEventSpy = sinon.spy();
     element.onCloseFixPreviewCallbacks.push(closeFixPreviewEventSpy);
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index aedeb6d..55c5af40 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -4,27 +4,26 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {
-  PatchRange,
-  PatchSetNum,
-  RobotCommentInfo,
+  Comment,
+  CommentInfo,
+  CommentMap,
+  CommentThread,
+  DraftInfo,
   FileInfo,
   PARENT,
-  CommentThread,
-  Comment,
-  CommentMap,
-  DraftInfo,
-  CommentInfo,
+  PatchRange,
+  PatchSetNum,
 } from '../../../types/common';
 import {
-  isUnresolved,
-  createCommentThreads,
-  isInPatchRange,
-  isDraftThread,
-  isPatchsetLevel,
   addPath,
+  createCommentThreads,
   id,
+  isDraftThread,
+  isInPatchRange,
+  isPatchsetLevel,
+  isUnresolved,
 } from '../../../utils/comment-util';
-import {PatchSetFile, PatchNumOnly, isPatchSetFile} from '../../../types/types';
+import {isPatchSetFile, PatchNumOnly, PatchSetFile} from '../../../types/types';
 import {CommentSide} from '../../../constants/constants';
 import {pluralize} from '../../../utils/string-util';
 import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
@@ -33,8 +32,6 @@
 export class ChangeComments {
   private readonly _comments: {[path: string]: CommentInfo[]};
 
-  private readonly _robotComments: {[path: string]: RobotCommentInfo[]};
-
   private readonly _drafts: {[path: string]: DraftInfo[]};
 
   private readonly _portedComments: {[path: string]: CommentInfo[]};
@@ -43,13 +40,11 @@
 
   constructor(
     comments?: {[path: string]: CommentInfo[]},
-    robotComments?: {[path: string]: RobotCommentInfo[]},
     drafts?: {[path: string]: DraftInfo[]},
     portedComments?: {[path: string]: CommentInfo[]},
     portedDrafts?: {[path: string]: DraftInfo[]}
   ) {
     this._comments = addPath(comments);
-    this._robotComments = addPath(robotComments);
     this._drafts = addPath(drafts);
     this._portedComments = portedComments || {};
     this._portedDrafts = portedDrafts || {};
@@ -61,8 +56,7 @@
 
   /**
    * Get an object mapping file paths to a boolean representing whether that
-   * path contains diff comments in the given patch set (including drafts and
-   * robot comments).
+   * path contains diff comments in the given patch set (including drafts).
    *
    * Paths with comments are mapped to true, whereas paths without comments
    * are not mapped.
@@ -74,7 +68,6 @@
     const responses: {[path: string]: Comment[]}[] = [
       this._comments,
       this.drafts,
-      this._robotComments,
     ];
     const commentMap: CommentMap = {};
     for (const response of responses) {
@@ -91,14 +84,14 @@
   }
 
   /**
-   * Gets all the comments and robot comments for the given change.
+   * Gets all the comments for the given change.
    */
   getAllPublishedComments(patchNum?: PatchSetNum) {
     return this.getAllComments(false, patchNum);
   }
 
   /**
-   * Gets all the comments and robot comments for the given change.
+   * Gets all the comments for the given change.
    */
   getAllComments(includeDrafts?: boolean, patchNum?: PatchSetNum) {
     const paths = this.getPaths();
@@ -126,7 +119,7 @@
   }
 
   /**
-   * Get the comments (robot comments) for a path and optional patch num.
+   * Get the comments for a path and optional patch num.
    *
    * This method will always return a new shallow copy of all comments,
    * so manipulation on one copy won't affect other copies.
@@ -137,9 +130,7 @@
     patchNum?: PatchSetNum,
     includeDrafts?: boolean
   ): Comment[] {
-    const comments: Comment[] = this._comments[path] || [];
-    const robotComments = this._robotComments[path] || [];
-    let allComments = comments.concat(robotComments);
+    let allComments: Comment[] = this._comments[path] || [];
     if (includeDrafts) {
       const drafts = this.getAllDraftsForPath(path);
       allComments = allComments.concat(drafts);
@@ -153,7 +144,7 @@
   }
 
   /**
-   * Get the comments (robot comments) for a file.
+   * Get the comments for a file.
    *
    * // TODO(taoalpha): maybe merge in *ForPath
    */
@@ -203,7 +194,7 @@
   }
 
   /**
-   * Get the comments (with drafts and robot comments) for a path and
+   * Get the comments (with drafts) for a path and
    * patch-range. Returns an array containing comments from either side of the
    * patch range for that path.
    *
@@ -213,18 +204,14 @@
   getCommentsForPath(path: string, patchRange: PatchRange): Comment[] {
     let comments: Comment[] = [];
     let drafts: DraftInfo[] = [];
-    let robotComments: RobotCommentInfo[] = [];
     if (this._comments && this._comments[path]) {
       comments = this._comments[path];
     }
     if (this.drafts && this.drafts[path]) {
       drafts = this.drafts[path];
     }
-    if (this._robotComments && this._robotComments[path]) {
-      robotComments = this._robotComments[path];
-    }
 
-    const all = comments.concat(drafts).concat(robotComments);
+    const all = comments.concat(drafts);
     const final = all
       .filter(c => isInPatchRange(c, patchRange))
       .map(c => {
@@ -284,10 +271,7 @@
     const allComments: Comment[] = this.getAllCommentsForFile(file, true);
 
     return createCommentThreads(allComments).filter(thread => {
-      // Robot comments and drafts are not ported over. A human reply to
-      // the robot comment will be ported over, therefore it's possible to
-      // have the root comment of the thread not be ported, hence loop over
-      // entire thread
+      // Drafts are not ported over.
       const portedComment = portedComments.find(portedComment =>
         thread.comments.some(c => id(portedComment) === id(c))
       );
@@ -338,7 +322,7 @@
   }
 
   /**
-   * Get the comments (with drafts and robot comments) for a file and
+   * Get the comments (with drafts) for a file and
    * patch-range. Returns an object with left and right properties mapping to
    * arrays of comments in on either side of the patch range for that path.
    *
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts
index 0a0c922..23d2989 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts
@@ -6,19 +6,18 @@
 import '../../../test/common-test-setup';
 import {ChangeComments} from './gr-comment-api';
 import {
-  isInRevisionOfPatchRange,
-  isInBaseOfPatchRange,
-  isDraftThread,
-  isUnresolved,
   createCommentThreads,
+  isDraftThread,
+  isInBaseOfPatchRange,
+  isInRevisionOfPatchRange,
+  isUnresolved,
 } from '../../../utils/comment-util';
 import {
-  createDraft,
-  createComment,
   createChangeComments,
+  createComment,
   createCommentThread,
+  createDraft,
   createFileInfo,
-  createRobotComment,
 } from '../../../test/test-data-generators';
 import {CommentSide, FileInfoStatus} from '../../../constants/constants';
 import {
@@ -30,7 +29,6 @@
   PatchRange,
   PatchSetNum,
   RevisionPatchSetNum,
-  RobotCommentInfo,
   Timestamp,
   UrlEncodedCommentId,
 } from '../../../types/common';
@@ -43,7 +41,6 @@
   suite('_changeComment methods', () => {
     setup(() => {
       stubRestApi('getDiffComments').resolves({});
-      stubRestApi('getDiffRobotComments').resolves({});
       stubRestApi('getDiffDrafts').resolves({});
     });
 
@@ -124,7 +121,6 @@
               comment1,
             ],
           },
-          {} /* robot comments */,
           {} /* drafts */,
           portedComments,
           {} /* ported drafts */
@@ -202,7 +198,6 @@
             // comment that is not ported over
             'karma.conf.js': [comment2],
           },
-          {} /* robot comments */,
           {
             /* drafts */ 'karma.conf.js': [draft2],
           },
@@ -234,7 +229,6 @@
             // comment left on Base
             'karma.conf.js': [comment3],
           },
-          {} /* robot comments */,
           {
             /* drafts */ 'karma.conf.js': [draft2],
           },
@@ -288,7 +282,6 @@
             // comment left on Base
             'karma.conf.js': [comment4],
           },
-          {} /* robot comments */,
           {
             /* drafts */ 'karma.conf.js': [draft2],
           },
@@ -359,7 +352,6 @@
       test('drafts are ported over', () => {
         changeComments = new ChangeComments(
           {} /* comments */,
-          {} /* robotComments */,
           {
             /* drafts */
             // draft1: resolved draft that will be ported over to ps 4
@@ -460,29 +452,14 @@
     suite('comment ranges and paths', () => {
       const comments = [
         {
-          ...createRobotComment(),
+          // legacy from when we were still supporting robot comments
+          ...createComment(),
           id: '01' as UrlEncodedCommentId,
-          patch_set: 2 as RevisionPatchSetNum,
-          path: 'file/1',
-          side: CommentSide.PARENT,
-          line: 1,
-          updated: makeTime(1),
-          range: {
-            start_line: 1,
-            start_character: 2,
-            end_line: 2,
-            end_character: 2,
-          },
         },
         {
-          ...createRobotComment(),
+          // legacy from when we were still supporting robot comments
+          ...createComment(),
           id: '02' as UrlEncodedCommentId,
-          in_reply_to: '04' as UrlEncodedCommentId,
-          patch_set: 2 as RevisionPatchSetNum,
-          path: 'file/1',
-          unresolved: true,
-          line: 1,
-          updated: makeTime(3),
         },
         {
           ...createComment(),
@@ -589,9 +566,6 @@
         'file/1': [comments[11], comments[12]],
         'file/2': [comments[13]],
       };
-      const robotComments: {[path: string]: RobotCommentInfo[]} = {
-        'file/1': [comments[0], comments[1]],
-      };
       const commentsByFile: {[path: string]: CommentInfo[]} = {
         'file/1': [comments[2], comments[3]],
         'file/2': [comments[4], comments[5]],
@@ -606,7 +580,6 @@
       setup(() => {
         changeComments = new ChangeComments(
           commentsByFile,
-          robotComments,
           drafts,
           {} /* portedComments */,
           {} /* portedDrafts */
@@ -697,7 +670,7 @@
       test('getAllCommentsForPath', () => {
         let path = 'file/1';
         let comments = changeComments.getAllCommentsForPath(path);
-        assert.equal(comments.length, 4);
+        assert.equal(comments.length, 2);
         path = 'file/2';
         comments = changeComments.getAllCommentsForPath(path, 2 as PatchSetNum);
         assert.equal(comments.length, 1);
@@ -916,7 +889,7 @@
             patchNum: 2 as PatchSetNum,
             path: 'file/1',
           }).length,
-          3
+          2
         );
         assert.deepEqual(
           changeComments.computeCommentThreads({
@@ -979,32 +952,32 @@
       test('getAllPublishedComments', () => {
         let publishedComments = changeComments.getAllPublishedComments();
         assert.equal(Object.keys(publishedComments).length, 4);
-        assert.equal(Object.keys(publishedComments['file/1']).length, 4);
+        assert.equal(Object.keys(publishedComments['file/1']).length, 2);
         assert.equal(Object.keys(publishedComments['file/2']).length, 2);
         publishedComments = changeComments.getAllPublishedComments(
           2 as PatchSetNum
         );
-        assert.equal(Object.keys(publishedComments['file/1']).length, 4);
+        assert.equal(Object.keys(publishedComments['file/1']).length, 2);
         assert.equal(Object.keys(publishedComments['file/2']).length, 1);
       });
 
       test('getAllComments', () => {
         let comments = changeComments.getAllComments();
         assert.equal(Object.keys(comments).length, 4);
-        assert.equal(Object.keys(comments['file/1']).length, 4);
+        assert.equal(Object.keys(comments['file/1']).length, 2);
         assert.equal(Object.keys(comments['file/2']).length, 2);
         comments = changeComments.getAllComments(false, 2 as PatchSetNum);
         assert.equal(Object.keys(comments).length, 4);
-        assert.equal(Object.keys(comments['file/1']).length, 4);
+        assert.equal(Object.keys(comments['file/1']).length, 2);
         assert.equal(Object.keys(comments['file/2']).length, 1);
         // Include drafts
         comments = changeComments.getAllComments(true);
         assert.equal(Object.keys(comments).length, 4);
-        assert.equal(Object.keys(comments['file/1']).length, 6);
+        assert.equal(Object.keys(comments['file/1']).length, 4);
         assert.equal(Object.keys(comments['file/2']).length, 3);
         comments = changeComments.getAllComments(true, 2 as PatchSetNum);
         assert.equal(Object.keys(comments).length, 4);
-        assert.equal(Object.keys(comments['file/1']).length, 6);
+        assert.equal(Object.keys(comments['file/1']).length, 4);
         assert.equal(Object.keys(comments['file/2']).length, 1);
       });
 
@@ -1013,7 +986,6 @@
           {
             ...createCommentThread([
               {...comments[3], path: 'file/1'},
-              {...comments[1], path: 'file/1'},
               {...comments[12], path: 'file/1'},
             ]),
           },
@@ -1021,9 +993,6 @@
             ...createCommentThread([{...comments[11], path: 'file/1'}]),
           },
           {
-            ...createCommentThread([{...comments[0], path: 'file/1'}]),
-          },
-          {
             ...createCommentThread([{...comments[2], path: 'file/1'}]),
           },
           {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index aed0857..4aac5b1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -6,7 +6,11 @@
 import '../../shared/gr-comment-thread/gr-comment-thread';
 import '../../checks/gr-diff-check-result';
 import '../../../embed/diff/gr-diff/gr-diff';
-import {getDiffLength, isImageDiff} from '../../../utils/diff-util';
+import {
+  getDiffLength,
+  isImageDiff,
+  isLineUnchanged,
+} from '../../../utils/diff-util';
 import {getAppContext} from '../../../services/app-context';
 import {
   getParentIndex,
@@ -41,14 +45,14 @@
   WebLinkInfo,
 } from '../../../types/diff';
 import {GrDiff} from '../../../embed/diff/gr-diff/gr-diff';
-import {DiffViewMode, Side, CommentSide} from '../../../constants/constants';
+import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
 import {FilesWebLinks} from '../gr-patch-range-select/gr-patch-range-select';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {
-  firePageError,
-  fireAlert,
-  fireServerError,
   fire,
+  fireAlert,
+  firePageError,
+  fireServerError,
   waitForEventOnce,
 } from '../../../utils/event-util';
 import {assertIsDefined} from '../../../utils/common-util';
@@ -64,6 +68,7 @@
   LineNumber,
   LineSelectedEventDetail,
   LOST,
+  RangeSelectedEventDetail,
   RenderPreferences,
 } from '../../../api/diff';
 import {resolve} from '../../../models/dependency';
@@ -83,8 +88,8 @@
 import {ValueChangedEvent} from '../../../types/events';
 import {
   debounceP,
-  DelayedPromise,
   DELAYED_CANCELLATION,
+  DelayedPromise,
   noAwait,
 } from '../../../utils/async-util';
 import {subscribe} from '../../lit/subscription-controller';
@@ -96,6 +101,7 @@
 import {Shortcut} from '../../lit/shortcut-controller';
 import {shortcutsServiceToken} from '../../../services/shortcuts/shortcuts-service';
 import {toComment} from '../../../models/checks/checks-util';
+import {lineNumberToNumber} from '../../../embed/diff/gr-diff/gr-diff-utils';
 
 const EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -114,6 +120,7 @@
     // prettier-ignore
     'render': CustomEvent<{}>;
     'create-comment': CustomEvent<CreateCommentEventDetail>;
+    'range-selected': CustomEvent<RangeSelectedEventDetail>;
     'is-blame-loaded-changed': ValueChangedEvent<boolean>;
     'diff-changed': ValueChangedEvent<DiffInfo | undefined>;
     'edit-weblinks-changed': ValueChangedEvent<WebLinkInfo[] | undefined>;
@@ -730,6 +737,12 @@
     if (draft.range) {
       rangeAttr = `${JSON.stringify(draft.range)}`;
     }
+    if (
+      check.show_on_unchanged_lines === false &&
+      isLineUnchanged(this.diff, Side.RIGHT, lineNumberToNumber(line))
+    ) {
+      return;
+    }
 
     return html`
       <gr-diff-check-result
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index dbd7b8d..00a627b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -31,13 +31,13 @@
   stubRestApi,
 } from '../../../test/test-utils';
 import {
+  Base64ImageFile,
   BasePatchSetNum,
   BlameInfo,
   CommentRange,
   CommentThread,
   DraftInfo,
   EDIT,
-  ImageInfo,
   NumericChangeId,
   PARENT,
   PatchSetNum,
@@ -50,9 +50,9 @@
 import {SinonStub, SinonStubbedMember} from 'sinon';
 import {RunResult} from '../../../models/checks/checks-model';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
-import {userModelToken, UserModel} from '../../../models/user/user-model';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
@@ -238,8 +238,8 @@
   });
 
   suite('image diffs', () => {
-    let mockFile1: ImageInfo;
-    let mockFile2: ImageInfo;
+    let mockFile1: Base64ImageFile;
+    let mockFile2: Base64ImageFile;
     setup(() => {
       mockFile1 = {
         body:
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 0a3392c..7631ef0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -3,7 +3,6 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-a11y-announcer/iron-a11y-announcer';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
@@ -15,7 +14,7 @@
 import {css, html, LitElement} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {userModelToken} from '../../../models/user/user-model';
-import {ironAnnouncerRequestAvailability} from '../../polymer-util';
+import {grAnnouncerRequestAvailability} from '../../lit-util';
 import {subscribe} from '../../lit/subscription-controller';
 
 @customElement('gr-diff-mode-selector')
@@ -47,7 +46,8 @@
 
   override connectedCallback() {
     super.connectedCallback();
-    ironAnnouncerRequestAvailability();
+
+    grAnnouncerRequestAvailability();
   }
 
   static override get styles() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 43dee0a..d8a12bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -9,7 +9,7 @@
 import {GrDiffModeSelector} from './gr-diff-mode-selector';
 import {DiffViewMode} from '../../../constants/constants';
 import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {wrapInProvider} from '../../../models/di-provider-element';
 import {
   BrowserModel,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
index ed5f7ee..48e9f4c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.ts
@@ -8,7 +8,7 @@
 import {GrDiffPreferences} from '../../shared/gr-diff-preferences/gr-diff-preferences';
 import {assertIsDefined} from '../../../utils/common-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {ValueChangedEvent} from '../../../types/events';
 import {modalStyles} from '../../../styles/gr-modal-styles';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
index 739468b..bc8e26e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
@@ -15,7 +15,7 @@
 } from '../../../test/test-utils';
 import {DiffPreferencesInfo} from '../../../api/diff';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-diff-preferences-dialog', () => {
   let element: GrDiffPreferencesDialog;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 32cf367..efe4ab9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -3,8 +3,6 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-dropdown/iron-dropdown';
-import '@polymer/iron-input/iron-input';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../../styles/gr-a11y-styles';
@@ -13,7 +11,6 @@
 import '../../shared/gr-dropdown/gr-dropdown';
 import '../../shared/gr-dropdown-list/gr-dropdown-list';
 import '../../shared/gr-icon/gr-icon';
-import '../../shared/gr-select/gr-select';
 import '../../shared/gr-weblink/gr-weblink';
 import '../../shared/revision-info/revision-info';
 import '../gr-apply-fix-dialog/gr-apply-fix-dialog';
@@ -23,7 +20,7 @@
 import '../gr-patch-range-select/gr-patch-range-select';
 import '../../change/gr-download-dialog/gr-download-dialog';
 import {getAppContext} from '../../../services/app-context';
-import {isMergeParent, getParentIndex} from '../../../utils/patch-set-util';
+import {getParentIndex, isMergeParent} from '../../../utils/patch-set-util';
 import {
   computeDisplayPath,
   computeTruncatedPath,
@@ -39,6 +36,9 @@
 import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
 import {
   BasePatchSetNum,
+  Comment,
+  CommentMap,
+  DropdownLink,
   EDIT,
   NumericChangeId,
   PARENT,
@@ -47,9 +47,6 @@
   PreferencesInfo,
   RepoName,
   RevisionPatchSetNum,
-  Comment,
-  CommentMap,
-  DropdownLink,
 } from '../../../types/common';
 import {DiffInfo, DiffPreferencesInfo, WebLinkInfo} from '../../../types/diff';
 import {ParsedChangeInfo} from '../../../types/types';
@@ -61,12 +58,12 @@
 import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
 import {GrApplyFixDialog} from '../gr-apply-fix-dialog/gr-apply-fix-dialog';
 import {OpenFixPreviewEvent, ValueChangedEvent} from '../../../types/events';
-import {fireAlert, fire} from '../../../utils/event-util';
-import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
+import {fire, fireAlert} from '../../../utils/event-util';
+import {assertIsDefined} from '../../../utils/common-util';
 import {whenVisible} from '../../../utils/dom-util';
 import {CursorMoveResult} from '../../../api/core';
 import {throttleWrap} from '../../../utils/async-util';
-import {filter, take, switchMap, map} from 'rxjs/operators';
+import {filter, map, switchMap, take} from 'rxjs/operators';
 import {combineLatest} from 'rxjs';
 import {
   Shortcut,
@@ -97,7 +94,6 @@
 } from '../../../models/views/change';
 import {userModelToken} from '../../../models/user/user-model';
 import {modalStyles} from '../../../styles/gr-modal-styles';
-import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
 import {GrDiffPreferencesDialog} from '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
 import {
   FileNameToNormalizedFileInfoMap,
@@ -108,6 +104,9 @@
 import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
 import {configModelToken} from '../../../models/config/config-model';
 import {trimWithEllipsis} from '../../../utils/string-util';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const LOADING_BLAME = 'Loading blame information. This may take a while ...';
 const LOADED_BLAME = 'Blame loaded';
@@ -521,6 +520,7 @@
       a11yStyles,
       sharedStyles,
       modalStyles,
+      materialStyles,
       css`
         :host {
           display: block;
@@ -562,6 +562,8 @@
         .headerSubject {
           margin-right: var(--spacing-m);
           font-weight: var(--font-weight-medium);
+          whitespace: no-wrap;
+          overflow: auto;
         }
         .patchRangeLeft {
           align-items: center;
@@ -668,9 +670,6 @@
             width: 100%;
             word-wrap: break-word;
           }
-          .reviewed {
-            vertical-align: -1px;
-          }
           .mobileNavLink {
             color: var(--primary-text-color);
             font-family: var(--header-font-family);
@@ -682,18 +681,10 @@
           .mobileNavLink:not([href]) {
             color: var(--deemphasized-text-color);
           }
-          .jumpToFileContainer {
-            display: block;
-            width: 100%;
-            word-break: break-all;
-          }
           /* prettier formatter removes semi-colons after css mixins. */
           /* prettier-ignore */
           gr-dropdown-list {
             width: 100%;
-            --gr-select-style-width: 100%;
-            --gr-select-style-display: block;
-            --native-select-style-width: 100%;
           }
         }
         :host(.hideComments) {
@@ -721,6 +712,10 @@
           border-left: 0;
           overflow: auto;
         }
+        md-checkbox {
+          --md-checkbox-container-size: 15px;
+          --md-checkbox-icon-size: 15px;
+        }
       `,
     ];
   }
@@ -899,20 +894,19 @@
         <span class="headerSubject"
           >${trimWithEllipsis(this.change?.subject, 80)}</span
         >
-        <input
+        <md-checkbox
           id="reviewed"
           class="reviewed hideOnEdit"
-          type="checkbox"
           ?hidden=${!this.loggedIn}
           title="Toggle reviewed status of file"
           aria-label="file reviewed"
-          .checked=${this.reviewed}
+          ?checked=${this.reviewed}
           @change=${this.handleReviewedChange}
-        />
+        ></md-checkbox>
         <div class="jumpToFileContainer">
           <gr-dropdown-list
             id="dropdown"
-            .value=${this.path}
+            .value=${this.path ?? ''}
             .items=${formattedFiles}
             show-copy-for-trigger-text
             @value-change=${this.handleFileChange}
@@ -1141,15 +1135,13 @@
       )}
       <gr-endpoint-decorator name="annotation-toggler">
         <span hidden="" id="annotation-span">
-          <label for="annotation-checkbox" id="annotation-label"></label>
-          <iron-input>
-            <input
-              is="iron-input"
-              type="checkbox"
-              id="annotation-checkbox"
-              disabled=""
-            />
-          </iron-input>
+          <label id="annotation-label" for="annotation-checkbox"></label>
+          <md-checkbox
+            id="annotation-checkbox"
+            touch-target="none"
+            disabled
+            aria-labelledby="annotation-label"
+          ></md-checkbox>
         </span>
       </gr-endpoint-decorator>
     </div>`;
@@ -1238,7 +1230,7 @@
   }
 
   private handleReviewedChange(e: Event) {
-    const input = e.target as HTMLInputElement;
+    const input = e.target as MdCheckbox;
     this.setReviewed(input.checked ?? false);
   }
 
@@ -1418,19 +1410,6 @@
       assertIsDefined(this.downloadModal, 'downloadModal');
       assertIsDefined(this.downloadDialog, 'downloadDialog');
       this.downloadDialog.focus();
-      const downloadCommands = queryAndAssert(
-        this.downloadDialog,
-        'gr-download-commands'
-      );
-      const paperTabs = queryAndAssert<PaperTabsElement>(
-        downloadCommands,
-        'paper-tabs'
-      );
-      // Paper Tabs normally listen to 'iron-resize' event to call this method.
-      // After migrating to Dialog element, this event is no longer fired
-      // which means this method is not called which ends up styling the
-      // selected paper tab with an underline.
-      paperTabs._onTabSizingChanged();
     });
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 4c17d40..8b65edd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -8,9 +8,9 @@
 import './gr-diff-view';
 import {
   ChangeStatus,
-  DiffViewMode,
   createDefaultDiffPrefs,
   createDefaultPreferences,
+  DiffViewMode,
 } from '../../../constants/constants';
 import {
   isVisible,
@@ -24,18 +24,18 @@
 } from '../../../test/test-utils';
 import {ChangeComments} from '../gr-comment-api/gr-comment-api';
 import {
-  createRevisions,
   createComment as createCommentGeneric,
-  createDiff,
-  createServerInfo,
   createConfig,
-  createParsedChange,
-  createRevision,
-  createFileInfo,
+  createDiff,
   createDiffViewState,
-  TEST_NUMERIC_CHANGE_ID,
-  createRunResult,
+  createFileInfo,
+  createParsedChange,
   createRange,
+  createRevision,
+  createRevisions,
+  createRunResult,
+  createServerInfo,
+  TEST_NUMERIC_CHANGE_ID,
 } from '../../../test/test-data-generators';
 import {
   BasePatchSetNum,
@@ -55,18 +55,18 @@
 import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 import {SinonFakeTimers, SinonStub, SinonStubbedMember} from 'sinon';
 import {
-  changeModelToken,
   ChangeModel,
+  changeModelToken,
 } from '../../../models/change/change-model';
 import {assertIsDefined} from '../../../utils/common-util';
 import {GrDiffModeSelector} from '../gr-diff-mode-selector/gr-diff-mode-selector';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {testResolver} from '../../../test/common-test-setup';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {
-  commentsModelToken,
   CommentsModel,
+  commentsModelToken,
 } from '../../../models/comments/comments-model';
 import {
   BrowserModel,
@@ -76,6 +76,7 @@
   ChangeViewModel,
   changeViewModelToken,
 } from '../../../models/views/change';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
 import {FileNameToNormalizedFileInfoMap} from '../../../models/change/files-model';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
@@ -137,7 +138,6 @@
       stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response()));
       diffCommentsStub = stubRestApi('getDiffComments');
       diffCommentsStub.returns(Promise.resolve({}));
-      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       stubRestApi('getPortedComments').returns(Promise.resolve({}));
 
@@ -170,7 +170,6 @@
 
       commentsModel.setState({
         comments: {},
-        robotComments: {},
         drafts: {},
         portedComments: {},
         portedDrafts: {},
@@ -222,13 +221,12 @@
                 <a href="/c/test-project/+/42"> 42 </a>
                 <span class="changeNumberColon"> : </span>
                 <span class="headerSubject"> Test subject </span>
-                <input
+                <md-checkbox
                   aria-label="file reviewed"
                   class="hideOnEdit reviewed"
                   id="reviewed"
                   title="Toggle reviewed status of file"
-                  type="checkbox"
-                />
+                ></md-checkbox>
                 <div class="jumpToFileContainer">
                   <gr-dropdown-list id="dropdown" show-copy-for-trigger-text="">
                   </gr-dropdown-list>
@@ -346,15 +344,13 @@
                   <span hidden="" id="annotation-span">
                     <label for="annotation-checkbox" id="annotation-label">
                     </label>
-                    <iron-input>
-                      <input
-                        disabled=""
-                        id="annotation-checkbox"
-                        is="iron-input"
-                        type="checkbox"
-                        value=""
-                      />
-                    </iron-input>
+                    <md-checkbox
+                      aria-labelledby="annotation-label"
+                      disabled=""
+                      id="annotation-checkbox"
+                      touch-target="none"
+                    >
+                    </md-checkbox>
                   </span>
                 </gr-endpoint-decorator>
               </div>
@@ -1478,9 +1474,9 @@
       changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
       await element.updateComplete;
 
-      const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
+      const reviewedStatusCheckBox = queryAndAssert<MdCheckbox>(
         element,
-        'input#reviewed'
+        'md-checkbox#reviewed'
       );
 
       assert.isTrue(reviewedStatusCheckBox.checked);
@@ -1638,14 +1634,14 @@
       element.leftSide = false;
       let result = element.getLineOfInterest();
       assert.isOk(result);
-      assert.equal(result!.lineNum, 12);
-      assert.equal(result!.side, Side.RIGHT);
+      assert.equal(result.lineNum, 12);
+      assert.equal(result.side, Side.RIGHT);
 
       element.leftSide = true;
       result = element.getLineOfInterest();
       assert.isOk(result);
-      assert.equal(result!.lineNum, 12);
-      assert.equal(result!.side, Side.LEFT);
+      assert.equal(result.lineNum, 12);
+      assert.equal(result.side, Side.LEFT);
     });
 
     test('onLineSelected', () => {
@@ -1852,10 +1848,10 @@
     test('f open file dropdown', async () => {
       assertIsDefined(element.dropdown);
       assertIsDefined(element.dropdown.dropdown);
-      assert.isFalse(element.dropdown.dropdown.opened);
+      assert.isFalse(element.dropdown.dropdown.open);
       pressKey(element, 'f');
       await element.updateComplete;
-      assert.isTrue(element.dropdown.dropdown.opened);
+      assert.isTrue(element.dropdown.dropdown.open);
     });
 
     suite('blame', () => {
@@ -1889,7 +1885,7 @@
         element.basePatchNum = PARENT;
         await element.updateComplete;
 
-        let checkbox = queryAndAssert(element, '#reviewed');
+        let checkbox = queryAndAssert(element, 'md-checkbox#reviewed');
         assert.isTrue(isVisible(checkbox));
 
         element.patchNum = EDIT;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 5c7ab03..6f59c01 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -4,26 +4,28 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../shared/gr-dropdown-list/gr-dropdown-list';
-import '../../shared/gr-select/gr-select';
 import '../../shared/gr-weblink/gr-weblink';
 import {convertToString, pluralize} from '../../../utils/string-util';
 import {getAppContext} from '../../../services/app-context';
 import {
   computeLatestPatchNum,
+  convertToPatchSetNum,
   findSortedIndex,
+  getParentCommit,
   getParentIndex,
+  getParentInfoString,
   getRevisionByPatchNum,
   isMergeParent,
   PatchSet,
-  convertToPatchSetNum,
-  getParentInfoString,
   shorten,
-  getParentCommit,
 } from '../../../utils/patch-set-util';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {
+  AccountInfo,
+  ApprovalInfo,
   BasePatchSetNum,
   EDIT,
+  LabelInfo,
   NumericChangeId,
   PARENT,
   PatchSetNum,
@@ -38,19 +40,29 @@
   DropdownItem,
   GrDropdownList,
 } from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {EditRevisionInfo} from '../../../types/types';
+import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
 import {a11yStyles} from '../../../styles/gr-a11y-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {resolve} from '../../../models/dependency';
 import {ValueChangedEvent} from '../../../types/events';
-import {changeModelToken} from '../../../models/change/change-model';
+import {
+  changeModelToken,
+  RevisionFileUpdateStatus,
+  RevisionUpdatedFiles,
+} from '../../../models/change/change-model';
 import {changeViewModelToken} from '../../../models/views/change';
 import {fireNoBubbleNoCompose} from '../../../utils/event-util';
-import {FlagsService, KnownExperimentId} from '../../../services/flags/flags';
+import {userModelToken} from '../../../models/user/user-model';
+import {getCodeReviewLabel} from '../../../utils/label-util';
+import {
+  getCodeReviewVotesFromMessage,
+  Score,
+} from '../../../utils/message-util';
+import {combineLatest} from 'rxjs';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -98,6 +110,12 @@
   @state()
   changeNum?: NumericChangeId;
 
+  @state()
+  change?: ParsedChangeInfo;
+
+  @state()
+  selfAccount?: AccountInfo;
+
   @property()
   path?: string;
 
@@ -119,10 +137,16 @@
   @state()
   changeComments?: ChangeComments;
 
+  @state()
+  revisionUpdatedFiles?: RevisionUpdatedFiles;
+
+  @state()
+  codeReviewVotes: Map<PatchSetNum, Score> = new Map();
+
   private readonly reporting: ReportingService =
     getAppContext().reportingService;
 
-  private readonly flags: FlagsService = getAppContext().flagsService;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getCommentsModel = resolve(this, commentsModelToken);
 
@@ -140,6 +164,17 @@
     subscribe(
       this,
       () => this.getChangeModel().change$,
+      x => (this.change = x)
+    );
+    subscribe(
+      this,
+      () => this.getUserModel().account$,
+      x => (this.selfAccount = x)
+    );
+
+    subscribe(
+      this,
+      () => this.getChangeModel().change$,
       x => (this.revisionInfo = x ? new RevisionInfoClass(x) : undefined)
     );
     subscribe(
@@ -167,6 +202,22 @@
       () => this.getCommentsModel().changeComments$,
       x => (this.changeComments = x)
     );
+    subscribe(
+      this,
+      () => this.getChangeModel().revisionUpdatedFiles$,
+      x => (this.revisionUpdatedFiles = x)
+    );
+    subscribe(
+      this,
+      () =>
+        combineLatest([
+          this.getChangeModel().change$,
+          this.getUserModel().account$,
+        ]),
+      ([change, account]) => {
+        this.codeReviewVotes = getCodeReviewVotesFromMessage(change, account);
+      }
+    );
   }
 
   static override get styles() {
@@ -262,28 +313,18 @@
     const parentCount = this.revisionInfo.getParentCount(this.patchNum);
     const rev = getRevisionByPatchNum(this.sortedRevisions, this.patchNum);
 
-    const dropdownContent: DropdownItem[] = [];
-    for (const basePatch of this.availablePatches) {
-      const basePatchNum = basePatch.num;
-      const entry: DropdownItem = this.createDropdownEntry(
-        basePatchNum,
-        'Patchset ',
-        shorten(basePatch.sha)!
+    const dropdownContent: DropdownItem[] = this.availablePatches
+      .filter(basePatch =>
+        this.isValidLeftPatchNum(basePatch.num, this.patchNum!)
+      )
+      .map(basePatch =>
+        this.createDropdownEntry(basePatch.num, 'Patchset ', basePatch.sha)
       );
-      dropdownContent.push({
-        ...entry,
-        disabled: this.computeLeftDisabled(basePatch.num, this.patchNum),
-      });
-    }
 
-    const showParentsData = this.flags.isEnabled(
-      KnownExperimentId.REVISION_PARENTS_DATA
-    );
     dropdownContent.push({
       triggerText: isMerge ? 'Auto Merge' : 'Base',
       text: isMerge ? 'Auto Merge' : `Base | ${getParentCommit(rev, 0)}`,
-      bottomText:
-        showParentsData && !isMerge ? getParentInfoString(rev, 0) : undefined,
+      bottomText: !isMerge ? getParentInfoString(rev, 0) : undefined,
       value: PARENT,
     });
 
@@ -292,7 +333,7 @@
         disabled: idx >= parentCount,
         triggerText: `Parent ${idx + 1}`,
         text: `Parent ${idx + 1} | ${getParentCommit(rev, idx)}`,
-        bottomText: showParentsData ? getParentInfoString(rev, idx) : undefined,
+        bottomText: getParentInfoString(rev, idx),
         mobileText: `Parent ${idx + 1}`,
         value: -(idx + 1),
       });
@@ -319,20 +360,15 @@
       return [];
     }
 
-    const dropdownContent: DropdownItem[] = [];
-    for (const patch of this.availablePatches) {
-      const patchNum = patch.num;
-      const entry = this.createDropdownEntry(
-        patchNum,
-        patchNum === EDIT ? '' : 'Patchset ',
-        shorten(patch.sha)!
+    return this.availablePatches
+      .filter(p => this.isValidRightPatchNum(this.basePatchNum!, p.num))
+      .map(patch =>
+        this.createDropdownEntry(
+          patch.num,
+          patch.num === EDIT ? '' : 'Patchset ',
+          patch.sha
+        )
       );
-      dropdownContent.push({
-        ...entry,
-        disabled: this.computeRightDisabled(this.basePatchNum, patchNum),
-      });
-    }
-    return dropdownContent;
   }
 
   private computeText(patchNum: PatchSetNum, prefix: string, sha: string) {
@@ -343,7 +379,7 @@
   createDropdownEntry(patchNum: PatchSetNum, prefix: string, sha: string) {
     const entry: DropdownItem = {
       triggerText: `${prefix}${patchNum}`,
-      text: this.computeText(patchNum, prefix, sha),
+      text: this.computeText(patchNum, prefix, shorten(sha)!),
       mobileText: this.computeMobileText(patchNum),
       bottomText: `${this.computePatchSetDescription(patchNum)}`,
       value: patchNum,
@@ -355,6 +391,9 @@
         // don't ignore patchset level comments if the path is not set
         !!this.path /* ignorePatchsetLevelComments*/
       ),
+      deemphasizeReason: this.computeDeemphasizeReason(sha),
+      vote: this.computeVote(this.selfAccount, this.change, patchNum),
+      label: this.computeCodeReviewLabel(this.change),
     };
     const date = this.computePatchSetDate(patchNum);
     if (date) {
@@ -363,6 +402,43 @@
     return entry;
   }
 
+  private computeDeemphasizeReason(sha: string) {
+    if (!this.path || !this.revisionUpdatedFiles) {
+      return undefined;
+    }
+
+    return this.revisionUpdatedFiles[sha]?.[this.path] ===
+      RevisionFileUpdateStatus.SAME
+      ? 'Unmodified'
+      : undefined;
+  }
+
+  private computeVote(
+    reviewer?: AccountInfo,
+    change?: ParsedChangeInfo,
+    revisionNum?: PatchSetNum
+  ): ApprovalInfo | undefined {
+    if (!change || !reviewer || !revisionNum) return undefined;
+
+    const vote = this.codeReviewVotes.get(revisionNum);
+
+    if (vote) {
+      return {
+        ...reviewer,
+        value: Number(vote.value),
+      };
+    }
+
+    return undefined;
+  }
+
+  private computeCodeReviewLabel(
+    change?: ParsedChangeInfo
+  ): LabelInfo | undefined {
+    if (!change?.labels) return;
+    return getCodeReviewLabel(change.labels);
+  }
+
   /**
    * The basePatchNum should always be <= patchNum -- because sortedRevisions
    * is sorted in reverse order (higher patchset nums first), invalid base
@@ -373,12 +449,12 @@
    * @param basePatchNum The possible base patch num.
    * @param patchNum The current selected patch num.
    */
-  computeLeftDisabled(
+  isValidLeftPatchNum(
     basePatchNum: PatchSetNum,
     patchNum: PatchSetNum
   ): boolean {
     return (
-      findSortedIndex(basePatchNum, this.sortedRevisions) <=
+      findSortedIndex(basePatchNum, this.sortedRevisions) >
       findSortedIndex(patchNum, this.sortedRevisions)
     );
   }
@@ -399,27 +475,27 @@
    * @param basePatchNum The current selected base patch num.
    * @param patchNum The possible patch num.
    */
-  computeRightDisabled(
+  isValidRightPatchNum(
     basePatchNum: BasePatchSetNum,
     patchNum: RevisionPatchSetNum
   ): boolean {
     if (basePatchNum === PARENT) {
-      return false;
+      return true;
     }
 
     if (isMergeParent(basePatchNum)) {
       if (!this.revisionInfo) {
-        return true;
+        return false;
       }
       // Note: parent indices use 1-offset.
       return (
-        this.revisionInfo.getParentCount(patchNum) <
+        this.revisionInfo.getParentCount(patchNum) >=
         getParentIndex(basePatchNum)
       );
     }
 
     return (
-      findSortedIndex(basePatchNum, this.sortedRevisions) <=
+      findSortedIndex(basePatchNum, this.sortedRevisions) >
       findSortedIndex(patchNum, this.sortedRevisions)
     );
   }
@@ -467,6 +543,7 @@
     addFrontSpace?: boolean
   ) {
     const rev = getRevisionByPatchNum(this.sortedRevisions, patchNum);
+
     return rev?.description
       ? (addFrontSpace ? ' ' : '') +
           rev.description.substring(0, PATCH_DESC_MAX_LENGTH)
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index 1400882..c19e8a7 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -12,20 +12,23 @@
 import {ChangeComments} from '../gr-comment-api/gr-comment-api';
 import {queryAll, stubReporting} from '../../../test/test-utils';
 import {
+  AccountId,
   BasePatchSetNum,
+  ChangeMessageId,
+  CommentInfo,
   EDIT,
-  RevisionPatchSetNum,
   PARENT,
   PatchSetNum,
   PatchSetNumber,
   RevisionInfo,
+  RevisionPatchSetNum,
   Timestamp,
   UrlEncodedCommentId,
-  CommentInfo,
 } from '../../../types/common';
 import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
 import {SpecialFilePath} from '../../../constants/constants';
 import {
+  createAccountDetailWithId,
   createChangeViewState,
   createEditRevision,
   createParsedChange,
@@ -39,10 +42,14 @@
 } from '../../shared/gr-dropdown-list/gr-dropdown-list';
 import {queryAndAssert} from '../../../test/test-utils';
 import {fire} from '../../../utils/event-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 import {changeViewModelToken} from '../../../models/views/change';
-import {changeModelToken} from '../../../models/change/change-model';
+import {
+  changeModelToken,
+  RevisionFileUpdateStatus,
+} from '../../../models/change/change-model';
+import {userModelToken} from '../../../models/user/user-model';
 
 type RevIdToRevisionInfo = {
   [revisionId: string]: RevisionInfo | EditRevisionInfo;
@@ -99,7 +106,7 @@
     );
   });
 
-  test('enabled/disabled options', async () => {
+  test('valid/invalid options', async () => {
     element.sortedRevisions = [
       createRevision(3),
       createEditRevision(2),
@@ -109,20 +116,16 @@
     await element.updateComplete;
 
     for (const patchNum of [1, 2, 3]) {
-      assert.isFalse(
-        element.computeRightDisabled(PARENT, patchNum as PatchSetNumber)
+      assert.isTrue(
+        element.isValidRightPatchNum(PARENT, patchNum as PatchSetNumber)
       );
     }
     for (const basePatchNum of [1, 2]) {
       const base = basePatchNum as PatchSetNum;
-      assert.isFalse(element.computeLeftDisabled(base, 3 as PatchSetNum));
+      assert.isTrue(element.isValidLeftPatchNum(base, 3 as PatchSetNum));
     }
-    assert.isTrue(
-      element.computeLeftDisabled(3 as PatchSetNum, 3 as PatchSetNum)
-    );
-
-    assert.isTrue(
-      element.computeLeftDisabled(3 as PatchSetNum, 3 as PatchSetNum)
+    assert.isFalse(
+      element.isValidLeftPatchNum(3 as PatchSetNum, 3 as PatchSetNum)
     );
   });
 
@@ -142,36 +145,6 @@
     element.revisionInfo = getInfo(element.sortedRevisions);
     const expectedResult: DropdownItem[] = [
       {
-        disabled: true,
-        triggerText: 'Patchset edit',
-        text: 'Patchset edit | 1',
-        mobileText: EDIT,
-        bottomText: '',
-        value: EDIT,
-        commentThreads: [],
-      },
-      {
-        disabled: true,
-        triggerText: 'Patchset 3',
-        text: 'Patchset 3 | 2',
-        mobileText: '3',
-        bottomText: '',
-        value: 3,
-        date: '2020-02-01 01:02:03.000000000' as Timestamp,
-        commentThreads: [],
-      } as DropdownItem,
-      {
-        disabled: true,
-        triggerText: 'Patchset 2',
-        text: 'Patchset 2 | 3',
-        mobileText: '2',
-        bottomText: '',
-        value: 2,
-        date: '2020-02-01 01:02:03.000000000' as Timestamp,
-        commentThreads: [],
-      } as DropdownItem,
-      {
-        disabled: true,
         triggerText: 'Patchset 1',
         text: 'Patchset 1 | 4',
         mobileText: '1',
@@ -179,15 +152,18 @@
         value: 1,
         date: '2020-02-01 01:02:03.000000000' as Timestamp,
         commentThreads: [],
+        deemphasizeReason: undefined,
+        label: undefined,
+        vote: undefined,
       } as DropdownItem,
       {
-        text: 'Base | ',
         triggerText: 'Base',
         value: PARENT,
-        bottomText: undefined,
+        bottomText: '',
+        text: 'Base | ',
       } as DropdownItem,
     ];
-    element.patchNum = 1 as PatchSetNumber;
+    element.patchNum = 2 as PatchSetNumber;
     element.basePatchNum = PARENT;
     await element.updateComplete;
 
@@ -288,16 +264,17 @@
 
     const expectedResult: DropdownItem[] = [
       {
-        disabled: false,
         triggerText: EDIT,
         text: 'edit | 1',
         mobileText: EDIT,
         bottomText: '',
         value: EDIT,
         commentThreads: [],
+        deemphasizeReason: undefined,
+        vote: undefined,
+        label: undefined,
       },
       {
-        disabled: false,
         triggerText: 'Patchset 3',
         text: 'Patchset 3 | 2',
         mobileText: '3',
@@ -305,9 +282,11 @@
         value: 3,
         date: '2020-02-01 01:02:03.000000000' as Timestamp,
         commentThreads: [],
+        deemphasizeReason: undefined,
+        vote: undefined,
+        label: undefined,
       } as DropdownItem,
       {
-        disabled: false,
         triggerText: 'Patchset 2',
         text: 'Patchset 2 | 3',
         mobileText: '2 description',
@@ -315,16 +294,9 @@
         value: 2,
         date: '2020-02-01 01:02:03.000000000' as Timestamp,
         commentThreads: [],
-      } as DropdownItem,
-      {
-        disabled: true,
-        triggerText: 'Patchset 1',
-        text: 'Patchset 1 | 4',
-        mobileText: '1',
-        bottomText: '',
-        value: 1,
-        date: '2020-02-01 01:02:03.000000000' as Timestamp,
-        commentThreads: [],
+        deemphasizeReason: undefined,
+        vote: undefined,
+        label: undefined,
       } as DropdownItem,
     ];
 
@@ -530,4 +502,153 @@
       'Should ignore patchset level comments when path is defined'
     );
   });
+
+  test('revisions without modification are deemphasized', async () => {
+    element.availablePatches = [
+      {num: 4, sha: 'sha4'} as PatchSet,
+      {num: 3, sha: 'sha3'} as PatchSet,
+      {num: 2, sha: 'sha2'} as PatchSet,
+      {num: 1, sha: 'sha1'} as PatchSet,
+    ];
+    element.sortedRevisions = [
+      createRevision(4),
+      createRevision(3),
+      createRevision(2),
+      createRevision(1),
+    ];
+    element.revisionUpdatedFiles = {
+      sha1: {
+        foo: RevisionFileUpdateStatus.MODIFIED,
+        bar: RevisionFileUpdateStatus.MODIFIED,
+      },
+      sha2: {
+        foo: RevisionFileUpdateStatus.SAME,
+        bar: RevisionFileUpdateStatus.MODIFIED,
+      },
+      sha3: {
+        foo: RevisionFileUpdateStatus.UNKNOWN,
+        bar: RevisionFileUpdateStatus.SAME,
+      },
+      sha4: {
+        foo: RevisionFileUpdateStatus.SAME,
+        bar: RevisionFileUpdateStatus.SAME,
+      },
+    };
+    element.path = 'foo';
+    element.revisionInfo = getInfo(element.sortedRevisions);
+    element.patchNum = 4 as PatchSetNumber;
+    element.basePatchNum = PARENT;
+    await element.updateComplete;
+
+    const expectedResult: {triggerText: string; deemphasizeReason?: string}[] =
+      [
+        {
+          triggerText: 'Patchset 3',
+          deemphasizeReason: undefined,
+        },
+        {
+          triggerText: 'Patchset 2',
+          deemphasizeReason: 'Unmodified',
+        },
+        {
+          triggerText: 'Patchset 1',
+          deemphasizeReason: undefined,
+        },
+        {
+          triggerText: 'Base',
+          deemphasizeReason: undefined,
+        },
+      ];
+    assert.deepEqual(
+      element.computeBaseDropdownContent().map(
+        x =>
+          ({
+            triggerText: x.triggerText,
+            deemphasizeReason: x.deemphasizeReason,
+          } as {triggerText: string; deemphasizeReason?: string})
+      ),
+      expectedResult
+    );
+  });
+});
+
+suite('gr-patch-range-select with votes', () => {
+  let element: GrPatchRangeSelect;
+
+  setup(async () => {
+    const changeModel = testResolver(changeModelToken);
+    const userModel = testResolver(userModelToken);
+
+    const viewModel = testResolver(changeViewModelToken);
+    viewModel.setState({
+      ...createChangeViewState(),
+      patchNum: 2 as RevisionPatchSetNum,
+      basePatchNum: PARENT,
+    });
+
+    const account = createAccountDetailWithId(1);
+    userModel.setAccount(account);
+
+    const change: ParsedChangeInfo = {
+      ...createParsedChange(),
+      messages: [
+        {
+          id: '1' as ChangeMessageId,
+          author: {_account_id: 1 as AccountId},
+          date: '2020-01-01 10:00:00' as Timestamp,
+          message: 'Patch Set 1: Code-Review+1',
+          _revision_number: 1 as PatchSetNumber,
+        },
+        {
+          id: '2' as ChangeMessageId,
+          author: {_account_id: 1 as AccountId},
+          date: '2020-01-01 11:00:00' as Timestamp,
+          message: 'Patch Set 2: Code-Review-1',
+          _revision_number: 2 as PatchSetNumber,
+        },
+      ],
+      revisions: {
+        sha1: createRevision(1),
+        sha2: createRevision(2),
+      },
+      labels: {
+        'Code-Review': {
+          values: {
+            '-1': 'No',
+            ' 0': 'No score',
+            '+1': 'Yes',
+          },
+        },
+      },
+    };
+
+    changeModel.updateStateChange(change);
+
+    element = await fixture(
+      html`<gr-patch-range-select></gr-patch-range-select>`
+    );
+    await element.updateComplete;
+
+    // Unclear why but it's required twice
+    changeModel.updateStateChange(change);
+    await element.updateComplete;
+  });
+
+  test('shows votes in dropdown', async () => {
+    const dropdown = queryAndAssert<GrDropdownList>(
+      element,
+      '#patchNumDropdown'
+    );
+    await dropdown.updateComplete;
+
+    dropdown.open();
+    await dropdown.updateComplete;
+
+    const menu = dropdown.shadowRoot?.querySelector('md-menu');
+    assert.isDefined(menu);
+
+    const voteChip = menu!.querySelector('gr-vote-chip');
+    assert.isDefined(voteChip);
+    assert.equal(voteChip!.vote?.value, -1);
+  });
 });
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index 87fe27f..a45d227 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -10,7 +10,7 @@
 import {getAppContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {LitElement, html} from 'lit';
+import {html, LitElement} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
index c92f5bb..9ec7785 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
@@ -9,7 +9,7 @@
 import {GrDocumentationSearch} from './gr-documentation-search';
 import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
 import {DocResult} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
index 25d3cf4..de415b6 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {ValueChangedEvent} from '../../../types/events';
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts
index 39b70ec..620483f 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts
@@ -11,7 +11,7 @@
   queryAndAssert,
   waitEventLoop,
 } from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-default-editor tests', () => {
   let element: GrDefaultEditor;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index 405df80..72a41fe 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -3,32 +3,34 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-dialog/gr-dialog';
-import '../../shared/gr-dropdown/gr-dropdown';
 import {GrEditAction, GrEditConstants} from '../gr-edit-constants';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
-import {ChangeInfo, RevisionPatchSetNum} from '../../../types/common';
+import {
+  ChangeInfo,
+  PARENT,
+  PatchSetNum,
+  RevisionPatchSetNum,
+} from '../../../types/common';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {
   AutocompleteQuery,
-  AutocompleteSuggestion,
   GrAutocomplete,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {getAppContext} from '../../../services/app-context';
 import {fireAlert} from '../../../utils/event-util';
 import {
   assertIsDefined,
-  query as queryUtil,
   queryAll,
+  query as queryUtil,
 } from '../../../utils/common-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {BindValueChangeEvent} from '../../../types/events';
-import {IronInputElement} from '@polymer/iron-input/iron-input';
 import {changeViewModelToken} from '../../../models/views/change';
 import {resolve} from '../../../models/dependency';
 import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -38,13 +40,15 @@
 import {formStyles} from '../../../styles/form-styles';
 import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {formatBytes} from '../../../utils/file-util';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const FILE_UPLOAD_FAILURE = 'File failed to upload.';
 
 @customElement('gr-edit-controls')
 export class GrEditControls extends LitElement {
   // private but used in test
-  @query('#newPathIronInput') newPathIronInput?: IronInputElement;
+  @query('#newPathInput') newPathInput?: MdOutlinedTextField;
 
   @query('#modal') modal?: HTMLDialogElement;
 
@@ -69,7 +73,7 @@
   change?: ChangeInfo;
 
   @property({type: String})
-  patchNum?: RevisionPatchSetNum;
+  patchNum?: PatchSetNum;
 
   @property({type: Array})
   hiddenActions: string[] = [GrEditConstants.Actions.RESTORE.id];
@@ -102,6 +106,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       formStyles,
       sharedStyles,
       modalStyles,
@@ -125,7 +130,7 @@
         gr-dialog .main {
           width: 100%;
         }
-        gr-dialog .main > iron-input {
+        gr-dialog .main > md-outlined-text-field {
           width: 100%;
         }
         input {
@@ -134,7 +139,7 @@
           margin: var(--spacing-m) 0;
           padding: var(--spacing-s);
           width: 100%;
-          box-sizing: content-box;
+          box-sizing: border-box;
         }
         #dragDropArea {
           border: 2px dashed var(--border-color);
@@ -165,6 +170,9 @@
           pointer-events: none;
           opacity: 0.6;
         }
+        md-outlined-text-field {
+          margin: var(--spacing-m) 0;
+        }
         @media screen and (max-width: 50em) {
           gr-dialog {
             height: 90vh;
@@ -190,7 +198,7 @@
         id=${action.id}
         class=${this.computeIsInvisible(action.id)}
         link=""
-        @click=${this.handleTap}
+        @click=${this.handleClick}
         >${action.label}</gr-button
       >
     `;
@@ -201,8 +209,8 @@
       <gr-dialog
         id="openDialog"
         class="invisible dialog"
-        ?disabled=${!this.isValidPath(this.path) || this.fileUploaded}
-        ?disableCancel=${this.fileUploaded}
+        ?disabled=${!this.isValidPath(this.path) || !!this.fileUploaded}
+        ?disableCancel=${!!this.fileUploaded}
         confirm-label="Confirm"
         confirm-on-enter=""
         @confirm=${(e: Event) => this.handleOpenConfirm(e)}
@@ -216,6 +224,7 @@
             placeholder="Enter an existing or new full file path."
             .query=${this.query}
             .text=${this.path}
+            .showBlueFocusBorder=${true}
             @text-changed=${(e: BindValueChangeEvent) =>
               this.handleTextChanged(e)}
           ></gr-autocomplete>
@@ -284,6 +293,7 @@
             placeholder="Enter an existing full file path."
             .query=${this.query}
             .text=${this.path}
+            .showBlueFocusBorder=${true}
             @text-changed=${(e: BindValueChangeEvent) =>
               this.handleTextChanged(e)}
           ></gr-autocomplete>
@@ -310,17 +320,21 @@
             placeholder="Enter an existing full file path."
             .query=${this.query}
             .text=${this.path}
+            .showBlueFocusBorder=${true}
             @text-changed=${(e: BindValueChangeEvent) =>
               this.handleTextChanged(e)}
           ></gr-autocomplete>
-          <iron-input
-            id="newPathIronInput"
-            .bindValue=${this.newPath}
-            @bind-value-changed=${(e: BindValueChangeEvent) =>
-              this.handleBindValueChangedNewPath(e)}
+          <md-outlined-text-field
+            id="newPathInput"
+            class="showBlueFocusBorder"
+            placeholder="Enter the new path."
+            .value=${this.newPath ?? ''}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
+              this.newPath = target.value;
+            }}
           >
-            <input id="newPathInput" placeholder="Enter the new path." />
-          </iron-input>
+          </md-outlined-text-field>
         </div>
       </gr-dialog>
     `;
@@ -338,19 +352,22 @@
       >
         <div class="header" slot="header">Restore this file?</div>
         <div class="main" slot="main">
-          <iron-input
-            .bindValue=${this.path}
-            @bind-value-changed=${(e: BindValueChangeEvent) =>
-              this.handleBindValueChangedPath(e)}
+          <md-outlined-text-field
+            class="showBlueFocusBorder"
+            ?readOnly=${true}
+            .value=${this.path ?? ''}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
+              this.path = target.value;
+            }}
           >
-            <input ?disabled=${''} />
-          </iron-input>
+          </md-outlined-text-field>
         </div>
       </gr-dialog>
     `;
   }
 
-  private readonly handleTap = (e: Event) => {
+  private readonly handleClick = (e: Event) => {
     e.preventDefault();
     const target = e.target as Element;
     const action = target.id;
@@ -469,8 +486,9 @@
         input.text = '';
       });
 
-      dialog.querySelectorAll('iron-input').forEach(input => {
-        input.bindValue = '';
+      dialog.querySelectorAll('md-outlined-text-field').forEach(input => {
+        input.value = '';
+        input.dispatchEvent(new Event('input', {bubbles: true}));
       });
     }
 
@@ -490,10 +508,15 @@
       this.closeDialog(this.openDialog);
       return;
     }
+    const patchNum = this.patchNum;
     assertIsDefined(this.patchNum, 'patchset number');
+    if (patchNum === PARENT) {
+      fireAlert(this, "This doesn't work on Parent");
+    }
     const url = this.getViewModel().editUrl({
       editView: {path: this.path},
-      patchNum: this.patchNum,
+      // since parent is checked above, it's revision patchset.
+      patchNum: patchNum as RevisionPatchSetNum,
     });
     this.getNavigation().setUrl(url);
     this.closeDialog(this.getDialogFromEvent(e));
@@ -658,14 +681,6 @@
     this.path = e.detail.value ?? '';
   }
 
-  private handleBindValueChangedNewPath(e: BindValueChangeEvent) {
-    this.newPath = e.detail.value ?? '';
-  }
-
-  private handleBindValueChangedPath(e: BindValueChangeEvent) {
-    this.path = e.detail.value ?? '';
-  }
-
   // Reset the upload status when dialog is opened or closed
   private async resetUploadStatus() {
     this.fileName = '';
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 0e9985d..57b6524 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -24,7 +24,7 @@
 } from '../../../types/common';
 import {RepoName} from '../../../api/rest-api';
 import {queryAndAssert} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import '../../shared/gr-dialog/gr-dialog';
 import {testResolver} from '../../../test/common-test-setup';
@@ -164,9 +164,15 @@
             <div class="main" slot="main">
               <gr-autocomplete placeholder="Enter an existing full file path.">
               </gr-autocomplete>
-              <iron-input id="newPathIronInput">
-                <input id="newPathInput" placeholder="Enter the new path." />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="newPathInput"
+                inputmode=""
+                placeholder="Enter the new path."
+                type="text"
+              >
+              </md-outlined-text-field>
             </div>
           </gr-dialog>
           <gr-dialog
@@ -178,9 +184,14 @@
           >
             <div class="header" slot="header">Restore this file?</div>
             <div class="main" slot="main">
-              <iron-input>
-                <input />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                inputmode=""
+                readonly=""
+                type="text"
+              >
+              </md-outlined-text-field>
             </div>
           </gr-dialog>
         </dialog>
@@ -225,6 +236,7 @@
       assert.isFalse(queryStub.called);
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
+      openAutoComplete.debounceWait = 10;
       openAutoComplete.focused = true;
       openAutoComplete.text = 'src/test.cpp';
       // Focus happens after updateComplete, so we first wait for it explicitly.
@@ -281,6 +293,7 @@
       assert.isFalse(queryStub.called);
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
+      deleteAutocomplete.debounceWait = 10;
       deleteAutocomplete.focused = true;
       deleteAutocomplete.text = 'src/test.cpp';
       // Focus happens after updateComplete, so we first wait for it explicitly.
@@ -312,6 +325,7 @@
       assert.isFalse(queryStub.called);
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
+      deleteAutocomplete.debounceWait = 10;
       deleteAutocomplete.focused = true;
       deleteAutocomplete.text = 'src/test.cpp';
       // Focus happens after updateComplete, so we first wait for it explicitly.
@@ -386,7 +400,10 @@
       assert.isTrue(queryStub.called);
       assert.isTrue(element.renameDialog!.disabled);
 
-      element.newPathIronInput!.bindValue = 'src/test.newPath';
+      element.newPathInput!.value = 'src/test.newPath';
+      element.newPathInput!.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
 
       assert.isFalse(element.renameDialog!.disabled);
@@ -411,6 +428,7 @@
       assert.isFalse(queryStub.called);
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
+      renameAutocomplete.debounceWait = 10;
       renameAutocomplete.focused = true;
       renameAutocomplete.text = 'src/test.cpp';
       // Focus happens after updateComplete, so we first wait for it explicitly.
@@ -422,7 +440,10 @@
       assert.isTrue(queryStub.called);
       assert.isTrue(element.renameDialog!.disabled);
 
-      element.newPathIronInput!.bindValue = 'src/test.newPath';
+      element.newPathInput!.value = 'src/test.newPath';
+      element.newPathInput!.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
 
       assert.isFalse(element.renameDialog!.disabled);
@@ -447,7 +468,10 @@
         element.renameDialog,
         'gr-autocomplete'
       ).text = 'src/test.cpp';
-      element.newPathIronInput!.bindValue = 'src/test.newPath';
+      element.newPathInput!.value = 'src/test.newPath';
+      element.newPathInput!.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isFalse(element.renameDialog!.disabled);
       queryAndAssert<GrButton>(element.renameDialog, 'gr-button').click();
@@ -468,7 +492,7 @@
 
     test('restore hidden by default', () => {
       assert.isTrue(
-        queryAndAssert(element, '#restore').classList.contains('invisible')!
+        queryAndAssert(element, '#restore').classList.contains('invisible')
       );
     });
 
@@ -572,7 +596,7 @@
 
   test('getDialogFromEvent', async () => {
     const spy = sinon.spy(element, 'getDialogFromEvent');
-    element.addEventListener('tap', element.getDialogFromEvent);
+    element.addEventListener('click', element.getDialogFromEvent);
 
     element.openDialog!.click();
     await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
index 97f7d44..3ba9385 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
@@ -7,7 +7,7 @@
 import {GrEditConstants} from '../gr-edit-constants';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {FileActionTapEvent} from '../../../types/events';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {DropdownLink} from '../../../types/common';
@@ -54,7 +54,6 @@
       id="actions"
       .items=${fileActions}
       down-arrow=""
-      vertical-offset="20"
       @tap-item=${this._handleActionTap}
       link=""
       >Actions</gr-dropdown
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.ts
index a406251..366351d 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.ts
@@ -10,7 +10,7 @@
 import {GrEditConstants} from '../gr-edit-constants';
 import {queryAndAssert} from '../../../test/test-utils';
 import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-edit-file-controls tests', () => {
   let element: GrEditFileControls;
@@ -30,9 +30,7 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <gr-dropdown down-arrow="" id="actions" link="" vertical-offset="20">
-          Actions
-        </gr-dropdown>
+        <gr-dropdown down-arrow="" id="actions" link=""> Actions </gr-dropdown>
       `
     );
   });
@@ -40,10 +38,10 @@
   test('open tap emits event', async () => {
     const actions = queryAndAssert<GrDropdown>(element, '#actions');
     element.filePath = 'foo';
-    actions.open();
+    actions.dropdownTriggerTapHandler();
     await actions.updateComplete;
 
-    const row = queryAndAssert<HTMLSpanElement>(actions, 'li [data-id="open"]');
+    const row = queryAndAssert<HTMLSpanElement>(actions, '[data-id="open"]');
     row.click();
     assert.isTrue(fileActionHandler.called);
     assert.deepEqual(fileActionHandler.lastCall.args[0].detail, {
@@ -55,13 +53,10 @@
   test('delete tap emits event', async () => {
     const actions = queryAndAssert<GrDropdown>(element, '#actions');
     element.filePath = 'foo';
-    actions.open();
+    actions.dropdownTriggerTapHandler();
     await actions.updateComplete;
 
-    const row = queryAndAssert<HTMLSpanElement>(
-      actions,
-      'li [data-id="delete"]'
-    );
+    const row = queryAndAssert<HTMLSpanElement>(actions, '[data-id="delete"]');
     row.click();
     assert.isTrue(fileActionHandler.called);
     assert.deepEqual(fileActionHandler.lastCall.args[0].detail, {
@@ -73,13 +68,10 @@
   test('restore tap emits event', async () => {
     const actions = queryAndAssert<GrDropdown>(element, '#actions');
     element.filePath = 'foo';
-    actions.open();
+    actions.dropdownTriggerTapHandler();
     await actions.updateComplete;
 
-    const row = queryAndAssert<HTMLSpanElement>(
-      actions,
-      'li [data-id="restore"]'
-    );
+    const row = queryAndAssert<HTMLSpanElement>(actions, '[data-id="restore"]');
     row.click();
     assert.isTrue(fileActionHandler.called);
     assert.deepEqual(fileActionHandler.lastCall.args[0].detail, {
@@ -91,13 +83,10 @@
   test('rename tap emits event', async () => {
     const actions = queryAndAssert<GrDropdown>(element, '#actions');
     element.filePath = 'foo';
-    actions.open();
+    actions.dropdownTriggerTapHandler();
     await actions.updateComplete;
 
-    const row = queryAndAssert<HTMLSpanElement>(
-      actions,
-      'li [data-id="rename"]'
-    );
+    const row = queryAndAssert<HTMLSpanElement>(actions, '[data-id="rename"]');
     row.click();
     assert.isTrue(fileActionHandler.called);
     assert.deepEqual(fileActionHandler.lastCall.args[0].detail, {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog.ts b/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog.ts
index 2d7c587..85196b4 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog.ts
@@ -8,7 +8,7 @@
 import {GrEditPreferences} from '../../settings/gr-edit-preferences/gr-edit-preferences';
 import {assertIsDefined} from '../../../utils/common-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {ValueChangedEvent} from '../../../types/events';
 import {modalStyles} from '../../../styles/gr-modal-styles';
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog_test.ts
index 083eaa9..c4b7913 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-preferences-dialog/gr-edit-preferences-dialog_test.ts
@@ -14,7 +14,7 @@
   waitUntil,
 } from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {EditPreferencesInfo} from '../../../types/common';
 
 suite('gr-edit-preferences-dialog', () => {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 5731522..f348a80 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -10,9 +10,12 @@
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
 import '../gr-default-editor/gr-default-editor';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {
-  EditPreferencesInfo,
   Base64FileContent,
+  ChangeInfo,
+  EditPreferencesInfo,
+  RevisionInfo,
   RevisionPatchSetNum,
 } from '../../../types/common';
 import {ParsedChangeInfo} from '../../../types/types';
@@ -22,10 +25,10 @@
 import {ErrorCallback} from '../../../api/rest';
 import {assertIsDefined} from '../../../utils/common-util';
 import {debounce, DelayedTask} from '../../../utils/async-util';
-import {changeIsMerged, changeIsAbandoned} from '../../../utils/change-util';
+import {changeIsAbandoned, changeIsMerged} from '../../../utils/change-util';
 import {Modifier} from '../../../utils/dom-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
@@ -94,6 +97,8 @@
 
   private readonly reporting = getAppContext().reportingService;
 
+  private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
   private readonly getStorage = resolve(this, storageServiceToken);
 
   private readonly getUserModel = resolve(this, userModelToken);
@@ -265,7 +270,7 @@
               ?disabled=${this.computeSaveDisabled()}
               primary=""
               link=""
-              title="Save and Close the file"
+              title=${this.computeButtonTitle('Save')}
               @click=${this.handleSaveTap}
               >Save</gr-button
             >
@@ -273,7 +278,7 @@
               id="publish"
               link=""
               primary=""
-              title="Publish your edit. A new patchset will be created."
+              title=${this.computeButtonTitle('Publish')}
               @click=${this.handlePublishTap}
               ?disabled=${this.computeSaveDisabled()}
               >Save & Publish</gr-button
@@ -478,6 +483,28 @@
     return this.content === this.newContent;
   }
 
+  private computeButtonTitle(buttonType: 'Save' | 'Publish'): string {
+    const disabledReasonPrefix = `${buttonType} disabled:`;
+
+    if (this.saving) {
+      return `${disabledReasonPrefix} Saving in progress...`;
+    }
+
+    if (this.content === undefined || this.newContent === undefined) {
+      return `${disabledReasonPrefix} Editor is initializing`;
+    }
+    if (this.content === this.newContent) {
+      return `${disabledReasonPrefix} ${
+        buttonType === 'Save' ? 'No changes made' : 'No changes to publish'
+      }`;
+    }
+
+    if (buttonType === 'Save') {
+      return 'Save and Close the file';
+    }
+    return 'Publish your edit. A new patchset will be created.';
+  }
+
   // private but used in test
   handleCloseTap = () => {
     // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
@@ -519,11 +546,27 @@
             return;
           }
           assertIsDefined(this.change, 'change');
+
+          this.getPluginLoader().jsApiService.handlePublishEdit(
+            this.change as ChangeInfo,
+            this.getLatestRevision(this.change as ChangeInfo)
+          );
+
           this.getChangeModel().navigateToChangeResetReload();
         });
     });
   };
 
+  private getLatestRevision(change: ChangeInfo): RevisionInfo | null {
+    const patchNum = this.latestPatchsetNumber;
+    for (const rev of Object.values(change.revisions ?? {})) {
+      if (rev._number === patchNum) {
+        return rev;
+      }
+    }
+    return null;
+  }
+
   private handleContentChange(e: CustomEvent<{value: string}>) {
     this.storeTask = debounce(
       this.storeTask,
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index 3bfeecb..c8b476b 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -27,7 +27,7 @@
 import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import {GrDefaultEditor} from '../gr-default-editor/gr-default-editor';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {Modifier} from '../../../utils/dom-util';
 import {testResolver} from '../../../test/common-test-setup';
 import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
@@ -106,7 +106,7 @@
                 primary=""
                 role="button"
                 tabindex="-1"
-                title="Save and Close the file"
+                title="Save disabled: Editor is initializing"
               >
                 Save
               </gr-button>
@@ -118,7 +118,7 @@
                 primary=""
                 role="button"
                 tabindex="-1"
-                title="Publish your edit. A new patchset will be created."
+                title="Publish disabled: Editor is initializing"
               >
                 Save & Publish
               </gr-button>
@@ -600,4 +600,33 @@
       assert.equal(element.storageKey, 'c1_ps1_test');
     });
   });
+
+  suite('save enabled/disabled', () => {
+    test('should be disabled when content is not modified', async () => {
+      element.content = 'initial content';
+      element.newContent = 'initial content';
+      element.saving = false;
+      await element.updateComplete;
+      const saveButton = query<GrButton>(element, '#save');
+      const publishButton = query<GrButton>(element, '#publish');
+      assert.isTrue(saveButton!.disabled, 'save button should be disabled');
+      assert.isTrue(
+        publishButton!.disabled,
+        'publish button should be disabled'
+      );
+    });
+    test('should be enabled when content is modified', async () => {
+      element.content = 'initial content';
+      element.newContent = 'modified content';
+      element.saving = false;
+      await element.updateComplete;
+      const saveButton = query<GrButton>(element, '#save');
+      const publishButton = query<GrButton>(element, '#publish');
+      assert.isFalse(saveButton!.disabled, 'save button should be enabled');
+      assert.isFalse(
+        publishButton!.disabled,
+        'publish button should be enabled'
+      );
+    });
+  });
 });
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 1805c0c..acc8cf2 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -17,7 +17,6 @@
 import './core/gr-error-manager/gr-error-manager';
 import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog';
 import './core/gr-main-header/gr-main-header';
-import './core/gr-smart-search/gr-smart-search';
 import './diff/gr-diff-view/gr-diff-view';
 import './edit/gr-editor-view/gr-editor-view';
 import './plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -53,7 +52,7 @@
 import {resolve} from '../models/dependency';
 import {browserModelToken} from '../models/browser/browser-model';
 import {sharedStyles} from '../styles/shared-styles';
-import {LitElement, PropertyValues, html, css, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {Shortcut, ShortcutController} from './lit/shortcut-controller';
 import {cache} from 'lit/directives/cache.js';
@@ -63,9 +62,13 @@
 import {isDarkTheme, prefersDarkColorScheme} from '../utils/theme-util';
 import {AppTheme} from '../constants/constants';
 import {subscribe} from './lit/subscription-controller';
-import {createSearchUrl} from '../models/views/search';
+import {
+  createSearchUrl,
+  searchViewModelToken,
+  SearchViewState,
+} from '../models/views/search';
 import {createSettingsUrl} from '../models/views/settings';
-import {DashboardType, createDashboardUrl} from '../models/views/dashboard';
+import {createDashboardUrl, DashboardType} from '../models/views/dashboard';
 import {userModelToken} from '../models/user/user-model';
 import {modalStyles} from '../styles/gr-modal-styles';
 import {AdminChildView, createAdminUrl} from '../models/views/admin';
@@ -126,8 +129,6 @@
 
   @state() private settingsUrl?: string;
 
-  @state() private mobileSearch = false;
-
   @state() private loadRegistrationDialog = false;
 
   @state() private loadKeyboardShortcutsDialog = false;
@@ -158,6 +159,8 @@
 
   private readonly getPluginViewModel = resolve(this, pluginViewModelToken);
 
+  private readonly getSearchViewModel = resolve(this, searchViewModelToken);
+
   constructor() {
     super();
 
@@ -289,13 +292,6 @@
             --header-background,
             var(--header-background-color, #eee)
           );
-          padding: var(--header-padding);
-          border-bottom: var(--header-border-bottom);
-          border-image: var(--header-border-image);
-          border-right: 0;
-          border-left: 0;
-          border-top: 0;
-          box-shadow: var(--header-box-shadow);
           /* Make sure the header is above the main content, to preserve
             box-shadow visibility. We need 111 here 1, because dropdowns in the
             header should be shown on top of the sticky diff header, which has a
@@ -358,8 +354,7 @@
       <gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
       ${this.renderHeader()}
       <main>
-        ${this.renderMobileSearch()} ${this.renderChangeListView()}
-        ${this.renderDashboardView()}
+        ${this.renderChangeListView()} ${this.renderDashboardView()}
         ${
           // `keyed(this.changeNum, ...)` makes sure that these views are not
           // re-used across changes, which is a precaution, because we have run
@@ -394,9 +389,7 @@
     return html`
       <gr-main-header
         id="mainHeader"
-        @mobile-search=${this.mobileSearchToggle}
         @show-keyboard-shortcuts=${this.showKeyboardShortcuts}
-        .mobileSearchHidden=${!this.mobileSearch}
       >
       </gr-main-header>
     `;
@@ -432,11 +425,6 @@
     );
   }
 
-  private renderMobileSearch() {
-    if (!this.mobileSearch) return nothing;
-    return html`<gr-smart-search id="search"></gr-smart-search>`;
-  }
-
   private renderChangeListView() {
     return cache(
       this.view === GerritView.SEARCH
@@ -652,6 +640,17 @@
     if (this.params?.view && viewsToCheck.includes(this.params.view)) {
       this.lastSearchPage = location.pathname;
     }
+
+    // Clear search queries if you leave the search page.
+    if (this.params?.view !== GerritView.SEARCH) {
+      const state: SearchViewState = {
+        view: GerritView.SEARCH,
+        query: '',
+        offset: '',
+        loading: false,
+      };
+      this.getSearchViewModel().updateState(state);
+    }
   }
 
   private handleTitleChange(e: CustomEvent<TitleChangeEventDetail>) {
@@ -710,10 +709,6 @@
   private handleRpcLog(e: RpcLogEvent) {
     this.reporting.reportRpcTiming(e.detail.anonymizedUrl, e.detail.elapsed);
   }
-
-  private mobileSearchToggle() {
-    this.mobileSearch = !this.mobileSearch;
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/gr-app-element_test.ts b/polygerrit-ui/app/elements/gr-app-element_test.ts
index 736fc53..c9d2a6b 100644
--- a/polygerrit-ui/app/elements/gr-app-element_test.ts
+++ b/polygerrit-ui/app/elements/gr-app-element_test.ts
@@ -5,7 +5,7 @@
  */
 import '../test/common-test-setup';
 import './gr-app';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrAppElement} from './gr-app-element';
 
 suite('gr-app-element tests', () => {
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index 45dc3b6..a82eed4 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -15,12 +15,12 @@
 import {AppContext, injectAppContext} from '../services/app-context';
 import {PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
 import {
-  initVisibilityReporter,
-  initPerformanceReporter,
-  initErrorReporter,
-  initWebVitals,
   initClickReporter,
+  initErrorReporter,
   initInteractionReporter,
+  initPerformanceReporter,
+  initVisibilityReporter,
+  initWebVitals,
 } from '../services/gr-reporting/gr-reporting_impl';
 import {Finalizable} from '../types/types';
 
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 2ea6983..bdfb052 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -16,8 +16,8 @@
  * See https://github.com/Polymer/polymer/issues/5289
  */
 import {
-  setPassiveTouchGestures,
   setCancelSyntheticClickEvents,
+  setPassiveTouchGestures,
 } from '@polymer/polymer/lib/utils/settings';
 setCancelSyntheticClickEvents(false);
 setPassiveTouchGestures(true);
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index 7929d9b..b873692 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -7,12 +7,12 @@
 import '../test/common-test-setup';
 import './gr-app';
 import {getAppContext} from '../services/app-context';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {queryAndAssert, stubElement, stubRestApi} from '../test/test-utils';
 import {GrApp} from './gr-app';
 import {
-  createChangeViewState,
   createAppElementSearchViewParams,
+  createChangeViewState,
   createPreferences,
   createServerInfo,
 } from '../test/test-data-generators';
diff --git a/polygerrit-ui/app/elements/gr-css-mixins.ts b/polygerrit-ui/app/elements/gr-css-mixins.ts
index 523a056..a4b01c2 100644
--- a/polygerrit-ui/app/elements/gr-css-mixins.ts
+++ b/polygerrit-ui/app/elements/gr-css-mixins.ts
@@ -50,20 +50,6 @@
           --paper-input-container-label: {
             display: none;
           };
-          --paper-tab-content: {
-            margin-bottom: var(--spacing-s);
-          };
-          --paper-tab-content-focused: {
-            /* paper-tabs uses 700 here, which can look awkward */
-            font-weight: var(--font-weight-h3);
-            background: var(--gray-background-focus);
-          };
-          --paper-tab-content-unselected: {
-            /* paper-tabs uses 0.8 here, but we want to control the color
-               directly */
-            opacity: 1;
-            color: var(--deemphasized-text-color);
-          };
           --paper-item: {
             min-height: 0;
             padding: 0px 16px;
@@ -77,10 +63,6 @@
           --paper-listbox: {
             padding: 0;
           };
-          --iron-autogrow-textarea: {
-            box-sizing: border-box;
-            padding: var(--spacing-s);
-          };
         }
       </style>
     `;
diff --git a/polygerrit-ui/app/elements/integration_test.ts b/polygerrit-ui/app/elements/integration_test.ts
index a6e02be..2c6075a 100644
--- a/polygerrit-ui/app/elements/integration_test.ts
+++ b/polygerrit-ui/app/elements/integration_test.ts
@@ -6,11 +6,11 @@
 import '../test/common-test-setup';
 import './gr-app-element';
 import {testResolver} from '../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrRouter, routerToken} from './core/gr-router/gr-router';
 import {
-  queryAndAssert,
   queryAll,
+  queryAndAssert,
   stubRestApi,
   waitQueryAndAssert,
 } from '../test/test-utils';
diff --git a/polygerrit-ui/app/elements/lit-util.ts b/polygerrit-ui/app/elements/lit-util.ts
new file mode 100644
index 0000000..57dc87f
--- /dev/null
+++ b/polygerrit-ui/app/elements/lit-util.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import './shared/gr-a11y-announcer/gr-a11y-announcer';
+import {GrA11yAnnouncer} from './shared/gr-a11y-announcer/gr-a11y-announcer';
+
+export interface FixGrA11yAnnouncer extends GrA11yAnnouncer {
+  requestAvailability(): void;
+}
+
+export function grAnnouncerRequestAvailability() {
+  (GrA11yAnnouncer as unknown as FixGrA11yAnnouncer).requestAvailability();
+}
diff --git a/polygerrit-ui/app/elements/lit/fit-controller_test.ts b/polygerrit-ui/app/elements/lit/fit-controller_test.ts
index 2a60b83..8c64dce 100644
--- a/polygerrit-ui/app/elements/lit/fit-controller_test.ts
+++ b/polygerrit-ui/app/elements/lit/fit-controller_test.ts
@@ -5,7 +5,7 @@
  */
 
 import '../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {FitController} from './fit-controller';
 import {LitElement} from 'lit';
 import {customElement} from 'lit/decorators.js';
diff --git a/polygerrit-ui/app/elements/lit/incremental-repeat.ts b/polygerrit-ui/app/elements/lit/incremental-repeat.ts
index edd1df1..9a2daf0 100644
--- a/polygerrit-ui/app/elements/lit/incremental-repeat.ts
+++ b/polygerrit-ui/app/elements/lit/incremental-repeat.ts
@@ -3,12 +3,12 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {directive, AsyncDirective} from 'lit/async-directive.js';
-import {DirectiveParameters, ChildPart} from 'lit/directive.js';
+import {AsyncDirective, directive} from 'lit/async-directive.js';
+import {ChildPart, DirectiveParameters} from 'lit/directive.js';
 import {
   insertPart,
-  setChildPartValue,
   removePart,
+  setChildPartValue,
 } from 'lit/directive-helpers.js';
 
 interface RepeatOptions<T> {
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts
index 76b0a4d..e3f6f16 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts
@@ -6,7 +6,7 @@
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {PluginApi} from '../../../api/plugin';
 import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-updates-api/gr-change-updates-api.ts b/polygerrit-ui/app/elements/plugins/gr-change-updates-api/gr-change-updates-api.ts
new file mode 100644
index 0000000..ebd2f25
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-change-updates-api/gr-change-updates-api.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {PluginApi} from '../../../api/plugin';
+import {PluginsModel} from '../../../models/plugins/plugins-model';
+import {
+  ChangeUpdatesPluginApi,
+  ChangeUpdatesPublisher,
+} from '../../../api/change-updates';
+
+enum State {
+  NOT_REGISTERED,
+  REGISTERED,
+}
+
+/**
+ * Plugin API for change updates.
+ *
+ * Instance of this type created and returned to plugins that want to provide change updates data.
+ * Plugins normally just call register() once at startup.
+ */
+export class GrChangeUpdatesApi implements ChangeUpdatesPluginApi {
+  private state = State.NOT_REGISTERED;
+
+  constructor(
+    private readonly pluginsModel: PluginsModel,
+    readonly plugin: PluginApi
+  ) {}
+
+  register(publisher: ChangeUpdatesPublisher): void {
+    if (this.state === State.REGISTERED) {
+      throw new Error('Only one publisher can be registered per plugin.');
+    }
+    this.state = State.REGISTERED;
+    this.pluginsModel.changeUpdatesRegister({
+      pluginName: this.plugin.getPluginName(),
+      publisher,
+    });
+  }
+}
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
index 51cddbe..5f02f1e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
@@ -5,11 +5,11 @@
  */
 import {PluginApi} from '../../../api/plugin';
 import {
-  ChecksApiConfig,
-  ChecksProvider,
-  ChecksPluginApi,
   CheckResult,
   CheckRun,
+  ChecksApiConfig,
+  ChecksPluginApi,
+  ChecksProvider,
 } from '../../../api/checks';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {PluginsModel} from '../../../models/plugins/plugins-model';
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
index 54197ea..7bea494 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api_test.ts
@@ -24,7 +24,7 @@
     );
     testResolver(pluginLoaderToken).loadPlugins([]);
     assert.isOk(pluginApi);
-    checksApi = pluginApi!.checks();
+    checksApi = (pluginApi as PluginApi).checks();
   });
 
   teardown(() => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts
index 57888fa..62419da 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.ts
@@ -7,7 +7,7 @@
 import './gr-endpoint-decorator';
 import '../gr-endpoint-param/gr-endpoint-param';
 import '../gr-endpoint-slot/gr-endpoint-slot';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {mockPromise, queryAndAssert} from '../../../test/test-utils';
 import {GrEndpointDecorator} from './gr-endpoint-decorator';
 import {PluginApi} from '../../../api/plugin';
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.ts b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.ts
index eaaab5d..1e28db7 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.ts
@@ -4,8 +4,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-// eslint-disable-next-line import/named
-import {fixture, html, assert} from '@open-wc/testing';
+
+import {assert, fixture, html} from '@open-wc/testing';
 import {PluginApi} from '../../../api/plugin';
 import {EventHelperPluginApi} from '../../../api/event-helper';
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
index 932dc96..7a1c00e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
@@ -7,7 +7,7 @@
 import '../../../test/common-test-setup';
 import './gr-plugin-host';
 import {GrPluginHost} from './gr-plugin-host';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {createServerInfo} from '../../../test/test-data-generators';
 import {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen.ts
index fd702b1..6b2cb23 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen.ts
@@ -5,7 +5,7 @@
  */
 import '../gr-endpoint-decorator/gr-endpoint-decorator';
 import '../gr-endpoint-param/gr-endpoint-param';
-import {LitElement, html} from 'lit';
+import {html, LitElement} from 'lit';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
 import {pluginViewModelToken} from '../../../models/views/plugin';
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen_test.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen_test.ts
index 10dff23..4a4dece 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-screen/gr-plugin-screen_test.ts
@@ -5,7 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import '../../gr-app';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {queryAndAssert} from '../../../utils/common-util';
 import {screenName} from '../../../models/views/plugin';
 import {GrEndpointDecorator} from '../gr-endpoint-decorator/gr-endpoint-decorator';
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.ts
index 7c99f14..f9f0966 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html} from 'lit';
+import {html, LitElement} from 'lit';
 import {customElement, query} from 'lit/decorators.js';
 import {modalStyles} from '../../../styles/gr-modal-styles';
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts
index 80ae34d..c11e5c8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import * as sinon from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {stubElement} from '../../../test/test-utils';
 import './gr-plugin-popup';
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.ts
index 93c4c54..c2ddfdd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.ts
@@ -10,9 +10,9 @@
 import {PluginApi} from '../../../api/plugin';
 import {HookApi, PluginElement} from '../../../api/hook';
 import {queryAndAssert, waitEventLoop} from '../../../test/test-utils';
-import {LitElement, html} from 'lit';
+import {html, LitElement} from 'lit';
 import {customElement} from 'lit/decorators.js';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 
 @customElement('gr-user-test-popup')
 class GrUserTestPopupElement extends LitElement {
@@ -59,7 +59,7 @@
       manual.innerHTML = 'manual content';
       const popup = instance._getElement();
       assert.isOk(popup);
-      popup!.appendChild(manual);
+      popup.appendChild(manual);
       await waitEventLoop();
       assert.equal(
         queryAndAssert(container, '#foobar').textContent,
diff --git a/polygerrit-ui/app/elements/polymer-util.ts b/polygerrit-ui/app/elements/polymer-util.ts
deleted file mode 100644
index d325b7b..0000000
--- a/polygerrit-ui/app/elements/polymer-util.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @license
- * Copyright 2023 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
-
-export interface FixIronA11yAnnouncer extends IronA11yAnnouncer {
-  requestAvailability(): void;
-}
-
-export function ironAnnouncerRequestAvailability() {
-  (IronA11yAnnouncer as unknown as FixIronA11yAnnouncer).requestAvailability();
-}
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index e25b738..f8c368d 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -3,8 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '@polymer/iron-input/iron-input';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../shared/gr-avatar/gr-avatar';
 import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
@@ -16,17 +15,20 @@
 import {EditableAccountField} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
 import {fire} from '../../../utils/event-util';
-import {LitElement, css, html, nothing, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {when} from 'lit/directives/when.js';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
+import {ValueChangedEvent} from '../../../types/events';
 import {formStyles} from '../../../styles/form-styles';
 import {getDocUrl} from '../../../utils/url-util';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 @customElement('gr-account-info')
 export class GrAccountInfo extends LitElement {
@@ -71,6 +73,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       sharedStyles,
       grFormStyles,
       formStyles,
@@ -93,10 +96,13 @@
           border-radius: var(--border-radius);
           box-shadow: var(--elevation-level-5);
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           background-color: var(--view-background-color);
           color: var(--primary-text-color);
         }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
+        }
         .lengthCounter {
           font-weight: var(--font-weight-normal);
         }
@@ -166,22 +172,20 @@
         ${when(
           this.computeUsernameEditable(),
           () => html`<span class="value">
-            <iron-input
+            <md-outlined-text-field
+              id="usernameInput"
+              class="showBlueFocusBorder"
+              ?disabled=${this.saving}
               @keydown=${this.handleKeydown}
-              .bindValue=${this.username}
-              @bind-value-changed=${(e: BindValueChangeEvent) => {
-                if (this.username === e.detail.value) return;
-                this.username = e.detail.value;
+              .value=${this.username ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
+                if (this.username === target.value) return;
+                this.username = target.value;
                 this.hasUsernameChange = true;
               }}
-              id="usernameIronInput"
             >
-              <input
-                id="usernameInput"
-                ?disabled=${this.saving}
-                @keydown=${this.handleKeydown}
-              />
-            </iron-input>
+            </md-outlined-text-field>
           </span>`,
           () => html`<span class="value">${this.username}</span>`
         )}
@@ -191,23 +195,21 @@
         ${when(
           this.nameMutable,
           () => html`<span class="value">
-            <iron-input
+            <md-outlined-text-field
+              id="nameInput"
+              class="showBlueFocusBorder"
+              ?disabled=${this.saving}
               @keydown=${this.handleKeydown}
-              .bindValue=${this.account?.name}
-              @bind-value-changed=${(e: BindValueChangeEvent) => {
+              .value=${this.account?.name ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
                 const oldAccount = this.account;
-                if (!oldAccount || oldAccount.name === e.detail.value) return;
-                this.account = {...oldAccount, name: e.detail.value};
+                if (!oldAccount || oldAccount.name === target.value) return;
+                this.account = {...oldAccount, name: target.value};
                 this.hasNameChange = true;
               }}
-              id="nameIronInput"
             >
-              <input
-                id="nameInput"
-                ?disabled=${this.saving}
-                @keydown=${this.handleKeydown}
-              />
-            </iron-input>
+            </md-outlined-text-field>
           </span>`,
           () => html` <span class="value">${this.account?.name}</span>`
         )}
@@ -215,24 +217,23 @@
       <section>
         <label class="title" for="displayNameInput">Display name</label>
         <span class="value">
-          <iron-input
+          <md-outlined-text-field
+            id="displayNameInput"
+            class="showBlueFocusBorder"
+            ?disabled=${this.saving}
             @keydown=${this.handleKeydown}
-            .bindValue=${this.account.display_name}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
+            .value=${this.account.display_name ?? ''}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
               const oldAccount = this.account;
-              if (!oldAccount || oldAccount.display_name === e.detail.value) {
+              if (!oldAccount || oldAccount.display_name === target.value) {
                 return;
               }
-              this.account = {...oldAccount, display_name: e.detail.value};
+              this.account = {...oldAccount, display_name: target.value};
               this.hasDisplayNameChange = true;
             }}
           >
-            <input
-              id="displayNameInput"
-              ?disabled=${this.saving}
-              @keydown=${this.handleKeydown}
-            />
-          </iron-input>
+          </md-outlined-text-field>
         </span>
       </section>
       <section>
@@ -243,19 +244,20 @@
           </div>
         </span>
         <span class="value">
-          <iron-autogrow-textarea
+          <gr-autogrow-textarea
             id="statusInput"
             .label=${'statusInput'}
             ?disabled=${this.saving}
             maxlength="140"
-            .value=${this.account?.status}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
+            .value=${this.account?.status ?? ''}
+            @input=${(e: InputEvent) => {
               const oldAccount = this.account;
-              if (!oldAccount || oldAccount.status === e.detail.value) return;
-              this.account = {...oldAccount, status: e.detail.value};
+              const value = (e.target as GrAutogrowTextarea).value ?? '';
+              if (!oldAccount || oldAccount.status === value) return;
+              this.account = {...oldAccount, status: value};
               this.hasStatusChange = true;
             }}
-          ></iron-autogrow-textarea>
+          ></gr-autogrow-textarea>
         </span>
       </section>
       <section>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_screenshot_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_screenshot_test.ts
new file mode 100644
index 0000000..50efb08
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_screenshot_test.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-account-info';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrAccountInfo} from './gr-account-info';
+import {
+  createAccountDetailWithId,
+  createAccountWithIdNameAndEmail,
+  createAuth,
+  createPreferences,
+  createServerInfo,
+} from '../../../test/test-data-generators';
+import {stubRestApi} from '../../../test/test-utils';
+import {EditableAccountField} from '../../../api/rest-api';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+
+suite('gr-account-info screenshot tests', () => {
+  let element: GrAccountInfo;
+
+  setup(async () => {
+    const account = {
+      ...createAccountDetailWithId(),
+      ...createAccountWithIdNameAndEmail(123),
+    };
+    const config = createServerInfo();
+    config.auth = {
+      ...createAuth(),
+      editable_account_fields: [
+        EditableAccountField.FULL_NAME,
+        EditableAccountField.USER_NAME,
+      ],
+    };
+
+    stubRestApi('getAccount').resolves(account);
+    stubRestApi('getConfig').resolves(config);
+    stubRestApi('getPreferences').resolves(createPreferences());
+    stubRestApi('getAvatarChangeUrl').resolves('');
+
+    element = await fixture<GrAccountInfo>(
+      html`<gr-account-info></gr-account-info>`
+    );
+    await element.loadData();
+    await element.updateComplete;
+  });
+
+  test('screenshot', async () => {
+    await visualDiff(element, 'gr-account-info');
+    await visualDiffDarkTheme(element, 'gr-account-info');
+  });
+});
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
index f954960..69754b9 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -20,21 +20,22 @@
   createPreferences,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {IronInputElement} from '@polymer/iron-input';
 import {SinonStubbedMember} from 'sinon';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {EditableAccountField} from '../../../api/rest-api';
-import {fixture, html, assert} from '@open-wc/testing';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-account-info tests', () => {
   let element!: GrAccountInfo;
   let account: AccountDetailInfo;
   let config: ServerInfo;
 
-  function queryIronInput(selector: string): IronInputElement {
-    const input = query<IronInputElement>(element, selector);
-    if (!input) assert.fail(`<iron-input> with id ${selector} not found.`);
+  function queryInput(selector: string): MdOutlinedTextField {
+    const input = query<MdOutlinedTextField>(element, selector);
+    if (!input)
+      assert.fail(`<md-outlined-text-field> with id ${selector} not found.`);
     return input;
   }
 
@@ -110,9 +111,14 @@
           <section>
             <label class="title" for="displayNameInput">Display name</label>
             <span class="value">
-              <iron-input>
-                <input id="displayNameInput" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="displayNameInput"
+                inputmode=""
+                type="text"
+              >
+              </md-outlined-text-field>
             </span>
           </section>
           <section>
@@ -121,11 +127,7 @@
               <div class="lengthCounter">0/140</div>
             </span>
             <span class="value">
-              <iron-autogrow-textarea
-                aria-disabled="false"
-                id="statusInput"
-                maxlength="140"
-              />
+              <gr-autogrow-textarea id="statusInput" maxlength="140" />
             </span>
           </section>
           <section>
@@ -182,7 +184,7 @@
     const inputSpan = section.querySelectorAll('.value')[0];
 
     assert.isTrue(element.nameMutable);
-    assert.equal(queryIronInput('#nameIronInput').bindValue, account.name);
+    assert.equal(queryInput('#nameInput').value, account.name);
     assert.isFalse(inputSpan.hasAttribute('hidden'));
   });
 
@@ -214,10 +216,7 @@
     const inputSpan = section.querySelectorAll('.value')[0];
 
     assert.isTrue(element.computeUsernameEditable());
-    assert.equal(
-      queryIronInput('#usernameIronInput').bindValue,
-      account.username
-    );
+    assert.equal(queryInput('#usernameInput').value, account.username);
     assert.isFalse(inputSpan.hasAttribute('hidden'));
   });
 
@@ -248,8 +247,11 @@
       assert.isTrue(element.nameMutable);
       assert.isFalse(element.hasUnsavedChanges);
 
-      const statusInputEl = queryIronInput('#nameIronInput');
-      statusInputEl.bindValue = 'new name';
+      const statusInputEl = queryInput('#nameInput');
+      statusInputEl.value = 'new name';
+      statusInputEl.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isTrue(element.hasNameChange);
       assert.isFalse(element.hasStatusChange);
@@ -277,8 +279,11 @@
       await element.updateComplete;
       assert.isTrue(element.computeUsernameEditable());
 
-      const statusInputEl = queryIronInput('#usernameIronInput');
-      statusInputEl.bindValue = 'new username';
+      const statusInputEl = queryInput('#usernameInput');
+      statusInputEl.value = 'new username';
+      statusInputEl.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isTrue(element.hasUsernameChange);
       assert.isFalse(element.hasNameChange);
@@ -296,11 +301,14 @@
     test('status', async () => {
       assert.isFalse(element.hasUnsavedChanges);
 
-      const statusTextarea = queryAndAssert<IronAutogrowTextareaElement>(
+      const statusTextarea = queryAndAssert<GrAutogrowTextarea>(
         element,
         '#statusInput'
       );
       statusTextarea.value = 'new status';
+      statusTextarea.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isFalse(element.hasNameChange);
       assert.isTrue(element.hasStatusChange);
@@ -338,16 +346,22 @@
       assert.isTrue(element.nameMutable);
       assert.isFalse(element.hasUnsavedChanges);
 
-      const inputEl = queryIronInput('#nameIronInput');
-      inputEl.bindValue = 'new name';
+      const inputEl = queryInput('#nameInput');
+      inputEl.value = 'new name';
+      inputEl.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isTrue(element.hasNameChange);
 
-      const statusTextarea = queryAndAssert<IronAutogrowTextareaElement>(
+      const statusTextarea = queryAndAssert<GrAutogrowTextarea>(
         element,
         '#statusInput'
       );
       statusTextarea.value = 'new status';
+      statusTextarea.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isTrue(element.hasStatusChange);
 
@@ -392,11 +406,14 @@
       assert.equal(displaySpan.textContent, account.name);
       assert.isUndefined(inputSpan);
 
-      const statusTextarea = queryAndAssert<IronAutogrowTextareaElement>(
+      const statusTextarea = queryAndAssert<GrAutogrowTextarea>(
         element,
         '#statusInput'
       );
       statusTextarea.value = 'new status';
+      statusTextarea.dispatchEvent(
+        new Event('input', {bubbles: true, composed: true})
+      );
       await element.updateComplete;
       assert.isTrue(element.hasStatusChange);
 
@@ -426,8 +443,9 @@
     await element.updateComplete;
     assert.isFalse(element.hasUsernameChange);
 
-    const inputEl = queryIronInput('#usernameIronInput');
-    inputEl.bindValue = 'test';
+    const inputEl = queryInput('#usernameInput');
+    inputEl.value = 'test';
+    inputEl.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
     await element.updateComplete;
 
     assert.isTrue(element.hasUsernameChange);
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.ts b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.ts
index 3fe55a5..098ab8b 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.ts
@@ -8,7 +8,7 @@
 import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
 import {GrAgreementsList} from './gr-agreements-list';
 import {ContributorAgreementInfo} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-agreements-list tests', () => {
   let element: GrAgreementsList;
diff --git a/polygerrit-ui/app/elements/settings/gr-auth-token/gr-auth-token.ts b/polygerrit-ui/app/elements/settings/gr-auth-token/gr-auth-token.ts
new file mode 100644
index 0000000..1637911
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-auth-token/gr-auth-token.ts
@@ -0,0 +1,452 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {getAppContext} from '../../../services/app-context';
+import {grFormStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
+import {userModelToken} from '../../../models/user/user-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {AuthTokenInfo} from '../../../types/common';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {fireAlert} from '../../../utils/event-util';
+import {parseDate} from '../../../utils/date-util';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-auth-token': GrAuthToken;
+  }
+}
+
+@customElement('gr-auth-token')
+export class GrAuthToken extends LitElement {
+  @query('#generatedAuthTokenModal')
+  generatedAuthTokenModal?: HTMLDialogElement;
+
+  @query('#deleteAuthTokenModal')
+  deleteAuthTokenModal?: HTMLDialogElement;
+
+  @state()
+  loading = false;
+
+  @state()
+  username?: string;
+
+  @state()
+  generatedAuthToken?: AuthTokenInfo;
+
+  @state()
+  status?: string;
+
+  @state()
+  passwordUrl: string | null = null;
+
+  @state()
+  maxLifetime = 'unlimited';
+
+  @property({type: Array})
+  tokens: AuthTokenInfo[] = [];
+
+  @property({type: String})
+  newTokenId = '';
+
+  @property({type: String})
+  newLifetime = '';
+
+  @query('#generateButton') generateButton!: GrButton;
+
+  @query('#newToken') tokenInput!: MdOutlinedTextField;
+
+  @query('#lifetime') tokenLifetime!: MdOutlinedTextField;
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  // Private but used in test
+  readonly getConfigModel = resolve(this, configModelToken);
+
+  // Private but used in test
+  readonly getUserModel = resolve(this, userModelToken);
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getConfigModel().serverConfig$,
+      info => {
+        if (info) {
+          this.passwordUrl = info.auth.http_password_url || null;
+          this.maxLifetime = info.auth.max_token_lifetime || 'unlimited';
+        } else {
+          this.passwordUrl = null;
+          this.maxLifetime = 'unlimited';
+        }
+      }
+    );
+    subscribe(
+      this,
+      () => this.getUserModel().account$,
+      account => {
+        if (account) {
+          this.username = account.username;
+        }
+      }
+    );
+  }
+
+  static override get styles() {
+    return [
+      materialStyles,
+      sharedStyles,
+      grFormStyles,
+      modalStyles,
+      css`
+        .token {
+          font-family: var(--monospace-font-family);
+          font-size: var(--font-size-mono);
+          line-height: var(--line-height-mono);
+        }
+        #deleteAuthTokenModal {
+          padding: var(--spacing-xxl);
+          width: 50em;
+        }
+        #generatedAuthTokenModal {
+          padding: var(--spacing-xxl);
+          width: 50em;
+        }
+        #generatedAuthTokenDisplay {
+          margin: var(--spacing-l) 0;
+        }
+        #generatedAuthTokenDisplay .title {
+          width: unset;
+        }
+        #generatedAuthTokenDisplay .value {
+          font-family: var(--monospace-font-family);
+          font-size: var(--font-size-mono);
+          line-height: var(--line-height-mono);
+        }
+        #authTokenWarning {
+          font-style: italic;
+          text-align: center;
+        }
+        #existing {
+          margin-top: var(--spacing-l);
+          margin-bottom: var(--spacing-l);
+        }
+        #existing .idColumn {
+          min-width: 15em;
+          width: auto;
+        }
+        .closeButton {
+          bottom: 2em;
+          position: absolute;
+          right: 2em;
+        }
+        .expired {
+          color: var(--negative-red-text-color);
+        }
+        .lifeTimeInput {
+          min-width: 23em;
+        }
+        #legacyPasswordNote {
+          width: 100%;
+          background: var(--label-background);
+          padding: 1em;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html` <div class="gr-form-styles">
+        <div ?hidden=${!!this.passwordUrl}>
+          <section>
+            <span class="title">Username</span>
+            <span class="value">${this.username ?? ''}</span>
+          </section>
+
+          <section
+            ?hidden=${!(
+              this.tokens.length === 1 && this.tokens[0].id === 'legacy'
+            )}
+          >
+            ${this.renderLegacyPasswordNote()}
+          </section>
+
+          <fieldset id="existing">
+            <table>
+              <thead>
+                <tr>
+                  <th class="idColumn">ID</th>
+                  <th class="expirationColumn">Expiration Date</th>
+                  <th></th>
+                </tr>
+              </thead>
+              <tbody>
+                ${this.tokens.map(tokenInfo => this.renderToken(tokenInfo))}
+              </tbody>
+              <tfoot>
+                ${this.renderFooterRow()}
+              </tfoot>
+            </table>
+          </fieldset>
+        </div>
+        <span ?hidden=${!this.passwordUrl}>
+          <a
+            href=${this.passwordUrl!}
+            target="_blank"
+            rel="noopener noreferrer"
+          >
+            Obtain password</a
+          >
+          (opens in a new tab)
+        </span>
+      </div>
+      <dialog
+        tabindex="-1"
+        id="generatedAuthTokenModal"
+        @closed=${this.generatedAuthTokenModalClosed}
+      >
+        <div class="gr-form-styles">
+          <section id="generatedAuthTokenDisplay">
+            <span class="title">New Token:</span>
+            <span class="value"
+              >${this.status || this.generatedAuthToken?.token}</span
+            >
+            <gr-copy-clipboard
+              hasTooltip=""
+              buttonTitle="Copy token to clipboard"
+              hideInput=""
+              .text=${this.status ? '' : this.generatedAuthToken?.token}
+            >
+            </gr-copy-clipboard>
+          </section>
+          <section
+            id="authTokenWarning"
+            ?hidden=${!this.generatedAuthToken?.expiration}
+          >
+            This token will be valid until &nbsp;
+            <gr-date-formatter
+              showDateAndTime
+              withTooltip
+              .dateStr=${this.generatedAuthToken?.expiration}
+            ></gr-date-formatter>
+            .
+          </section>
+          <section id="authTokenWarning">
+            This token will not be displayed again.<br />
+            If you lose it, you will need to generate a new one.
+          </section>
+          <gr-button
+            link=""
+            class="closeButton"
+            @click=${this.closeGenerateModal}
+            >Close</gr-button
+          >
+        </div>
+      </dialog>
+      <dialog tabindex="-1" id="deleteAuthTokenModal">
+        <gr-dialog
+          id="deleteDialog"
+          class="confirmDialog"
+          confirm-label="Delete"
+          confirm-on-enter
+          @confirm=${() => this.handleDeleteAuthTokenConfirmed()}
+          @cancel=${() => this.closeDeleteModal()}
+        >
+          <div class="header" slot="header">Delete Authentication Token</div>
+          <div class="main" slot="main">
+            <section>
+              Do you really want to delete the token? The deletion cannot be
+              reverted.
+            </section>
+          </div>
+        </gr-dialog>
+      </dialog>`;
+  }
+
+  private renderLegacyPasswordNote() {
+    return html`<div id="legacyPasswordNote">
+      This account only has a legacy HTTP password configured. The legacy HTTP
+      password will be accepted until the first authentication token has been
+      created. At this point the HTTP password will be removed from the account.
+    </div>`;
+  }
+
+  private renderToken(tokenInfo: AuthTokenInfo) {
+    return html` <tr class=${this.isTokenExpired(tokenInfo) ? 'expired' : ''}>
+      <td class="idColumn">${tokenInfo.id}</td>
+      <td class="expirationColumn">
+        <gr-date-formatter
+          withTooltip
+          showDateAndTime
+          dateFormat="STD"
+          .dateStr=${tokenInfo.expiration}
+        ></gr-date-formatter>
+      </td>
+      <td>
+        <gr-button
+          id="deleteButton"
+          aria-label=${`delete token ${tokenInfo.id}`}
+          @click=${() => this.handleDeleteTap(tokenInfo.id)}
+          >Delete</gr-button
+        >
+      </td>
+    </tr>`;
+  }
+
+  private renderFooterRow() {
+    return html`
+      <tr>
+        <th style="vertical-align: top;">
+          <md-outlined-text-field
+            id="newToken"
+            class="showBlueFocusBorder"
+            placeholder="New Token ID"
+            .value=${this.newTokenId ?? ''}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
+              this.newTokenId = target.value;
+            }}
+            @keydown=${this.handleInputKeydown}
+          >
+          </md-outlined-text-field>
+        </th>
+        <th style="vertical-align: top;">
+          <md-outlined-text-field
+            id="lifetime"
+            class="lifeTimeInput showBlueFocusBorder"
+            placeholder="Lifetime (e.g. 30d)"
+            supporting-text="Max. allowed lifetime: ${this.formatDuration(
+              this.maxLifetime
+            )}. Leave empty to use maximum allowed lifetime."
+            .value=${this.newLifetime ?? ''}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
+              this.newLifetime = target.value;
+            }}
+            @keydown=${this.handleInputKeydown}
+          >
+          </md-outlined-text-field>
+        </th>
+        <th>
+          <gr-button
+            id="generateButton"
+            link=""
+            ?loading=${this.loading}
+            ?disabled=${!this.newTokenId.length}
+            @click=${this.handleGenerateTap}
+            >Generate</gr-button
+          >
+        </th>
+      </tr>
+    `;
+  }
+
+  private formatDuration(durationMinutes: string) {
+    if (!durationMinutes) return '';
+    if (durationMinutes === 'unlimited') return 'unlimited';
+    let minutes = parseInt(durationMinutes, 10);
+    let hours = Math.floor(minutes / 60);
+    minutes = minutes % 60;
+    let days = Math.floor(hours / 24);
+    hours = hours % 24;
+    const years = Math.floor(days / 365);
+    days = days % 365;
+    let formatted = '';
+    if (years) formatted += `${years}y `;
+    if (days) formatted += `${days}d `;
+    if (hours) formatted += `${hours}h `;
+    if (minutes) formatted += `${minutes}m`;
+    return formatted;
+  }
+
+  loadData() {
+    return this.restApiService.getAccountAuthTokens().then(tokens => {
+      if (!tokens) return;
+      this.tokens = tokens;
+    });
+  }
+
+  private isTokenExpired(tokenInfo: AuthTokenInfo) {
+    if (!tokenInfo.expiration) return false;
+    return parseDate(tokenInfo.expiration) < new Date();
+  }
+
+  private handleGenerateTap() {
+    this.loading = true;
+    this.status = 'Generating...';
+    this.generatedAuthTokenModal?.showModal();
+    this.restApiService
+      .generateAccountAuthToken(this.newTokenId, this.newLifetime)
+      .then(newToken => {
+        if (newToken) {
+          this.generatedAuthToken = newToken;
+          this.status = undefined;
+          this.loadData();
+          this.tokenInput.value = '';
+          this.tokenInput.dispatchEvent(new Event('input', {bubbles: true}));
+          this.tokenLifetime.value = '';
+          this.tokenLifetime.dispatchEvent(new Event('input', {bubbles: true}));
+        } else {
+          this.status = 'Failed to generate';
+        }
+      })
+      .finally(() => {
+        this.loading = false;
+      });
+  }
+
+  private handleDeleteTap(id: string) {
+    this.deleteAuthTokenModal?.setAttribute('tokenId', id);
+    this.deleteAuthTokenModal?.showModal();
+  }
+
+  private handleDeleteAuthTokenConfirmed() {
+    const id = this.deleteAuthTokenModal?.getAttribute('tokenId');
+    if (id === undefined || id === null) {
+      return;
+    }
+    this.restApiService
+      .deleteAccountAuthToken(id)
+      .then(() => {
+        this.loadData();
+      })
+      .catch(err => {
+        fireAlert(this, `Failed to delete token: ${err}`);
+      })
+      .finally(() => {
+        this.closeDeleteModal();
+      });
+  }
+
+  private closeGenerateModal() {
+    this.generatedAuthTokenModal?.close();
+  }
+
+  private closeDeleteModal() {
+    this.deleteAuthTokenModal?.close();
+  }
+
+  private generatedAuthTokenModalClosed() {
+    this.status = undefined;
+    this.generatedAuthToken = undefined;
+  }
+
+  private handleInputKeydown(e: KeyboardEvent) {
+    if (e.key === 'Enter') {
+      e.stopPropagation();
+      this.handleGenerateTap();
+    }
+  }
+}
diff --git a/polygerrit-ui/app/elements/settings/gr-auth-token/gr-auth-token_test.ts b/polygerrit-ui/app/elements/settings/gr-auth-token/gr-auth-token_test.ts
new file mode 100644
index 0000000..f16f4a8
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-auth-token/gr-auth-token_test.ts
@@ -0,0 +1,214 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-auth-token';
+import {GrAuthToken} from './gr-auth-token';
+import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
+import {
+  createAccountDetailWithId,
+  createServerInfo,
+} from '../../../test/test-data-generators';
+import {AccountDetailInfo, ServerInfo} from '../../../types/common';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {assert, fixture, html, waitUntil} from '@open-wc/testing';
+import {AuthTokenInfo} from '../../../types/common';
+
+suite('gr-auth-token tests', () => {
+  let element: GrAuthToken;
+  let account: AccountDetailInfo;
+  let config: ServerInfo;
+
+  setup(async () => {
+    account = {...createAccountDetailWithId(), username: 'user name'};
+    config = createServerInfo();
+
+    stubRestApi('getAccount').returns(Promise.resolve(account));
+    stubRestApi('getConfig').returns(Promise.resolve(config));
+
+    element = await fixture(html`<gr-auth-token></gr-auth-token>`);
+    await waitUntil(
+      () => element.getUserModel().getState().account === account
+    );
+    await waitUntil(
+      () => element.getConfigModel().getState().serverConfig === config
+    );
+    await waitEventLoop();
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(
+      element,
+      `
+        <div class="gr-form-styles">
+          <div>
+            <section>
+              <span class="title"> Username </span>
+              <span class="value"> user name </span>
+            </section>
+            <section hidden="">
+              <div id="legacyPasswordNote">
+                This account only has a legacy HTTP password configured. The legacy HTTP
+      password will be accepted until the first authentication token has been
+      created. At this point the HTTP password will be removed from the account.
+              </div>
+            </section>
+            <fieldset id="existing">
+              <table>
+                <thead>
+                  <tr>
+                    <th class="idColumn">ID</th>
+                    <th class="expirationColumn">Expiration Date</th>
+                    <th></th>
+                  </tr>
+                </thead>
+                <tbody></tbody>
+                <tfoot>
+                  <tr>
+                    <th style="vertical-align: top;">
+                    <md-outlined-text-field
+                      autocomplete=""
+                      class="showBlueFocusBorder"
+                      id="newToken"
+                      inputmode=""
+                      placeholder="New Token ID"
+                      type="text"
+                    >
+                    </md-outlined-text-field>
+                    </th>
+                    <th style="vertical-align: top;">
+                    <md-outlined-text-field
+                      autocomplete=""
+                      class="lifeTimeInput showBlueFocusBorder"
+                      id="lifetime"
+                      inputmode=""
+                      placeholder="Lifetime (e.g. 30d)"
+                      supporting-text="Max. allowed lifetime: unlimited. Leave empty to use maximum allowed lifetime."
+                      type="text"
+                    >
+                    </md-outlined-text-field>
+                    </th>
+                    <th>
+                      <gr-button
+                        aria-disabled="true"
+                        id="generateButton"
+                        link=""
+                        disabled=""
+                        role="button"
+                        tabindex="-1"
+                        >Generate</gr-button
+                      >
+                    </th>
+                  </tr>
+                </tfoot>
+              </table>
+            </fieldset>
+          </div>
+          <span hidden="">
+            <a href="" target="_blank" rel="noopener noreferrer">
+              Obtain password
+            </a>
+            (opens in a new tab)
+          </span>
+        </div>
+        <dialog tabindex="-1" id="generatedAuthTokenModal">
+          <div class="gr-form-styles">
+            <section id="generatedAuthTokenDisplay">
+              <span class="title"> New Token: </span>
+              <span class="value"> </span>
+              <gr-copy-clipboard
+                buttontitle="Copy token to clipboard"
+                hastooltip=""
+                hideinput=""
+              >
+              </gr-copy-clipboard>
+            </section>
+            <section hidden="" id="authTokenWarning">
+              This token will be valid until
+              <gr-date-formatter showdateandtime="" withtooltip="">
+              </gr-date-formatter>
+              .
+            </section>
+            <section id="authTokenWarning">
+              This token will not be displayed again.
+              <br />
+              If you lose it, you will need to generate a new one.
+            </section>
+            <gr-button
+              aria-disabled="false"
+              class="closeButton"
+              link=""
+              role="button"
+              tabindex="0"
+            >
+              Close
+            </gr-button>
+          </div>
+        </dialog>
+        <dialog id="deleteAuthTokenModal" tabindex="-1">
+          <gr-dialog
+            class="confirmDialog"
+            confirm-label="Delete"
+            confirm-on-enter=""
+            id="deleteDialog"
+          >
+            <div class="header" slot="header">Delete Authentication Token</div>
+            <div class="main" slot="main">
+              <section>
+                Do you really want to delete the token? The deletion cannot be
+              reverted.
+              </section>
+            </div>
+          </gr-dialog>
+        </dialog>
+      `
+    );
+  });
+
+  test('generate token', async () => {
+    const button = queryAndAssert<GrButton>(element, '#generateButton');
+    const nextToken = {id: 'next-token-id', token: 'next-token'};
+    let generateResolve: (
+      value: AuthTokenInfo | PromiseLike<AuthTokenInfo>
+    ) => void;
+    const generateStub = stubRestApi('generateAccountAuthToken').callsFake(
+      () =>
+        new Promise(resolve => {
+          generateResolve = resolve;
+        })
+    );
+
+    assert.isNotOk(element.generatedAuthToken);
+
+    element.tokenInput.value = nextToken.id;
+    element.tokenInput.dispatchEvent(new Event('input', {bubbles: true}));
+
+    await element.updateComplete;
+
+    assert.isFalse(button.disabled);
+    button.click();
+
+    assert.isTrue(generateStub.called);
+    assert.equal(element.status, 'Generating...');
+
+    generateStub.lastCall.returnValue.then(() => {
+      generateResolve(nextToken);
+      assert.equal(element.generatedAuthToken, nextToken);
+    });
+  });
+
+  test('without http_password_url', () => {
+    assert.isNull(element.passwordUrl);
+  });
+
+  test('with http_password_url', async () => {
+    config.auth.http_password_url = 'http://example.com/';
+    element.passwordUrl = config.auth.http_password_url;
+    await element.updateComplete;
+    assert.isNotNull(element.passwordUrl);
+    assert.equal(element.passwordUrl, config.auth.http_password_url);
+  });
+});
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index ea20b54..55ce032 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -3,39 +3,55 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+
 import '../../shared/gr-button/gr-button';
-import {ServerInfo} from '../../../types/common';
-import {LitElement, css, html} from 'lit';
+import {PreferencesInput, ServerInfo} from '../../../types/common';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
-import {fire} from '../../../utils/event-util';
-import {ValueChangedEvent} from '../../../types/events';
 import {ColumnNames} from '../../../constants/constants';
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
 import {configModelToken} from '../../../models/config/config-model';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {
+  changeTablePrefs,
+  userModelToken,
+} from '../../../models/user/user-model';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {classMap} from 'lit/directives/class-map.js';
 
 @customElement('gr-change-table-editor')
 export class GrChangeTableEditor extends LitElement {
   @property({type: Array})
-  displayedColumns: string[] = [];
-
-  @property({type: Boolean})
-  showNumber?: boolean;
-
-  @property({type: Array})
   defaultColumns: string[] = Object.values(ColumnNames);
 
   @state()
   serverConfig?: ServerInfo;
 
+  @state() prefs: PreferencesInput = {};
+
+  // private but used in test
+  @state() localChangeTableColumns: string[] = [];
+
+  // private but used in test
+  @state() showNumber?: boolean;
+
   private readonly getConfigModel = resolve(this, configModelToken);
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
   static override get styles() {
     return [
-      sharedStyles,
+      materialStyles,
       grFormStyles,
+      sharedStyles,
+      fontStyles,
+      menuPageStyles,
       css`
         #changeCols {
           width: auto;
@@ -44,15 +60,8 @@
           text-align: center;
         }
         .checkboxContainer {
-          cursor: pointer;
           text-align: center;
         }
-        .checkboxContainer input {
-          cursor: pointer;
-        }
-        .checkboxContainer:hover {
-          outline: 1px solid var(--border-color);
-        }
       `,
     ];
   }
@@ -66,114 +75,151 @@
         this.serverConfig = config;
       }
     );
+    subscribe(
+      this,
+      () => this.getUserModel().preferences$,
+      prefs => {
+        if (!prefs) {
+          throw new Error('getPreferences returned undefined');
+        }
+        this.prefs = prefs;
+        this.showNumber = !!prefs.legacycid_in_change_table;
+        this.localChangeTableColumns = changeTablePrefs(prefs);
+      }
+    );
   }
 
   override render() {
-    return html`<div class="gr-form-styles">
-      <table id="changeCols">
-        <thead>
-          <tr>
-            <th class="nameHeader">Column</th>
-            <th class="visibleHeader">Visible</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr>
-            <td><label for="numberCheckbox">Number</label></td>
-            <td
-              class="checkboxContainer"
-              @click=${this.handleCheckboxContainerClick}
-            >
-              <input
-                id="numberCheckbox"
-                type="checkbox"
-                name="number"
-                @click=${this.handleNumberCheckboxClick}
-                ?checked=${this.showNumber}
-              />
-            </td>
-          </tr>
-          ${this.defaultColumns.map(column => this.renderRow(column))}
-        </tbody>
-      </table>
-    </div>`;
+    const classes = {
+      'heading-2': true,
+      edited: this.hasUnsavedChanges(),
+    };
+    return html`
+      <div class="gr-form-styles">
+        <h2 id="ChangeTableColumns" class=${classMap(classes)}>
+          Change Table Columns
+        </h2>
+        <fieldset id="changeTableColumns">
+          <table id="changeCols">
+            <thead>
+              <tr>
+                <th class="nameHeader">Column</th>
+                <th class="visibleHeader">Visible</th>
+              </tr>
+            </thead>
+
+            <tbody>
+              <tr>
+                <td><label for="numberCheckbox">Number</label></td>
+                <td class="checkboxContainer">
+                  <md-checkbox
+                    id="numberCheckbox"
+                    name="number"
+                    .checked=${!!this.showNumber}
+                    @change=${this.handleNumberCheckboxClick}
+                  ></md-checkbox>
+                </td>
+              </tr>
+
+              ${this.defaultColumns.map(col => this.renderRow(col))}
+            </tbody>
+          </table>
+          <gr-button
+            id="saveChangeTable"
+            @click=${this.handleSaveChangeTable}
+            ?disabled=${!this.hasUnsavedChanges()}
+            >Save Changes</gr-button
+          >
+        </fieldset>
+      </div>
+    `;
   }
 
   renderRow(column: string) {
-    return html`<tr>
-      <td><label for=${column}>${column}</label></td>
-      <td class="checkboxContainer" @click=${this.handleCheckboxContainerClick}>
-        <input
-          id=${column}
-          type="checkbox"
-          name=${column}
-          @click=${this.handleTargetClick}
-          ?checked=${!this.computeIsColumnHidden(column)}
-        />
-      </td>
-    </tr>`;
-  }
-
-  /**
-   * Get the list of enabled column names from whichever checkboxes are
-   * checked (excluding the number checkbox).
-   * private but used in test
-   */
-  getDisplayedColumns() {
-    if (this.shadowRoot === null) return [];
-    return Array.from(
-      this.shadowRoot.querySelectorAll<HTMLInputElement>(
-        '.checkboxContainer input:not([name=number])'
-      )
-    )
-      .filter(checkbox => checkbox.checked)
-      .map(checkbox => checkbox.name);
-  }
-
-  private computeIsColumnHidden(columnToCheck?: string) {
-    if (!this.displayedColumns || !columnToCheck) {
-      return false;
-    }
-    return !this.displayedColumns.includes(columnToCheck);
-  }
-
-  /**
-   * Handle a click on a checkbox container and relay the click to the checkbox it
-   * contains.
-   */
-  private handleCheckboxContainerClick(e: MouseEvent) {
-    if (e.target === null) return;
-    const checkbox = (e.target as HTMLElement).querySelector('input');
-    if (!checkbox) {
-      return;
-    }
-    checkbox.click();
+    return html`
+      <tr>
+        <td><label for=${column}>${column}</label></td>
+        <td class="checkboxContainer">
+          <md-checkbox
+            id=${column}
+            name=${column}
+            .checked=${this.localChangeTableColumns.includes(column)}
+            @change=${this.handleTargetClick}
+          ></md-checkbox>
+        </td>
+      </tr>
+    `;
   }
 
   /**
    * Handle a click on the number checkbox and update the showNumber property
    * accordingly.
    */
-  private handleNumberCheckboxClick(e: MouseEvent) {
-    this.showNumber = (e.target as HTMLInputElement).checked;
-    fire(this, 'show-number-changed', {value: this.showNumber});
+  private handleNumberCheckboxClick(e: Event) {
+    const checkbox = e.target as MdCheckbox;
+
+    const oldValue = this.showNumber ?? false;
+    const newValue = checkbox.checked;
+
+    if (oldValue === newValue) return;
+
+    this.showNumber = newValue;
   }
 
   /**
    * Handle a click on a displayed column checkboxes (excluding number) and
-   * update the displayedColumns property accordingly.
+   * update the localChangeTableColumns property accordingly.
    */
-  private handleTargetClick() {
-    this.displayedColumns = this.getDisplayedColumns();
-    fire(this, 'displayed-columns-changed', {value: this.displayedColumns});
+  private handleTargetClick(e: Event) {
+    const checkbox = e.target as MdCheckbox;
+
+    const column = checkbox.name;
+    const checked = checkbox.checked;
+
+    const exists = this.localChangeTableColumns.includes(column);
+
+    if (checked === exists) return;
+
+    let updated: string[];
+
+    if (checked) {
+      updated = [...this.localChangeTableColumns, column];
+    } else {
+      updated = this.localChangeTableColumns.filter(c => c !== column);
+    }
+
+    // Normalize order based on defaultColumns
+    this.localChangeTableColumns = this.defaultColumns.filter(c =>
+      updated.includes(c)
+    );
+  }
+
+  // private but used in test
+  async handleSaveChangeTable() {
+    const newPrefs = {
+      ...this.prefs,
+      change_table: this.localChangeTableColumns,
+      legacycid_in_change_table: this.showNumber,
+    };
+
+    await this.getUserModel().updatePreferences(newPrefs);
+  }
+
+  private hasUnsavedChanges(): boolean {
+    const prefsColumns = changeTablePrefs(this.prefs);
+
+    const columnsChanged =
+      prefsColumns.length !== this.localChangeTableColumns.length ||
+      prefsColumns.some(c => !this.localChangeTableColumns.includes(c));
+
+    const numberChanged =
+      !!this.prefs.legacycid_in_change_table !== !!this.showNumber;
+
+    return columnsChanged || numberChanged;
   }
 }
 
 declare global {
-  interface HTMLElementEventMap {
-    'show-number-changed': ValueChangedEvent<boolean>;
-    'displayed-columns-changed': ValueChangedEvent<string[]>;
-  }
   interface HTMLElementTagNameMap {
     'gr-change-table-editor': GrChangeTableEditor;
   }
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index 9efebef..1c5c589 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -8,8 +8,9 @@
 import {GrChangeTableEditor} from './gr-change-table-editor';
 import {queryAndAssert} from '../../../test/test-utils';
 import {createServerInfo} from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {ColumnNames} from '../../../constants/constants';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
 
 suite('gr-change-table-editor tests', () => {
   let element: GrChangeTableEditor;
@@ -31,7 +32,7 @@
       // ColumnNames.STATUS omitted for testing
     ];
 
-    element.displayedColumns = columns;
+    element.localChangeTableColumns = columns;
     element.showNumber = false;
     element.serverConfig = createServerInfo();
     await element.updateComplete;
@@ -41,75 +42,101 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ ` <div class="gr-form-styles">
-        <table id="changeCols">
-          <thead>
-            <tr>
-              <th class="nameHeader">Column</th>
-              <th class="visibleHeader">Visible</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr>
-              <td><label for="numberCheckbox"> Number </label></td>
-              <td class="checkboxContainer">
-                <input id="numberCheckbox" name="number" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Subject"> Subject </label></td>
-              <td class="checkboxContainer">
-                <input checked="" id="Subject" name="Subject" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Owner"> Owner </label></td>
-              <td class="checkboxContainer">
-                <input checked="" id="Owner" name="Owner" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Reviewers"> Reviewers </label></td>
-              <td class="checkboxContainer">
-                <input
-                  checked=""
-                  id="Reviewers"
-                  name="Reviewers"
-                  type="checkbox"
-                />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Repo"> Repo </label></td>
-              <td class="checkboxContainer">
-                <input checked="" id="Repo" name="Repo" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Branch"> Branch </label></td>
-              <td class="checkboxContainer">
-                <input checked="" id="Branch" name="Branch" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Updated"> Updated </label></td>
-              <td class="checkboxContainer">
-                <input checked="" id="Updated" name="Updated" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Size"> Size </label></td>
-              <td class="checkboxContainer">
-                <input checked="" id="Size" name="Size" type="checkbox" />
-              </td>
-            </tr>
-            <tr>
-              <td><label for="Status"> Status </label></td>
-              <td class="checkboxContainer">
-                <input id="Status" name="Status" type="checkbox" />
-              </td>
-            </tr>
-          </tbody>
-        </table>
+        <h2 class="edited heading-2" id="ChangeTableColumns">
+          Change Table Columns
+        </h2>
+        <fieldset id="changeTableColumns">
+          <table id="changeCols">
+            <thead>
+              <tr>
+                <th class="nameHeader">Column</th>
+                <th class="visibleHeader">Visible</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td>
+                  <label for="numberCheckbox"> Number </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="numberCheckbox" name="number"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Subject"> Subject </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Subject" name="Subject"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Owner"> Owner </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Owner" name="Owner"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Reviewers"> Reviewers </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Reviewers" name="Reviewers"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Repo"> Repo </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Repo" name="Repo"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Branch"> Branch </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Branch" name="Branch"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Updated"> Updated </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Updated" name="Updated"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Size"> Size </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Size" name="Size"> </md-checkbox>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  <label for="Status"> Status </label>
+                </td>
+                <td class="checkboxContainer">
+                  <md-checkbox id="Status" name="Status"> </md-checkbox>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+          <gr-button
+            aria-disabled="false"
+            id="saveChangeTable"
+            role="button"
+            tabindex="0"
+          >
+            Save Changes
+          </gr-button>
+        </fieldset>
       </div>`
     );
   });
@@ -128,22 +155,22 @@
   });
 
   test('hide item', async () => {
-    const checkbox = queryAndAssert<HTMLInputElement>(
+    const checkbox = queryAndAssert<MdCheckbox>(
       element,
-      'table tr:nth-child(2) input'
+      'table tr:nth-child(2) md-checkbox'
     );
     const isChecked = checkbox.checked;
-    const displayedLength = element.displayedColumns.length;
+    const displayedLength = element.localChangeTableColumns.length;
     assert.isTrue(isChecked);
 
     checkbox.click();
     await element.updateComplete;
 
-    assert.equal(element.displayedColumns.length, displayedLength - 1);
+    assert.equal(element.localChangeTableColumns.length, displayedLength - 1);
   });
 
-  test('show item', async () => {
-    element.displayedColumns = [
+  test('show and hide item', async () => {
+    element.localChangeTableColumns = [
       ColumnNames.STATUS,
       ColumnNames.OWNER,
       ColumnNames.REPO,
@@ -153,76 +180,74 @@
     // trigger computation of enabled displayed columns
     element.serverConfig = createServerInfo();
     await element.updateComplete;
-    const checkbox = queryAndAssert<HTMLInputElement>(
-      element,
-      'table tr:nth-child(2) input'
-    );
-    const isChecked = checkbox.checked;
-    const displayedLength = element.displayedColumns.length;
-    assert.isFalse(isChecked);
-    const table = queryAndAssert<HTMLTableElement>(element, 'table');
-    assert.equal(table.style.display, '');
 
-    checkbox.click();
+    const checkboxSubject = queryAndAssert<MdCheckbox>(
+      element,
+      'table tr:nth-child(2) md-checkbox'
+    );
+    assert.equal(checkboxSubject.name, 'Subject');
+    const checkboxOwner = queryAndAssert<MdCheckbox>(
+      element,
+      'table tr:nth-child(3) md-checkbox'
+    );
+    assert.equal(checkboxOwner.name, 'Owner');
+
+    assert.equal(element.localChangeTableColumns.length, 5);
+    assert.isFalse(checkboxSubject.checked);
+    assert.isTrue(checkboxOwner.checked);
+
+    checkboxSubject.click();
     await element.updateComplete;
 
-    assert.equal(element.displayedColumns.length, displayedLength + 1);
-  });
+    assert.equal(element.localChangeTableColumns.length, 6);
+    assert.isTrue(checkboxSubject.checked);
+    assert.isTrue(checkboxOwner.checked);
 
-  test('getDisplayedColumns', () => {
-    const enabledColumns = columns;
-    assert.deepEqual(element.getDisplayedColumns(), enabledColumns);
-    const input = queryAndAssert<HTMLInputElement>(
-      element,
-      '.checkboxContainer input[name=Subject]'
-    );
-    input.click();
-    assert.deepEqual(
-      element.getDisplayedColumns(),
-      enabledColumns.filter(c => c !== 'Subject')
-    );
-  });
-
-  test('handleCheckboxContainerClick relays taps to checkboxes', async () => {
-    const firstContainer = queryAndAssert<HTMLTableRowElement>(
-      element,
-      'table tr:first-of-type .checkboxContainer'
-    );
-    assert.isFalse(element.showNumber);
-    firstContainer.click();
-    assert.isTrue(element.showNumber);
-
-    const lastContainer = queryAndAssert<HTMLTableRowElement>(
-      element,
-      'table tr:last-of-type .checkboxContainer'
-    );
-    const lastColumn =
-      element.defaultColumns[element.defaultColumns.length - 1];
-    assert.notInclude(element.displayedColumns, lastColumn);
-    lastContainer.click();
+    checkboxOwner.click();
     await element.updateComplete;
-    assert.include(element.displayedColumns, lastColumn);
+
+    assert.equal(element.localChangeTableColumns.length, 5);
+    assert.isTrue(checkboxSubject.checked);
+    assert.isFalse(checkboxOwner.checked);
   });
 
-  test('handleNumberCheckboxClick', () => {
-    const numberInput = queryAndAssert<HTMLInputElement>(
+  test('handleNumberCheckboxClick', async () => {
+    const numberInput = queryAndAssert<MdCheckbox>(
       element,
-      '.checkboxContainer input[name=number]'
+      '.checkboxContainer md-checkbox[name=number]'
     );
     numberInput.click();
+    await element.updateComplete;
     assert.isTrue(element.showNumber);
 
     numberInput.click();
+    await element.updateComplete;
     assert.isFalse(element.showNumber);
   });
 
   test('handleTargetClick', () => {
-    assert.include(element.displayedColumns, 'Subject');
-    const subjectInput = queryAndAssert<HTMLInputElement>(
+    assert.include(element.localChangeTableColumns, 'Subject');
+    const subjectInput = queryAndAssert<MdCheckbox>(
       element,
-      '.checkboxContainer input[name=Subject]'
+      '.checkboxContainer md-checkbox[name=Subject]'
     );
     subjectInput.click();
-    assert.notInclude(element.displayedColumns, 'Subject');
+    assert.notInclude(element.localChangeTableColumns, 'Subject');
+  });
+
+  test('handleSaveChangeTable', async () => {
+    let newColumns = ['Owner', 'Project', 'Branch'];
+    element.localChangeTableColumns = newColumns.slice(0);
+    element.showNumber = false;
+    await element.handleSaveChangeTable();
+    assert.deepEqual(element.prefs.change_table, newColumns);
+    assert.isNotOk(element.prefs.legacycid_in_change_table);
+
+    newColumns = ['Size'];
+    element.localChangeTableColumns = newColumns;
+    element.showNumber = true;
+    await element.handleSaveChangeTable();
+    assert.deepEqual(element.prefs.change_table, newColumns);
+    assert.isTrue(element.prefs.legacycid_in_change_table);
   });
 });
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
index 437276e..f43ea62 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
@@ -3,23 +3,24 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
 import {getBaseUrl} from '../../../utils/url-util';
 import {
-  ServerInfo,
-  GroupInfo,
   ContributorAgreementInfo,
+  GroupInfo,
+  ServerInfo,
 } from '../../../types/common';
 import {fireAlert, fireTitleChange} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
 import {ifDefined} from 'lit/directives/if-defined.js';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/radio/radio';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -58,6 +59,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       fontStyles,
       grFormStyles,
       sharedStyles,
@@ -110,15 +112,15 @@
   private renderAgreementsButton(item: ContributorAgreementInfo) {
     return html`
       <span class="contributorAgreementButton">
-        <input
+        <md-radio
           id="claNewAgreementsInput${item.name}"
           name="claNewAgreementsRadio"
-          type="radio"
           data-name=${ifDefined(item.name)}
           data-url=${ifDefined(item.url)}
-          @click=${this.handleShowAgreement}
           ?disabled=${this.disableAgreements(item)}
-        />
+          @change=${this.handleShowAgreement}
+        >
+        </md-radio>
         <label id="claNewAgreementsLabel">${item.name}</label>
       </span>
       ${this.renderAlreadySubmittedText(item)}
@@ -142,7 +144,7 @@
         <div id="agreementsUrl" class="agreementsUrl">
           <a
             href=${ifDefined(this.agreementsUrl)}
-            target="blank"
+            target="_blank"
             rel="noopener"
           >
             Please review the agreement.</a
@@ -158,12 +160,17 @@
     return html`
       <div class="agreementsTextBox">
         <h3 class="heading-3">Complete the agreement:</h3>
-        <iron-input
-          .bindValue=${this.agreementsText}
-          @bind-value-changed=${this.handleBindValueChanged}
+        <md-outlined-text-field
+          id="input-agreements"
+          class="showBlueFocusBorder"
+          placeholder="Enter 'I agree' here"
+          .value=${this.agreementsText ?? ''}
+          @input=${(e: InputEvent) => {
+            const target = e.target as HTMLInputElement;
+            this.agreementsText = target.value;
+          }}
         >
-          <input id="input-agreements" placeholder="Enter 'I agree' here" />
-        </iron-input>
+        </md-outlined-text-field>
         <gr-button
           @click=${this.handleSaveAgreements}
           ?disabled=${this.agreementsText?.toLowerCase() !== 'i agree'}
@@ -269,8 +276,4 @@
         !contributorAgreement.auto_verify_group
     );
   }
-
-  private readonly handleBindValueChanged = (e: BindValueChangeEvent) => {
-    this.agreementsText = e.detail.value;
-  };
 }
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts
index a321610..b842027 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.ts
@@ -16,7 +16,7 @@
 } from '../../../types/common';
 import {AuthType} from '../../../constants/constants';
 import {createServerInfo} from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-cla-view tests', () => {
   let element: GrClaView;
@@ -132,31 +132,32 @@
           <h1 class="heading-1">New Contributor Agreement</h1>
           <h3 class="heading-3">Select an agreement type:</h3>
           <span class="contributorAgreementButton">
-            <input
+            <md-radio
               data-name="Individual"
               data-url="static/cla_individual.html"
               id="claNewAgreementsInputIndividual"
               name="claNewAgreementsRadio"
-              type="radio"
-            />
+            >
+            </md-radio>
             <label id="claNewAgreementsLabel"> Individual </label>
           </span>
           <div class="agreementsUrl">test-description</div>
           <span class="contributorAgreementButton">
-            <input
+            <md-radio
               data-name="CLA"
               data-url="static/cla.html"
               disabled=""
               id="claNewAgreementsInputCLA"
               name="claNewAgreementsRadio"
-              type="radio"
-            />
+            >
+            </md-radio>
             <label id="claNewAgreementsLabel"> CLA </label>
           </span>
           <div class="alreadySubmittedText">Agreement already submitted.</div>
           <div class="agreementsUrl">Contributor License Agreement</div>
         </main>
-      `
+      `,
+      {ignoreAttributes: ['tabindex']}
     );
   });
 
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index 2e473f3..9cc584c 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -3,12 +3,11 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
 import {EditPreferencesInfo} from '../../../types/common';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {convertToString} from '../../../utils/string-util';
 import {subscribe} from '../../lit/subscription-controller';
@@ -16,6 +15,10 @@
 import {userModelToken} from '../../../models/user/user-model';
 import {fire} from '../../../utils/event-util';
 import {ValueChangedEvent} from '../../../types/events';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
 
 @customElement('gr-edit-preferences')
 export class GrEditPreferences extends LitElement {
@@ -26,22 +29,22 @@
   @query('#editIndentUnit') private editIndentUnit?: HTMLInputElement;
 
   @query('#editSyntaxHighlighting')
-  private editSyntaxHighlighting?: HTMLInputElement;
+  private editSyntaxHighlighting?: MdCheckbox;
 
   @query('#showAutoCloseBrackets')
-  private showAutoCloseBrackets?: HTMLInputElement;
+  private showAutoCloseBrackets?: MdCheckbox;
 
-  @query('#showIndentWithTabs') private showIndentWithTabs?: HTMLInputElement;
+  @query('#showIndentWithTabs') private showIndentWithTabs?: MdCheckbox;
 
-  @query('#showMatchBrackets') private showMatchBrackets?: HTMLInputElement;
+  @query('#showMatchBrackets') private showMatchBrackets?: MdCheckbox;
 
   @query('#editShowLineWrapping')
-  private editShowLineWrapping?: HTMLInputElement;
+  private editShowLineWrapping?: MdCheckbox;
 
-  @query('#editShowTabs') private editShowTabs?: HTMLInputElement;
+  @query('#editShowTabs') private editShowTabs?: MdCheckbox;
 
   @query('#editShowTrailingWhitespaceInput')
-  private editShowTrailingWhitespaceInput?: HTMLInputElement;
+  private editShowTrailingWhitespaceInput?: MdCheckbox;
 
   @state() editPrefs?: EditPreferencesInfo;
 
@@ -62,7 +65,16 @@
   }
 
   static override get styles() {
-    return [sharedStyles, grFormStyles];
+    return [
+      materialStyles,
+      sharedStyles,
+      grFormStyles,
+      css`
+        md-outlined-text-field {
+          max-width: 25em;
+        }
+      `,
+    ];
   }
 
   override render() {
@@ -71,37 +83,70 @@
         <section>
           <label for="editTabWidth" class="title">Tab width</label>
           <span class="value">
-            <iron-input
-              .allowedPattern=${'[0-9]'}
-              .bindValue=${convertToString(this.editPrefs?.tab_size)}
-              @change=${this.handleEditTabWidthChanged}
+            <md-outlined-text-field
+              id="editTabWidth"
+              class="showBlueFocusBorder"
+              type="number"
+              step="1"
+              .value=${convertToString(this.editPrefs?.tab_size)}
+              @input=${this.handleEditTabWidthInput}
+              @beforeinput=${(e: InputEvent) => {
+                // In iron-input we had allowedPattern, but this is not supported
+                // in md-outlined-text-field. Which uses native input functionality.
+                // We workaround this.
+                const data = e.data;
+                if (data && !/^[0-9]*$/.test(data)) {
+                  e.preventDefault();
+                }
+              }}
             >
-              <input id="editTabWidth" type="number" />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
           <label for="editColumns" class="title">Columns</label>
           <span class="value">
-            <iron-input
-              .allowedPattern=${'[0-9]'}
-              .bindValue=${convertToString(this.editPrefs?.line_length)}
-              @change=${this.handleEditLineLengthChanged}
+            <md-outlined-text-field
+              id="editColumns"
+              class="showBlueFocusBorder"
+              type="number"
+              step="1"
+              .value=${convertToString(this.editPrefs?.line_length)}
+              @input=${this.handleEditLineLengthInput}
+              @beforeinput=${(e: InputEvent) => {
+                // In iron-input we had allowedPattern, but this is not supported
+                // in md-outlined-text-field. Which uses native input functionality.
+                // We workaround this.
+                const data = e.data;
+                if (data && !/^[0-9]*$/.test(data)) {
+                  e.preventDefault();
+                }
+              }}
             >
-              <input id="editColumns" type="number" />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
           <label for="editIndentUnit" class="title">Indent unit</label>
           <span class="value">
-            <iron-input
-              .allowedPattern=${'[0-9]'}
-              .bindValue=${convertToString(this.editPrefs?.indent_unit)}
-              @change=${this.handleEditIndentUnitChanged}
+            <md-outlined-text-field
+              id="editIndentUnit"
+              class="showBlueFocusBorder"
+              type="number"
+              step="1"
+              .value=${convertToString(this.editPrefs?.indent_unit)}
+              @input=${this.handleEditIndentUnitInput}
+              @beforeinput=${(e: InputEvent) => {
+                // In iron-input we had allowedPattern, but this is not supported
+                // in md-outlined-text-field. Which uses native input functionality.
+                // We workaround this.
+                const data = e.data;
+                if (data && !/^[0-9]*$/.test(data)) {
+                  e.preventDefault();
+                }
+              }}
             >
-              <input id="editIndentUnit" type="number" />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
@@ -109,23 +154,21 @@
             >Syntax highlighting</label
           >
           <span class="value">
-            <input
+            <md-checkbox
               id="editSyntaxHighlighting"
-              type="checkbox"
-              ?checked=${this.editPrefs?.syntax_highlighting}
+              ?checked=${!!this.editPrefs?.syntax_highlighting}
               @change=${this.handleEditSyntaxHighlightingChanged}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
           <label for="editShowTabs" class="title">Show tabs</label>
           <span class="value">
-            <input
+            <md-checkbox
               id="editShowTabs"
-              type="checkbox"
-              ?checked=${this.editPrefs?.show_tabs}
+              ?checked=${!!this.editPrefs?.show_tabs}
               @change=${this.handleEditShowTabsChanged}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
@@ -133,45 +176,41 @@
             >Show trailing whitespace</label
           >
           <span class="value">
-            <input
+            <md-checkbox
               id="editShowTrailingWhitespaceInput"
-              type="checkbox"
-              ?checked=${this.editPrefs?.show_whitespace_errors}
+              ?checked=${!!this.editPrefs?.show_whitespace_errors}
               @change=${this.handleEditShowTrailingWhitespaceTap}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
           <label for="showMatchBrackets" class="title">Match brackets</label>
           <span class="value">
-            <input
+            <md-checkbox
               id="showMatchBrackets"
-              type="checkbox"
-              ?checked=${this.editPrefs?.match_brackets}
+              ?checked=${!!this.editPrefs?.match_brackets}
               @change=${this.handleMatchBracketsChanged}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
           <label for="editShowLineWrapping" class="title">Line wrapping</label>
           <span class="value">
-            <input
+            <md-checkbox
               id="editShowLineWrapping"
-              type="checkbox"
-              ?checked=${this.editPrefs?.line_wrapping}
+              ?checked=${!!this.editPrefs?.line_wrapping}
               @change=${this.handleEditLineWrappingChanged}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
           <label for="showIndentWithTabs" class="title">Indent with tabs</label>
           <span class="value">
-            <input
+            <md-checkbox
               id="showIndentWithTabs"
-              type="checkbox"
-              ?checked=${this.editPrefs?.indent_with_tabs}
+              ?checked=${!!this.editPrefs?.indent_with_tabs}
               @change=${this.handleIndentWithTabsChanged}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
@@ -179,33 +218,32 @@
             >Auto close brackets</label
           >
           <span class="value">
-            <input
+            <md-checkbox
               id="showAutoCloseBrackets"
-              type="checkbox"
-              ?checked=${this.editPrefs?.auto_close_brackets}
+              ?checked=${!!this.editPrefs?.auto_close_brackets}
               @change=${this.handleAutoCloseBracketsChanged}
-            />
+            ></md-checkbox>
           </span>
         </section>
       </div>
     `;
   }
 
-  private readonly handleEditTabWidthChanged = () => {
+  private readonly handleEditTabWidthInput = () => {
     this.editPrefs!.tab_size = Number(this.editTabWidth!.value);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
     });
   };
 
-  private readonly handleEditLineLengthChanged = () => {
+  private readonly handleEditLineLengthInput = () => {
     this.editPrefs!.line_length = Number(this.editColumns!.value);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
     });
   };
 
-  private readonly handleEditIndentUnitChanged = () => {
+  private readonly handleEditIndentUnitInput = () => {
     this.editPrefs!.indent_unit = Number(this.editIndentUnit!.value);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts
index 04f6b64..49c56a9 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.ts
@@ -13,9 +13,9 @@
 } from '../../../test/test-utils';
 import {GrEditPreferences} from './gr-edit-preferences';
 import {EditPreferencesInfo} from '../../../types/common';
-import {IronInputElement} from '@polymer/iron-input';
 import {createDefaultEditPrefs} from '../../../constants/constants';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-edit-preferences tests', () => {
   let element: GrEditPreferences;
@@ -52,25 +52,43 @@
           <section>
             <label class="title" for="editTabWidth"> Tab width </label>
             <span class="value">
-              <iron-input>
-                <input id="editTabWidth" type="number" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="editTabWidth"
+                inputmode=""
+                step="1"
+                type="number"
+              >
+              </md-outlined-text-field>
             </span>
           </section>
           <section>
             <label class="title" for="editColumns"> Columns </label>
             <span class="value">
-              <iron-input>
-                <input id="editColumns" type="number" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="editColumns"
+                inputmode=""
+                step="1"
+                type="number"
+              >
+              </md-outlined-text-field>
             </span>
           </section>
           <section>
             <label class="title" for="editIndentUnit"> Indent unit </label>
             <span class="value">
-              <iron-input>
-                <input id="editIndentUnit" type="number" />
-              </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="editIndentUnit"
+                inputmode=""
+                step="1"
+                type="number"
+              >
+              </md-outlined-text-field>
             </span>
           </section>
           <section>
@@ -78,13 +96,14 @@
               Syntax highlighting
             </label>
             <span class="value">
-              <input checked="" id="editSyntaxHighlighting" type="checkbox" />
+              <md-checkbox checked="" id="editSyntaxHighlighting">
+              </md-checkbox>
             </span>
           </section>
           <section>
             <label class="title" for="editShowTabs"> Show tabs </label>
             <span class="value">
-              <input checked="" id="editShowTabs" type="checkbox" />
+              <md-checkbox checked="" id="editShowTabs"> </md-checkbox>
             </span>
           </section>
           <section>
@@ -92,11 +111,8 @@
               Show trailing whitespace
             </label>
             <span class="value">
-              <input
-                checked=""
-                id="editShowTrailingWhitespaceInput"
-                type="checkbox"
-              />
+              <md-checkbox checked="" id="editShowTrailingWhitespaceInput">
+              </md-checkbox>
             </span>
           </section>
           <section>
@@ -104,7 +120,7 @@
               Match brackets
             </label>
             <span class="value">
-              <input checked="" id="showMatchBrackets" type="checkbox" />
+              <md-checkbox checked="" id="showMatchBrackets"> </md-checkbox>
             </span>
           </section>
           <section>
@@ -112,7 +128,7 @@
               Line wrapping
             </label>
             <span class="value">
-              <input id="editShowLineWrapping" type="checkbox" />
+              <md-checkbox id="editShowLineWrapping"> </md-checkbox>
             </span>
           </section>
           <section>
@@ -120,7 +136,7 @@
               Indent with tabs
             </label>
             <span class="value">
-              <input id="showIndentWithTabs" type="checkbox" />
+              <md-checkbox id="showIndentWithTabs"> </md-checkbox>
             </span>
           </section>
           <section>
@@ -128,7 +144,7 @@
               Auto close brackets
             </label>
             <span class="value">
-              <input id="showAutoCloseBrackets" type="checkbox" />
+              <md-checkbox id="showAutoCloseBrackets"> </md-checkbox>
             </span>
           </section>
         </div>
@@ -139,16 +155,16 @@
   test('input values match preferences', () => {
     // Rendered with the expected preferences selected.
     const tabWidthInput = valueOf('Tab width', 'editPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(tabWidthInput.bindValue, `${editPreferences.tab_size}`);
+      .firstElementChild as MdOutlinedTextField;
+    assert.equal(tabWidthInput.value, `${editPreferences.tab_size}`);
 
     const columnsInput = valueOf('Columns', 'editPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(columnsInput.bindValue, `${editPreferences.line_length}`);
+      .firstElementChild as MdOutlinedTextField;
+    assert.equal(columnsInput.value, `${editPreferences.line_length}`);
 
     const indentInput = valueOf('Indent unit', 'editPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(indentInput.bindValue, `${editPreferences.indent_unit}`);
+      .firstElementChild as MdOutlinedTextField;
+    assert.equal(indentInput.value, `${editPreferences.indent_unit}`);
 
     const syntaxInput = valueOf('Syntax highlighting', 'editPreferences')
       .firstElementChild as HTMLInputElement;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
index 2220d2f..a523c20 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
@@ -6,7 +6,7 @@
 import '../../shared/gr-button/gr-button';
 import {EmailInfo} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
@@ -16,6 +16,9 @@
 import {userModelToken} from '../../../models/user/user-model';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
+import '@material/web/radio/radio';
+import {MdRadio} from '@material/web/radio/radio';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 @customElement('gr-email-editor')
 export class GrEmailEditor extends LitElement {
@@ -51,6 +54,7 @@
     return [
       sharedStyles,
       grFormStyles,
+      materialStyles,
       css`
         th {
           color: var(--deemphasized-text-color);
@@ -65,16 +69,9 @@
           width: 6em;
         }
         #emailTable .preferredControl {
-          cursor: pointer;
           height: auto;
           text-align: center;
         }
-        #emailTable .preferredControl .preferredRadio {
-          height: auto;
-        }
-        .preferredControl:hover {
-          outline: 1px solid var(--border-color);
-        }
       `,
     ];
   }
@@ -99,17 +96,15 @@
   private renderEmail(email: EmailInfo, index: number) {
     return html`<tr>
       <td class="emailColumn">${email.email}</td>
-      <td class="preferredControl" @click=${this.handlePreferredControlClick}>
-        <!-- We have to use \`.checked\` rather then \`?checked\` as there
-              appears to be an issue when deleting, checked doesn't work correctly. -->
-        <input
+      <td class="preferredControl">
+        <md-radio
           class="preferredRadio"
-          type="radio"
           name="preferred"
           .value=${email.email}
-          .checked=${email.preferred}
+          ?checked=${!!email.preferred}
           @change=${this.handlePreferredChange}
-        />
+        >
+        </md-radio>
       </td>
       <td>
         <gr-button
@@ -158,18 +153,8 @@
     this.setHasUnsavedChanges();
   }
 
-  private handlePreferredControlClick(e: Event) {
-    if (
-      e.target instanceof HTMLElement &&
-      e.target.classList.contains('preferredControl') &&
-      e.target.firstElementChild instanceof HTMLInputElement
-    ) {
-      e.target.firstElementChild.click();
-    }
-  }
-
   private handlePreferredChange(e: Event) {
-    if (!(e.target instanceof HTMLInputElement)) return;
+    if (!(e.target instanceof MdRadio)) return;
     const preferred = e.target.value;
     for (let i = 0; i < this.emails.length; i++) {
       if (preferred === this.emails[i].email) {
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
index fbcd494..7e090fb 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
@@ -8,8 +8,9 @@
 import './gr-email-editor';
 import {GrEmailEditor} from './gr-email-editor';
 import {spyRestApi, stubRestApi} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {EmailAddress} from '../../../api/rest-api';
+import {MdRadio} from '@material/web/radio/radio';
 
 suite('gr-email-editor tests', () => {
   let element: GrEmailEditor;
@@ -49,12 +50,8 @@
             <tr>
               <td class="emailColumn">email@one.com</td>
               <td class="preferredControl">
-                <input
-                  class="preferredRadio"
-                  name="preferred"
-                  type="radio"
-                  value="email@one.com"
-                />
+                <md-radio class="preferredRadio" name="preferred" tabindex="-1">
+                </md-radio>
               </td>
               <td>
                 <gr-button
@@ -70,12 +67,13 @@
             <tr>
               <td class="emailColumn">email@two.com</td>
               <td class="preferredControl">
-                <input
+                <md-radio
+                  checked=""
                   class="preferredRadio"
                   name="preferred"
-                  type="radio"
-                  value="email@two.com"
-                />
+                  tabindex="0"
+                >
+                </md-radio>
               </td>
               <td>
                 <gr-button
@@ -92,12 +90,8 @@
             <tr>
               <td class="emailColumn">email@three.com</td>
               <td class="preferredControl">
-                <input
-                  class="preferredRadio"
-                  name="preferred"
-                  type="radio"
-                  value="email@three.com"
-                />
+                <md-radio class="preferredRadio" name="preferred" tabindex="-1">
+                </md-radio>
               </td>
               <td>
                 <gr-button
@@ -129,25 +123,19 @@
 
     assert.equal(rows.length, 3);
 
-    assert.isFalse(
-      (rows[0].querySelector('input[type=radio]') as HTMLInputElement).checked
-    );
+    assert.isFalse((rows[0].querySelector('md-radio') as MdRadio).checked);
     assert.isNotOk(rows[0].querySelector('gr-button')!.disabled);
 
-    assert.isTrue(
-      (rows[1].querySelector('input[type=radio]') as HTMLInputElement).checked
-    );
+    assert.isTrue((rows[1].querySelector('md-radio') as MdRadio).checked);
     assert.isOk(rows[1].querySelector('gr-button')!.disabled);
 
-    assert.isFalse(
-      (rows[2].querySelector('input[type=radio]') as HTMLInputElement).checked
-    );
+    assert.isFalse((rows[2].querySelector('md-radio') as MdRadio).checked);
     assert.isNotOk(rows[2].querySelector('gr-button')!.disabled);
 
     assert.isFalse(hasUnsavedChangesSpy.called);
   });
 
-  test('edit preferred', () => {
+  test('edit preferred', async () => {
     const hasUnsavedChangesSpy = sinon.spy();
     element.addEventListener(
       'has-unsaved-changes-changed',
@@ -156,7 +144,8 @@
 
     const radios = element
       .shadowRoot!.querySelector('table')!
-      .querySelectorAll<HTMLInputElement>('input[type=radio]');
+      .querySelectorAll<HTMLInputElement>('md-radio');
+    await element.updateComplete;
 
     assert.isFalse(hasUnsavedChangesSpy.called);
     assert.isNotOk(element.newPreferred);
@@ -168,6 +157,8 @@
 
     radios[0].click();
 
+    await element.updateComplete;
+
     assert.isTrue(hasUnsavedChangesSpy.called);
     assert.isOk(element.newPreferred);
     assert.equal(element.emailsToRemove.length, 0);
@@ -224,7 +215,8 @@
 
     // Delete the first email and set the last as preferred.
     rows[0].querySelector('gr-button')!.click();
-    rows[2].querySelector<HTMLInputElement>('input[type=radio]')!.click();
+    rows[2].querySelector<HTMLInputElement>('md-radio')!.click();
+    await element.updateComplete;
 
     assert.isTrue(hasUnsavedChangesSpy.called);
     assert.isTrue(hasUnsavedChangesSpy.lastCall.args[0].detail.value);
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
index 731961e..13e0464 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.ts
@@ -3,22 +3,21 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {GpgKeyInfo, GpgKeyId} from '../../../types/common';
+import {GpgKeyId, GpgKeyInfo} from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
 import {getAppContext} from '../../../services/app-context';
 import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {assertIsDefined} from '../../../utils/common-util';
-import {BindValueChangeEvent} from '../../../types/events';
 import {fire} from '../../../utils/event-util';
 import {modalStyles} from '../../../styles/gr-modal-styles';
 import {formStyles} from '../../../styles/form-styles';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -31,7 +30,7 @@
 
   @query('#addButton') addButton?: GrButton;
 
-  @query('#newKey') newKeyTextarea?: IronAutogrowTextareaElement;
+  @query('#newKey') newKeyTextarea?: GrAutogrowTextarea;
 
   @property({type: Boolean})
   hasUnsavedChanges = false;
@@ -75,8 +74,22 @@
         #existing {
           margin-bottom: var(--spacing-l);
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           background-color: var(--view-background-color);
+          position: relative;
+          height: auto;
+          min-height: 4em;
+          --gr-autogrow-textarea-border-width: 0px;
+          --gr-autogrow-textarea-border-color: var(--border-color);
+          --input-field-bg: var(--view-background-color);
+          --input-field-disabled-bg: var(--view-background-color);
+          --secondary-bg-color: var(--background-color-secondary);
+          --text-default: var(--primary-text-color);
+          --text-disabled: var(--deemphasized-text-color);
+          --text-secondary: var(--deemphasized-text-color);
+        }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
         }
       `,
     ];
@@ -128,14 +141,13 @@
           <section>
             <span class="title">New GPG key</span>
             <span class="value">
-              <iron-autogrow-textarea
+              <gr-autogrow-textarea
                 id="newKey"
                 autocomplete="on"
-                .bindValue=${this.newKey}
-                @bind-value-changed=${(e: BindValueChangeEvent) =>
-                  this.handleNewKeyChanged(e)}
+                .value=${this.newKey}
+                @input=${(e: InputEvent) => this.handleNewKeyChanged(e)}
                 placeholder="New GPG Key"
-              ></iron-autogrow-textarea>
+              ></gr-autogrow-textarea>
             </span>
           </section>
           <gr-button
@@ -208,8 +220,9 @@
     this.viewKeyModal?.showModal();
   }
 
-  private handleNewKeyChanged(e: BindValueChangeEvent) {
-    this.newKey = e.detail.value ?? '';
+  private handleNewKeyChanged(e: InputEvent) {
+    const rawValue = (e.target as GrAutogrowTextarea).value ?? '';
+    this.newKey = rawValue;
   }
 
   private handleDeleteKey(index: number) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
index fc502f7..0455eab 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.ts
@@ -20,7 +20,7 @@
   OpenPgpUserIds,
 } from '../../../api/rest-api';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-gpg-editor tests', () => {
   let element: GrGpgEditor;
@@ -170,13 +170,12 @@
           <section>
             <span class="title"> New GPG key </span>
             <span class="value">
-              <iron-autogrow-textarea
-                aria-disabled="false"
+              <gr-autogrow-textarea
                 autocomplete="on"
                 id="newKey"
                 placeholder="New GPG Key"
               >
-              </iron-autogrow-textarea>
+              </gr-autogrow-textarea>
             </span>
           </section>
           <gr-button
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
index bfff989..d02ec39 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
@@ -3,11 +3,11 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {GroupInfo, GroupId} from '../../../types/common';
+import {GroupId, GroupInfo} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, state} from 'lit/decorators.js';
 import {createGroupUrl} from '../../../models/views/group';
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
index 08ce11c..01280a0 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
@@ -8,7 +8,7 @@
 import {GrGroupList} from './gr-group-list';
 import {GroupId, GroupInfo, GroupName} from '../../../types/common';
 import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-group-list tests', () => {
   let element: GrGroupList;
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
deleted file mode 100644
index cbc2fc4..0000000
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../shared/gr-button/gr-button';
-import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {getAppContext} from '../../../services/app-context';
-import {grFormStyles} from '../../../styles/gr-form-styles';
-import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
-import {customElement, query, state} from 'lit/decorators.js';
-import {modalStyles} from '../../../styles/gr-modal-styles';
-import {resolve} from '../../../models/dependency';
-import {configModelToken} from '../../../models/config/config-model';
-import {userModelToken} from '../../../models/user/user-model';
-import {subscribe} from '../../lit/subscription-controller';
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-http-password': GrHttpPassword;
-  }
-}
-
-@customElement('gr-http-password')
-export class GrHttpPassword extends LitElement {
-  @query('#generatedPasswordModal')
-  generatedPasswordModal?: HTMLDialogElement;
-
-  @state()
-  username?: string;
-
-  @state()
-  generatedPassword?: string;
-
-  @state()
-  status?: string;
-
-  @state()
-  passwordUrl: string | null = null;
-
-  private readonly restApiService = getAppContext().restApiService;
-
-  // Private but used in test
-  readonly getConfigModel = resolve(this, configModelToken);
-
-  // Private but used in test
-  readonly getUserModel = resolve(this, userModelToken);
-
-  constructor() {
-    super();
-    subscribe(
-      this,
-      () => this.getConfigModel().serverConfig$,
-      info => {
-        if (info) {
-          this.passwordUrl = info.auth.http_password_url || null;
-        } else {
-          this.passwordUrl = null;
-        }
-      }
-    );
-    subscribe(
-      this,
-      () => this.getUserModel().account$,
-      account => {
-        if (account) {
-          this.username = account.username;
-        }
-      }
-    );
-  }
-
-  static override get styles() {
-    return [
-      sharedStyles,
-      grFormStyles,
-      modalStyles,
-      css`
-        .password {
-          font-family: var(--monospace-font-family);
-          font-size: var(--font-size-mono);
-          line-height: var(--line-height-mono);
-        }
-        #generatedPasswordModal {
-          padding: var(--spacing-xxl);
-          width: 50em;
-        }
-        #generatedPasswordDisplay {
-          margin: var(--spacing-l) 0;
-        }
-        #generatedPasswordDisplay .title {
-          width: unset;
-        }
-        #generatedPasswordDisplay .value {
-          font-family: var(--monospace-font-family);
-          font-size: var(--font-size-mono);
-          line-height: var(--line-height-mono);
-        }
-        #passwordWarning {
-          font-style: italic;
-          text-align: center;
-        }
-        .closeButton {
-          bottom: 2em;
-          position: absolute;
-          right: 2em;
-        }
-      `,
-    ];
-  }
-
-  override render() {
-    return html` <div class="gr-form-styles">
-        <div ?hidden=${!!this.passwordUrl}>
-          <section>
-            <span class="title">Username</span>
-            <span class="value">${this.username ?? ''}</span>
-          </section>
-          <gr-button id="generateButton" @click=${this._handleGenerateTap}
-            >Generate New Password</gr-button
-          >
-        </div>
-        <span ?hidden=${!this.passwordUrl}>
-          <a
-            href=${this.passwordUrl!}
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            Obtain password</a
-          >
-          (opens in a new tab)
-        </span>
-      </div>
-      <dialog
-        tabindex="-1"
-        id="generatedPasswordModal"
-        @closed=${this._generatedPasswordModalClosed}
-      >
-        <div class="gr-form-styles">
-          <section id="generatedPasswordDisplay">
-            <span class="title">New Password:</span>
-            <span class="value">${this.status || this.generatedPassword}</span>
-            <gr-copy-clipboard
-              hasTooltip=""
-              buttonTitle="Copy password to clipboard"
-              hideInput=""
-              .text=${this.status ? '' : this.generatedPassword}
-            >
-            </gr-copy-clipboard>
-          </section>
-          <section id="passwordWarning">
-            This password will not be displayed again.<br />
-            If you lose it, you will need to generate a new one.
-          </section>
-          <gr-button link="" class="closeButton" @click=${this._closeModal}
-            >Close</gr-button
-          >
-        </div>
-      </dialog>`;
-  }
-
-  _handleGenerateTap() {
-    this.status = 'Generating...';
-    this.generatedPasswordModal?.showModal();
-    this.restApiService.generateAccountHttpPassword().then(newPassword => {
-      if (newPassword) {
-        this.generatedPassword = newPassword;
-        this.status = undefined;
-      } else {
-        this.status = 'Failed to generate';
-      }
-    });
-  }
-
-  _closeModal() {
-    this.generatedPasswordModal?.close();
-  }
-
-  _generatedPasswordModalClosed() {
-    this.status = undefined;
-    this.generatedPassword = '';
-  }
-}
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
deleted file mode 100644
index 7bca385..0000000
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import './gr-http-password';
-import {GrHttpPassword} from './gr-http-password';
-import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
-import {
-  createAccountDetailWithId,
-  createServerInfo,
-} from '../../../test/test-data-generators';
-import {AccountDetailInfo, ServerInfo} from '../../../types/common';
-import {queryAndAssert} from '../../../test/test-utils';
-import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert, waitUntil} from '@open-wc/testing';
-
-suite('gr-http-password tests', () => {
-  let element: GrHttpPassword;
-  let account: AccountDetailInfo;
-  let config: ServerInfo;
-
-  setup(async () => {
-    account = {...createAccountDetailWithId(), username: 'user name'};
-    config = createServerInfo();
-
-    stubRestApi('getAccount').returns(Promise.resolve(account));
-    stubRestApi('getConfig').returns(Promise.resolve(config));
-
-    element = await fixture(html`<gr-http-password></gr-http-password>`);
-    await waitUntil(
-      () => element.getUserModel().getState().account === account
-    );
-    await waitUntil(
-      () => element.getConfigModel().getState().serverConfig === config
-    );
-    await waitEventLoop();
-  });
-
-  test('renders', () => {
-    assert.shadowDom.equal(
-      element,
-      /* HTML */ `
-        <div class="gr-form-styles">
-          <div>
-            <section>
-              <span class="title"> Username </span>
-              <span class="value"> user name </span>
-            </section>
-            <gr-button
-              aria-disabled="false"
-              id="generateButton"
-              role="button"
-              tabindex="0"
-            >
-              Generate New Password
-            </gr-button>
-          </div>
-          <span hidden="">
-            <a href="" target="_blank" rel="noopener noreferrer">
-              Obtain password
-            </a>
-            (opens in a new tab)
-          </span>
-        </div>
-        <dialog tabindex="-1" id="generatedPasswordModal">
-          <div class="gr-form-styles">
-            <section id="generatedPasswordDisplay">
-              <span class="title"> New Password: </span>
-              <span class="value"> </span>
-              <gr-copy-clipboard
-                buttontitle="Copy password to clipboard"
-                hastooltip=""
-                hideinput=""
-              >
-              </gr-copy-clipboard>
-            </section>
-            <section id="passwordWarning">
-              This password will not be displayed again.
-              <br />
-              If you lose it, you will need to generate a new one.
-            </section>
-            <gr-button
-              aria-disabled="false"
-              class="closeButton"
-              link=""
-              role="button"
-              tabindex="0"
-            >
-              Close
-            </gr-button>
-          </div>
-        </dialog>
-      `
-    );
-  });
-
-  test('generate password', () => {
-    const button = queryAndAssert<GrButton>(element, '#generateButton');
-    const nextPassword = 'the new password';
-    let generateResolve: (value: string | PromiseLike<string>) => void;
-    const generateStub = stubRestApi('generateAccountHttpPassword').callsFake(
-      () =>
-        new Promise(resolve => {
-          generateResolve = resolve;
-        })
-    );
-
-    assert.isNotOk(element.generatedPassword);
-
-    button.click();
-
-    assert.isTrue(generateStub.called);
-    assert.equal(element.status, 'Generating...');
-
-    generateStub.lastCall.returnValue.then(() => {
-      generateResolve(nextPassword);
-      assert.equal(element.generatedPassword, nextPassword);
-    });
-  });
-
-  test('without http_password_url', () => {
-    assert.isNull(element.passwordUrl);
-  });
-
-  test('with http_password_url', async () => {
-    config.auth.http_password_url = 'http://example.com/';
-    element.passwordUrl = config.auth.http_password_url;
-    await element.updateComplete;
-    assert.isNotNull(element.passwordUrl);
-    assert.equal(element.passwordUrl, config.auth.http_password_url);
-  });
-});
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
index c4f7bca..ea5dc74 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
@@ -9,7 +9,7 @@
 import {AccountExternalIdInfo, ServerInfo} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {AuthType} from '../../../constants/constants';
-import {LitElement, css, html, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
@@ -50,7 +50,7 @@
         tr td.statusColumn,
         tr td.emailAddressColumn,
         tr td.identityColumn {
-          word-break: break-word;
+          overflow-wrap: anywhere;
         }
         tr td.emailAddressColumn,
         tr td.identityColumn {
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
index e96fa39..068fd48 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
@@ -13,7 +13,7 @@
 import {createServerInfo} from '../../../test/test-data-generators';
 import {queryAll, queryAndAssert} from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-identities tests', () => {
   let element: GrIdentities;
@@ -123,7 +123,7 @@
 
     assert.equal(rows.length, 2);
 
-    const nameCells = rows.map(row => queryAll(row, 'td')[1]!.textContent);
+    const nameCells = rows.map(row => queryAll(row, 'td')[1].textContent);
 
     assert.equal(nameCells[0]!, 'gerrit@example.com');
     assert.equal(nameCells[1]!, 'gerrit2@example.com');
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index 84ce3d49..55276dc 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -3,36 +3,43 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
+
 import '../../shared/gr-button/gr-button';
 import {PreferencesInfo, TopMenuItemInfo} from '../../../types/common';
 import {css, html, LitElement} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
-import {state, customElement} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
+import {customElement, state} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {deepEqual} from '../../../utils/deep-util';
 import {createDefaultPreferences} from '../../../constants/constants';
 import {fontStyles} from '../../../styles/gr-font-styles';
-import {classMap} from 'lit/directives/class-map.js';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {classMap} from 'lit/directives/class-map.js';
 import {userModelToken} from '../../../models/user/user-model';
 import {resolve} from '../../../models/dependency';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
+import {when} from 'lit/directives/when.js';
 
 @customElement('gr-menu-editor')
 export class GrMenuEditor extends LitElement {
-  @state()
-  menuItems: TopMenuItemInfo[] = [];
+  @state() menuItems: TopMenuItemInfo[] = [];
 
-  @state()
-  originalPrefs: PreferencesInfo = createDefaultPreferences();
+  @state() originalPrefs: PreferencesInfo = createDefaultPreferences();
 
-  @state()
-  newName = '';
+  @state() newName = '';
 
-  @state()
-  newUrl = '';
+  @state() newUrl = '';
+
+  @state() newTarget = false;
+
+  @state() editingIndex: number | null = null;
+
+  @state() editingField: 'name' | 'url' | null = null;
+
+  @state() private editingBackup: TopMenuItemInfo | null = null;
 
   private readonly getUserModel = resolve(this, userModelToken);
 
@@ -50,6 +57,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       grFormStyles,
       sharedStyles,
       fontStyles,
@@ -66,12 +74,52 @@
         tbody tr:last-of-type td .moveDownButton {
           display: none;
         }
-        td.urlCell {
-          word-break: break-word;
-        }
         .newUrlInput {
           min-width: 23em;
         }
+        th {
+          white-space: nowrap;
+        }
+        tfoot th:nth-child(1),
+        tfoot th:nth-child(2),
+        tfoot th:nth-child(3) {
+          padding-right: 1em;
+        }
+        table {
+          margin-bottom: 8px;
+        }
+        .editNameInput,
+        .editUrlInput,
+        #cancelBtn,
+        #editBtn {
+          vertical-align: middle;
+        }
+        .editRow {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+        }
+        .editNameInput,
+        .editUrlInput {
+          flex: 1;
+        }
+        td.nameCell .displayRow {
+          min-width: 11em;
+          width: auto;
+        }
+        td.nameCell,
+        td.urlCell {
+          padding-right: 1em;
+        }
+        .displayRow {
+          display: flex;
+          align-items: flex-start;
+          gap: 4px;
+        }
+        .displayRow .text {
+          flex: 1;
+          overflow-wrap: anywhere;
+        }
       `,
     ];
   }
@@ -91,6 +139,10 @@
               <tr>
                 <th>Name</th>
                 <th>URL</th>
+                <th>New Tab</th>
+                <th></th>
+                <th></th>
+                <th></th>
               </tr>
             </thead>
             <tbody>
@@ -102,22 +154,90 @@
               ${this.renderFooterRow()}
             </tfoot>
           </table>
-          <gr-button id="save" @click=${this.handleSave} ?disabled=${unchanged}
-            >Save Changes</gr-button
-          >
-          <gr-button id="reset" link @click=${this.handleReset}
-            >Reset</gr-button
-          >
+          <gr-button id="save" @click=${this.handleSave} ?disabled=${unchanged}>
+            Save Changes
+          </gr-button>
+          <gr-button id="reset" link @click=${this.handleReset}>
+            Reset
+          </gr-button>
         </fieldset>
       </div>
     `;
   }
 
   private renderMenuItemRow(item: TopMenuItemInfo, index: number) {
+    const isEditingName =
+      this.editingIndex === index && this.editingField === 'name';
+    const isEditingUrl =
+      this.editingIndex === index && this.editingField === 'url';
+
     return html`
       <tr>
-        <td>${item.name}</td>
-        <td class="urlCell">${item.url}</td>
+        <td class="nameCell">
+          ${when(
+            isEditingName,
+            () => html`
+              <div class="editRow">
+                <md-outlined-text-field
+                  class="editNameInput showBlueFocusBorder"
+                  .value=${item.name}
+                  @input=${(e: InputEvent) => this.handleNameChanged(e, index)}
+                ></md-outlined-text-field>
+                <gr-button id="cancelBtn" link @click=${this.cancelEdit}>
+                  <gr-icon icon="cancel"></gr-icon>
+                </gr-button>
+              </div>
+            `,
+            () => html`
+              <div class="displayRow">
+                <span class="text">${item.name}</span>
+                <gr-button
+                  id="editBtn"
+                  link
+                  @click=${() => this.startEdit(index, 'name')}
+                >
+                  <gr-icon icon="edit" filled small></gr-icon>
+                </gr-button>
+              </div>
+            `
+          )}
+        </td>
+
+        <td class="urlCell">
+          ${when(
+            isEditingUrl,
+            () => html`
+              <div class="editRow">
+                <md-outlined-text-field
+                  class="editUrlInput showBlueFocusBorder"
+                  .value=${item.url}
+                  @input=${(e: InputEvent) => this.handleUrlChanged(e, index)}
+                ></md-outlined-text-field>
+                <gr-button id="cancelBtn" link @click=${this.cancelEdit}>
+                  <gr-icon icon="cancel"></gr-icon>
+                </gr-button>
+              </div>
+            `,
+            () => html`
+              <div class="displayRow">
+                <span class="text">${item.url}</span>
+                <gr-button
+                  id="editBtn"
+                  link
+                  @click=${() => this.startEdit(index, 'url')}
+                >
+                  <gr-icon icon="edit" filled small></gr-icon>
+                </gr-button>
+              </div>
+            `
+          )}
+        </td>
+        <td>
+          <md-checkbox
+            ?checked=${item.target === '_blank'}
+            @change=${(e: Event) => this.handleCheckboxChange(e, index)}
+          ></md-checkbox>
+        </td>
         <td class="buttonColumn">
           <gr-button
             link
@@ -140,10 +260,7 @@
           <gr-button
             link
             data-index=${index}
-            @click=${() => {
-              this.menuItems.splice(index, 1);
-              this.requestUpdate('menuItems');
-            }}
+            @click=${() => this.deleteItem(index)}
             class="remove-button"
             >Delete</gr-button
           >
@@ -156,32 +273,30 @@
     return html`
       <tr>
         <th>
-          <iron-input
-            .bindValue=${this.newName}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
-              this.newName = e.detail.value ?? '';
-            }}
-          >
-            <input
-              is="iron-input"
-              placeholder="New Title"
-              @keydown=${this.handleInputKeydown}
-            />
-          </iron-input>
+          <md-outlined-text-field
+            class="showBlueFocusBorder"
+            placeholder="New Title"
+            .value=${this.newName}
+            @input=${(e: InputEvent) =>
+              (this.newName = (e.target as HTMLInputElement).value)}
+            @keydown=${this.handleInputKeydown}
+          ></md-outlined-text-field>
         </th>
         <th>
-          <iron-input
-            .bindValue=${this.newUrl}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
-              this.newUrl = e.detail.value ?? '';
-            }}
-          >
-            <input
-              class="newUrlInput"
-              placeholder="New URL"
-              @keydown=${this.handleInputKeydown}
-            />
-          </iron-input>
+          <md-outlined-text-field
+            class="newUrlInput showBlueFocusBorder"
+            placeholder="New URL"
+            .value=${this.newUrl}
+            @input=${(e: InputEvent) =>
+              (this.newUrl = (e.target as HTMLInputElement).value)}
+            @keydown=${this.handleInputKeydown}
+          ></md-outlined-text-field>
+        </th>
+        <th>
+          <md-checkbox
+            ?checked=${this.newTarget}
+            @change=${() => (this.newTarget = !this.newTarget)}
+          ></md-checkbox>
         </th>
         <th></th>
         <th></th>
@@ -198,39 +313,65 @@
     `;
   }
 
-  private handleSave() {
-    this.getUserModel().updatePreferences({
-      ...this.originalPrefs,
-      my: this.menuItems,
-    });
+  private startEdit(index: number, field: 'name' | 'url') {
+    this.editingIndex = null;
+    this.editingField = null;
+    this.editingBackup = null;
+
+    this.editingBackup = structuredClone(this.menuItems[index]);
+    this.editingIndex = index;
+    this.editingField = field;
   }
 
-  private handleReset() {
-    this.menuItems = [...this.originalPrefs.my];
+  private cancelEdit() {
+    if (this.editingIndex === null || !this.editingBackup) return;
+
+    this.menuItems = this.menuItems.map((item, i) =>
+      i === this.editingIndex ? this.editingBackup! : item
+    );
+
+    this.editingIndex = null;
+    this.editingField = null;
+    this.editingBackup = null;
+  }
+
+  private handleNameChanged(e: InputEvent, index: number) {
+    const value = (e.target as HTMLInputElement).value;
+
+    this.menuItems = this.menuItems.map((item, i) =>
+      i === index ? {...item, name: value} : item
+    );
+  }
+
+  private handleUrlChanged(e: InputEvent, index: number) {
+    const value = (e.target as HTMLInputElement).value;
+
+    this.menuItems = this.menuItems.map((item, i) =>
+      i === index ? {...item, url: value} : item
+    );
   }
 
   private swapItems(i: number, j: number) {
-    const max = this.menuItems.length - 1;
     if (i < 0 || j < 0) return;
-    if (i > max || j > max) return;
-    const x = this.menuItems[i];
-    this.menuItems[i] = this.menuItems[j];
-    this.menuItems[j] = x;
-    this.requestUpdate('menuItems');
+    if (i >= this.menuItems.length || j >= this.menuItems.length) return;
+
+    const updated = [...this.menuItems];
+    [updated[i], updated[j]] = [updated[j], updated[i]];
+    this.menuItems = updated;
+
+    if (this.editingIndex === i) {
+      this.editingIndex = j;
+    } else if (this.editingIndex === j) {
+      this.editingIndex = i;
+    }
   }
 
-  // visible for testing
-  handleAddButton() {
-    if (this.newName.length === 0 || this.newUrl.length === 0) return;
+  private deleteItem(index: number) {
+    this.menuItems = this.menuItems.filter((_, i) => i !== index);
 
-    this.menuItems.push({
-      name: this.newName,
-      url: this.newUrl,
-      target: '_blank',
-    });
-    this.newName = '';
-    this.newUrl = '';
-    this.requestUpdate('menuItems');
+    this.editingIndex = null;
+    this.editingField = null;
+    this.editingBackup = null;
   }
 
   private handleInputKeydown(e: KeyboardEvent) {
@@ -239,6 +380,59 @@
       this.handleAddButton();
     }
   }
+
+  private handleCheckboxChange(e: Event, index: number) {
+    const checked = (e.target as HTMLInputElement).checked;
+
+    this.menuItems = this.menuItems.map((item, i) => {
+      if (i !== index) return item;
+
+      if (checked) {
+        return {...item, target: '_blank'};
+      }
+
+      const newItem = {...item};
+      delete newItem.target;
+      return newItem;
+    });
+  }
+
+  // visible for testing
+  handleAddButton() {
+    if (!this.newName || !this.newUrl) return;
+
+    this.menuItems = [
+      ...this.menuItems,
+      {
+        name: this.newName,
+        url: this.newUrl,
+        target: this.newTarget ? '_blank' : undefined,
+      },
+    ];
+
+    this.newName = '';
+    this.newUrl = '';
+    this.newTarget = false;
+  }
+
+  private handleSave() {
+    this.getUserModel().updatePreferences({
+      ...this.originalPrefs,
+      my: this.menuItems,
+    });
+
+    this.editingIndex = null;
+    this.editingField = null;
+    this.editingBackup = null;
+  }
+
+  private handleReset() {
+    this.menuItems = [...this.originalPrefs.my];
+
+    this.editingIndex = null;
+    this.editingField = null;
+    this.editingBackup = null;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts
index f8d8229..fe0df92 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.ts
@@ -7,10 +7,10 @@
 import './gr-menu-editor';
 import {GrMenuEditor} from './gr-menu-editor';
 import {query, queryAndAssert, waitUntil} from '../../../test/test-utils';
-import {PaperButtonElement} from '@polymer/paper-button';
+import {MdTextButton} from '@material/web/button/text-button';
 import {TopMenuItemInfo} from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {createDefaultPreferences} from '../../../constants/constants';
 
 suite('gr-menu-editor tests', () => {
@@ -33,9 +33,9 @@
   function move(element: GrMenuEditor, index: number, direction: string) {
     const selector = `tr:nth-child(${index + 1}) .move${direction}Button`;
 
-    const button = query<PaperButtonElement>(
+    const button = query<MdTextButton>(
       query<HTMLElement>(query<HTMLTableElement>(element, 'tbody'), selector),
-      'paper-button'
+      'md-text-button'
     );
     button!.click();
   }
@@ -46,7 +46,7 @@
     );
     menu = [
       {url: '/first/url', name: 'first name', target: '_blank'},
-      {url: '/second/url', name: 'second name', target: '_blank'},
+      {url: '/second/url', name: 'second name'},
       {url: '/third/url', name: 'third name', target: '_blank'},
     ];
     element.originalPrefs = {...createDefaultPreferences(), my: menu};
@@ -66,12 +66,45 @@
                 <tr>
                   <th>Name</th>
                   <th>URL</th>
+                  <th>New Tab</th>
+                  <th></th>
+                  <th></th>
+                  <th></th>
                 </tr>
               </thead>
               <tbody>
                 <tr>
-                  <td>first name</td>
-                  <td class="urlCell">/first/url</td>
+                  <td class="nameCell">
+                    <div class="displayRow">
+                      <span class="text"> first name </span>
+                      <gr-button
+                        aria-disabled="false"
+                        id="editBtn"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        <gr-icon filled="" icon="edit" small=""> </gr-icon>
+                      </gr-button>
+                    </div>
+                  </td>
+                  <td class="urlCell">
+                    <div class="displayRow">
+                      <span class="text"> /first/url </span>
+                      <gr-button
+                        aria-disabled="false"
+                        id="editBtn"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        <gr-icon filled="" icon="edit" small=""> </gr-icon>
+                      </gr-button>
+                    </div>
+                  </td>
+                  <td>
+                    <md-checkbox checked=""> </md-checkbox>
+                  </td>
                   <td class="buttonColumn">
                     <gr-button
                       aria-disabled="false"
@@ -110,8 +143,37 @@
                   </td>
                 </tr>
                 <tr>
-                  <td>second name</td>
-                  <td class="urlCell">/second/url</td>
+                  <td class="nameCell">
+                    <div class="displayRow">
+                      <span class="text"> second name </span>
+                      <gr-button
+                        aria-disabled="false"
+                        id="editBtn"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        <gr-icon filled="" icon="edit" small=""> </gr-icon>
+                      </gr-button>
+                    </div>
+                  </td>
+                  <td class="urlCell">
+                    <div class="displayRow">
+                      <span class="text"> /second/url </span>
+                      <gr-button
+                        aria-disabled="false"
+                        id="editBtn"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        <gr-icon filled="" icon="edit" small=""> </gr-icon>
+                      </gr-button>
+                    </div>
+                  </td>
+                  <td>
+                    <md-checkbox> </md-checkbox>
+                  </td>
                   <td class="buttonColumn">
                     <gr-button
                       aria-disabled="false"
@@ -150,8 +212,37 @@
                   </td>
                 </tr>
                 <tr>
-                  <td>third name</td>
-                  <td class="urlCell">/third/url</td>
+                  <td class="nameCell">
+                    <div class="displayRow">
+                      <span class="text"> third name </span>
+                      <gr-button
+                        aria-disabled="false"
+                        id="editBtn"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        <gr-icon filled="" icon="edit" small=""> </gr-icon>
+                      </gr-button>
+                    </div>
+                  </td>
+                  <td class="urlCell">
+                    <div class="displayRow">
+                      <span class="text"> /third/url </span>
+                      <gr-button
+                        aria-disabled="false"
+                        id="editBtn"
+                        link=""
+                        role="button"
+                        tabindex="0"
+                      >
+                        <gr-icon filled="" icon="edit" small=""> </gr-icon>
+                      </gr-button>
+                    </div>
+                  </td>
+                  <td>
+                    <md-checkbox checked=""> </md-checkbox>
+                  </td>
                   <td class="buttonColumn">
                     <gr-button
                       aria-disabled="false"
@@ -193,14 +284,27 @@
               <tfoot>
                 <tr>
                   <th>
-                    <iron-input>
-                      <input is="iron-input" placeholder="New Title" />
-                    </iron-input>
+                    <md-outlined-text-field
+                      autocomplete=""
+                      class="showBlueFocusBorder"
+                      inputmode=""
+                      placeholder="New Title"
+                      type="text"
+                    >
+                    </md-outlined-text-field>
                   </th>
                   <th>
-                    <iron-input>
-                      <input class="newUrlInput" placeholder="New URL" />
-                    </iron-input>
+                    <md-outlined-text-field
+                      autocomplete=""
+                      class="newUrlInput showBlueFocusBorder"
+                      inputmode=""
+                      placeholder="New URL"
+                      type="text"
+                    >
+                    </md-outlined-text-field>
+                  </th>
+                  <th>
+                    <md-checkbox> </md-checkbox>
                   </th>
                   <th></th>
                   <th></th>
@@ -316,12 +420,14 @@
     assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
   });
 
-  test('move items up', () => {
+  test('move items up', async () => {
     assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
 
     // Move the last item up twice to be the first.
     move(element, 2, 'Up');
+    await element.updateComplete;
     move(element, 1, 'Up');
+    await element.updateComplete;
     assertMenuNamesEqual(element, ['third name', 'first name', 'second name']);
 
     // Moving the top item up is a no-op.
@@ -329,29 +435,31 @@
     assertMenuNamesEqual(element, ['third name', 'first name', 'second name']);
   });
 
-  test('remove item', () => {
+  test('remove item', async () => {
     assertMenuNamesEqual(element, ['first name', 'second name', 'third name']);
 
     // Tap the delete button for the middle item.
-    query<PaperButtonElement>(
+    query<MdTextButton>(
       query<HTMLElement>(
         query<HTMLTableElement>(element, 'tbody'),
         'tr:nth-child(2) .remove-button'
       ),
-      'paper-button'
+      'md-text-button'
     )!.click();
+    await element.updateComplete;
 
     assertMenuNamesEqual(element, ['first name', 'third name']);
 
     // Delete remaining items.
     for (let i = 0; i < 2; i++) {
-      query<PaperButtonElement>(
+      query<MdTextButton>(
         query<HTMLElement>(
           query<HTMLTableElement>(element, 'tbody'),
           'tr:first-child .remove-button'
         ),
-        'paper-button'
+        'md-text-button'
       )!.click();
+      await element.updateComplete;
     }
     assertMenuNamesEqual(element, []);
 
@@ -361,4 +469,109 @@
     element.handleAddButton();
     assertMenuNamesEqual(element, ['new name']);
   });
+
+  test('edit name', async () => {
+    const editBtn = queryAndAssert<GrButton>(
+      element,
+      'tbody tr:nth-child(1) td.nameCell #editBtn'
+    );
+    editBtn.click();
+    await element.updateComplete;
+
+    const input = queryAndAssert<HTMLInputElement>(
+      element,
+      'tbody tr:nth-child(1) .editNameInput'
+    );
+
+    input.value = 'updated name';
+    input.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+
+    assert.equal(element.menuItems[0].name, 'updated name');
+  });
+
+  test('edit url', async () => {
+    const editBtn = queryAndAssert<GrButton>(
+      element,
+      'tbody tr:nth-child(1) td.urlCell #editBtn'
+    );
+    editBtn.click();
+    await element.updateComplete;
+
+    const input = queryAndAssert<HTMLInputElement>(
+      element,
+      'tbody tr:nth-child(1) .editUrlInput'
+    );
+
+    input.value = '/updated/url';
+    input.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+
+    assert.equal(element.menuItems[0].url, '/updated/url');
+  });
+
+  test('cancel editing restores original value after change', async () => {
+    const originalName = element.menuItems[0].name;
+
+    queryAndAssert<GrButton>(
+      element,
+      'tbody tr:nth-child(1) td.nameCell #editBtn'
+    ).click();
+
+    await element.updateComplete;
+
+    const input = queryAndAssert<HTMLInputElement>(
+      element,
+      'tbody tr:nth-child(1) .editNameInput'
+    );
+
+    input.value = 'modified name';
+    input.dispatchEvent(new Event('input'));
+    await element.updateComplete;
+
+    assert.equal(element.menuItems[0].name, 'modified name');
+
+    queryAndAssert<GrButton>(
+      element,
+      'tbody tr:nth-child(1) #cancelBtn'
+    ).click();
+
+    await element.updateComplete;
+
+    assert.equal(element.menuItems[0].name, originalName);
+
+    assert.isNull(element.editingIndex);
+    assert.isNull(element.editingField);
+  });
+
+  test('checkbox toggle does not leave dirty state after reverting', async () => {
+    const saveButton = queryAndAssert<GrButton>(element, 'gr-button#save');
+
+    assert.deepEqual(element.menuItems, element.originalPrefs.my);
+    assert.isTrue(saveButton.hasAttribute('disabled'));
+
+    let checkbox = queryAndAssert<HTMLInputElement>(
+      element,
+      'tbody tr:nth-child(2) md-checkbox'
+    );
+
+    checkbox.click();
+    await element.updateComplete;
+
+    assert.notDeepEqual(element.menuItems, element.originalPrefs.my);
+
+    assert.isFalse(saveButton.hasAttribute('disabled'));
+
+    checkbox = queryAndAssert<HTMLInputElement>(
+      element,
+      'tbody tr:nth-child(2) md-checkbox'
+    );
+
+    checkbox.click();
+    await element.updateComplete;
+
+    assert.deepEqual(element.menuItems, element.originalPrefs.my);
+
+    assert.isTrue(saveButton.hasAttribute('disabled'));
+  });
 });
diff --git a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts
index f02ada7..203d8a2 100644
--- a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts
@@ -3,14 +3,12 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-select/gr-select';
 import {AccountDetailInfo, PreferencesInput} from '../../../types/common';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {convertToString} from '../../../utils/string-util';
 import {subscribe} from '../../lit/subscription-controller';
@@ -31,6 +29,11 @@
 import {configModelToken} from '../../../models/config/config-model';
 import {SuggestionsProvider} from '../../../api/suggestions';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
 
 /**
  * This provides an interface to show settings for a user profile
@@ -38,49 +41,31 @@
  */
 @customElement('gr-preferences')
 export class GrPreferences extends LitElement {
-  @query('#themeSelect') themeSelect!: HTMLInputElement;
-
-  @query('#changesPerPageSelect') changesPerPageSelect!: HTMLInputElement;
-
-  @query('#dateTimeFormatSelect') dateTimeFormatSelect!: HTMLInputElement;
-
-  @query('#timeFormatSelect') timeFormatSelect!: HTMLInputElement;
-
-  @query('#emailNotificationsSelect')
-  emailNotificationsSelect!: HTMLInputElement;
-
-  @query('#emailFormatSelect') emailFormatSelect!: HTMLInputElement;
-
   @query('#allowBrowserNotifications')
-  allowBrowserNotifications?: HTMLInputElement;
+  allowBrowserNotifications?: MdCheckbox;
 
   @query('#allowSuggestCodeWhileCommenting')
-  allowSuggestCodeWhileCommenting?: HTMLInputElement;
+  allowSuggestCodeWhileCommenting?: MdCheckbox;
 
   @query('#allowAiCommentAutocompletion')
-  allowAiCommentAutocompletion?: HTMLInputElement;
-
-  @query('#defaultBaseForMergesSelect')
-  defaultBaseForMergesSelect!: HTMLInputElement;
+  allowAiCommentAutocompletion?: MdCheckbox;
 
   @query('#relativeDateInChangeTable')
-  relativeDateInChangeTable!: HTMLInputElement;
+  relativeDateInChangeTable!: MdCheckbox;
 
-  @query('#diffViewSelect') diffViewSelect!: HTMLInputElement;
+  @query('#showSizeBarsInFileList') showSizeBarsInFileList!: MdCheckbox;
 
-  @query('#showSizeBarsInFileList') showSizeBarsInFileList!: HTMLInputElement;
+  @query('#publishCommentsOnPush') publishCommentsOnPush!: MdCheckbox;
 
-  @query('#publishCommentsOnPush') publishCommentsOnPush!: HTMLInputElement;
-
-  @query('#workInProgressByDefault') workInProgressByDefault!: HTMLInputElement;
+  @query('#workInProgressByDefault') workInProgressByDefault!: MdCheckbox;
 
   @query('#disableKeyboardShortcuts')
-  disableKeyboardShortcuts!: HTMLInputElement;
+  disableKeyboardShortcuts!: MdCheckbox;
 
   @query('#disableTokenHighlighting')
-  disableTokenHighlighting!: HTMLInputElement;
+  disableTokenHighlighting!: MdCheckbox;
 
-  @query('#insertSignedOff') insertSignedOff!: HTMLInputElement;
+  @query('#insertSignedOff') insertSignedOff!: MdCheckbox;
 
   @state() prefs?: PreferencesInput;
 
@@ -138,6 +123,7 @@
       sharedStyles,
       menuPageStyles,
       grFormStyles,
+      materialStyles,
       css`
         :host {
           border: none;
@@ -163,19 +149,24 @@
           <section>
             <label class="title" for="themeSelect">Theme</label>
             <span class="value">
-              <gr-select
-                .bindValue=${this.prefs?.theme ?? AppTheme.AUTO}
-                @change=${() => {
-                  this.prefs!.theme = this.themeSelect.value as AppTheme;
+              <md-outlined-select
+                value=${this.prefs?.theme ?? AppTheme.AUTO}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.theme = select.value as AppTheme;
                   this.requestUpdate();
                 }}
               >
-                <select id="themeSelect">
-                  <option value="AUTO">Auto (based on OS prefs)</option>
-                  <option value="LIGHT">Light</option>
-                  <option value="DARK">Dark</option>
-                </select>
-              </gr-select>
+                <md-select-option value="AUTO">
+                  <div slot="headline">Auto (based on OS prefs)</div>
+                </md-select-option>
+                <md-select-option value="LIGHT">
+                  <div slot="headline">Light</div>
+                </md-select-option>
+                <md-select-option value="DARK">
+                  <div slot="headline">Dark</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </section>
           <section>
@@ -183,22 +174,31 @@
               >Changes per page</label
             >
             <span class="value">
-              <gr-select
-                .bindValue=${convertToString(this.prefs?.changes_per_page)}
-                @change=${() => {
-                  this.prefs!.changes_per_page = Number(
-                    this.changesPerPageSelect.value
-                  ) as 10 | 25 | 50 | 100;
+              <md-outlined-select
+                value=${convertToString(this.prefs?.changes_per_page)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.changes_per_page = Number(select.value) as
+                    | 10
+                    | 25
+                    | 50
+                    | 100;
                   this.requestUpdate();
                 }}
               >
-                <select id="changesPerPageSelect">
-                  <option value="10">10 rows per page</option>
-                  <option value="25">25 rows per page</option>
-                  <option value="50">50 rows per page</option>
-                  <option value="100">100 rows per page</option>
-                </select>
-              </gr-select>
+                <md-select-option value="10">
+                  <div slot="headline">10 rows per page</div>
+                </md-select-option>
+                <md-select-option value="25">
+                  <div slot="headline">25 rows per page</div>
+                </md-select-option>
+                <md-select-option value="50">
+                  <div slot="headline">50 rows per page</div>
+                </md-select-option>
+                <md-select-option value="100">
+                  <div slot="headline">100 rows per page</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </section>
           <section>
@@ -206,36 +206,45 @@
               >Date/time format</label
             >
             <span class="value">
-              <gr-select
-                .bindValue=${convertToString(this.prefs?.date_format)}
-                @change=${() => {
-                  this.prefs!.date_format = this.dateTimeFormatSelect
-                    .value as DateFormat;
+              <md-outlined-select
+                value=${convertToString(this.prefs?.date_format)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.date_format = select.value as DateFormat;
                   this.requestUpdate();
                 }}
               >
-                <select id="dateTimeFormatSelect">
-                  <option value="STD">Jun 3 ; Jun 3, 2016</option>
-                  <option value="US">06/03 ; 06/03/16</option>
-                  <option value="ISO">06-03 ; 2016-06-03</option>
-                  <option value="EURO">3. Jun ; 03.06.2016</option>
-                  <option value="UK">03/06 ; 03/06/2016</option>
-                </select>
-              </gr-select>
-              <gr-select
-                .bindValue=${convertToString(this.prefs?.time_format)}
-                aria-label="Time Format"
-                @change=${() => {
-                  this.prefs!.time_format = this.timeFormatSelect
-                    .value as TimeFormat;
+                <md-select-option value="STD">
+                  <div slot="headline">Jun 3 ; Jun 3, 2016</div>
+                </md-select-option>
+                <md-select-option value="US">
+                  <div slot="headline">06/03 ; 06/03/16</div>
+                </md-select-option>
+                <md-select-option value="ISO">
+                  <div slot="headline">06-03 ; 2016-06-03</div>
+                </md-select-option>
+                <md-select-option value="EURO">
+                  <div slot="headline">3. Jun ; 03.06.2016</div>
+                </md-select-option>
+                <md-select-option value="UK">
+                  <div slot="headline">03/06 ; 03/06/2016</div>
+                </md-select-option>
+              </md-outlined-select>
+              <md-outlined-select
+                value=${convertToString(this.prefs?.time_format)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.time_format = select.value as TimeFormat;
                   this.requestUpdate();
                 }}
               >
-                <select id="timeFormatSelect">
-                  <option value="HHMM_12">4:10 PM</option>
-                  <option value="HHMM_24">16:10</option>
-                </select>
-              </gr-select>
+                <md-select-option value="HHMM_12">
+                  <div slot="headline">4:10 PM</div>
+                </md-select-option>
+                <md-select-option value="HHMM_24">
+                  <div slot="headline">16:10</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </section>
           <section>
@@ -243,41 +252,44 @@
               >Email notifications</label
             >
             <span class="value">
-              <gr-select
-                .bindValue=${convertToString(this.prefs?.email_strategy)}
-                @change=${() => {
-                  this.prefs!.email_strategy = this.emailNotificationsSelect
-                    .value as EmailStrategy;
+              <md-outlined-select
+                value=${convertToString(this.prefs?.email_strategy)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.email_strategy = select.value as EmailStrategy;
                   this.requestUpdate();
                 }}
               >
-                <select id="emailNotificationsSelect">
-                  <option value="CC_ON_OWN_COMMENTS">Every comment</option>
-                  <option value="ENABLED">Only comments left by others</option>
-                  <option value="ATTENTION_SET_ONLY">
-                    Only when I am in the attention set
-                  </option>
-                  <option value="DISABLED">None</option>
-                </select>
-              </gr-select>
+                <md-select-option value="CC_ON_OWN_COMMENTS">
+                  <div slot="headline">Every comment</div>
+                </md-select-option>
+                <md-select-option value="ENABLED">
+                  <div slot="headline">Only comments left by others</div>
+                </md-select-option>
+                <md-select-option value="ATTENTION_SET_ONLY">
+                  <div slot="headline">Only when I am in the attention set</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </section>
           <section>
             <label class="title" for="emailFormatSelect">Email format</label>
             <span class="value">
-              <gr-select
-                .bindValue=${convertToString(this.prefs?.email_format)}
-                @change=${() => {
-                  this.prefs!.email_format = this.emailFormatSelect
-                    .value as EmailFormat;
+              <md-outlined-select
+                value=${convertToString(this.prefs?.email_format)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.email_format = select.value as EmailFormat;
                   this.requestUpdate();
                 }}
               >
-                <select id="emailFormatSelect">
-                  <option value="HTML_PLAINTEXT">HTML and plaintext</option>
-                  <option value="PLAINTEXT">Plaintext only</option>
-                </select>
-              </gr-select>
+                <md-select-option value="HTML_PLAINTEXT">
+                  <div slot="headline">HTML and plaintext</div>
+                </md-select-option>
+                <md-select-option value="PLAINTEXT">
+                  <div slot="headline">Plaintext only</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </section>
           ${this.renderBrowserNotifications()}
@@ -289,34 +301,35 @@
               >Show Relative Dates In Changes Table</label
             >
             <span class="value">
-              <input
+              <md-checkbox
                 id="relativeDateInChangeTable"
-                type="checkbox"
-                ?checked=${this.prefs?.relative_date_in_change_table}
+                ?checked=${!!this.prefs?.relative_date_in_change_table}
                 @change=${() => {
                   this.prefs!.relative_date_in_change_table =
                     this.relativeDateInChangeTable.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
           <section>
             <span class="title">Diff view</span>
             <span class="value">
-              <gr-select
-                .bindValue=${convertToString(this.prefs?.diff_view)}
-                @change=${() => {
-                  this.prefs!.diff_view = this.diffViewSelect
-                    .value as DiffViewMode;
+              <md-outlined-select
+                value=${convertToString(this.prefs?.diff_view)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.prefs!.diff_view = select.value as DiffViewMode;
                   this.requestUpdate();
                 }}
               >
-                <select id="diffViewSelect">
-                  <option value="SIDE_BY_SIDE">Side by side</option>
-                  <option value="UNIFIED_DIFF">Unified diff</option>
-                </select>
-              </gr-select>
+                <md-select-option value="SIDE_BY_SIDE">
+                  <div slot="headline">Side by side</div>
+                </md-select-option>
+                <md-select-option value="UNIFIED_DIFF">
+                  <div slot="headline">Unified diff</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </section>
           <section>
@@ -324,16 +337,15 @@
               >Show size bars in file list</label
             >
             <span class="value">
-              <input
+              <md-checkbox
                 id="showSizeBarsInFileList"
-                type="checkbox"
-                ?checked=${this.prefs?.size_bar_in_change_table}
+                ?checked=${!!this.prefs?.size_bar_in_change_table}
                 @change=${() => {
                   this.prefs!.size_bar_in_change_table =
                     this.showSizeBarsInFileList.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
           <section>
@@ -341,16 +353,15 @@
               >Publish comments on push</label
             >
             <span class="value">
-              <input
+              <md-checkbox
                 id="publishCommentsOnPush"
-                type="checkbox"
-                ?checked=${this.prefs?.publish_comments_on_push}
+                ?checked=${!!this.prefs?.publish_comments_on_push}
                 @change=${() => {
                   this.prefs!.publish_comments_on_push =
                     this.publishCommentsOnPush.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
           <section>
@@ -358,16 +369,15 @@
               >Set new changes to "work in progress" by default</label
             >
             <span class="value">
-              <input
+              <md-checkbox
                 id="workInProgressByDefault"
-                type="checkbox"
-                ?checked=${this.prefs?.work_in_progress_by_default}
+                ?checked=${!!this.prefs?.work_in_progress_by_default}
                 @change=${() => {
                   this.prefs!.work_in_progress_by_default =
                     this.workInProgressByDefault.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
           <section>
@@ -375,16 +385,15 @@
               >Disable all keyboard shortcuts</label
             >
             <span class="value">
-              <input
+              <md-checkbox
                 id="disableKeyboardShortcuts"
-                type="checkbox"
-                ?checked=${this.prefs?.disable_keyboard_shortcuts}
+                ?checked=${!!this.prefs?.disable_keyboard_shortcuts}
                 @change=${() => {
                   this.prefs!.disable_keyboard_shortcuts =
                     this.disableKeyboardShortcuts.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
           <section>
@@ -392,16 +401,15 @@
               >Disable token highlighting on hover</label
             >
             <span class="value">
-              <input
+              <md-checkbox
                 id="disableTokenHighlighting"
-                type="checkbox"
-                ?checked=${this.prefs?.disable_token_highlighting}
+                ?checked=${!!this.prefs?.disable_token_highlighting}
                 @change=${() => {
                   this.prefs!.disable_token_highlighting =
                     this.disableTokenHighlighting.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
           <section>
@@ -409,15 +417,14 @@
               Insert Signed-off-by Footer For Inline Edit Changes
             </label>
             <span class="value">
-              <input
+              <md-checkbox
                 id="insertSignedOff"
-                type="checkbox"
-                ?checked=${this.prefs?.signed_off_by}
+                ?checked=${!!this.prefs?.signed_off_by}
                 @change=${() => {
                   this.prefs!.signed_off_by = this.insertSignedOff.checked;
                   this.requestUpdate();
                 }}
-              />
+              ></md-checkbox>
             </span>
           </section>
         </div>
@@ -460,16 +467,15 @@
         </a>
       </div>
       <span class="value">
-        <input
+        <md-checkbox
           id="allowBrowserNotifications"
-          type="checkbox"
-          ?checked=${this.prefs?.allow_browser_notifications}
+          ?checked=${!!this.prefs?.allow_browser_notifications}
           @change=${() => {
             this.prefs!.allow_browser_notifications =
               this.allowBrowserNotifications!.checked;
             this.requestUpdate();
           }}
-        />
+        ></md-checkbox>
       </span>
     </section>`;
   }
@@ -477,7 +483,14 @@
   // When the experiment is over, move this back to render(),
   // removing this function.
   private renderGenerateSuggestionWhenCommenting() {
-    if (!this.suggestionsProvider) return nothing;
+    if (
+      !this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2) ||
+      this.flagsService.isEnabled(
+        KnownExperimentId.ML_SUGGESTED_EDIT_UNCHECK_BY_DEFAULT
+      ) ||
+      !this.suggestionsProvider
+    )
+      return nothing;
     return html`
       <section id="allowSuggestCodeWhileCommentingSection">
         <div class="title">
@@ -497,16 +510,15 @@
           </a>
         </div>
         <span class="value">
-          <input
+          <md-checkbox
             id="allowSuggestCodeWhileCommenting"
-            type="checkbox"
-            ?checked=${this.prefs?.allow_suggest_code_while_commenting}
+            ?checked=${!!this.prefs?.allow_suggest_code_while_commenting}
             @change=${() => {
               this.prefs!.allow_suggest_code_while_commenting =
                 this.allowSuggestCodeWhileCommenting!.checked;
               this.requestUpdate();
             }}
-          />
+          ></md-checkbox>
         </span>
       </section>
     `;
@@ -515,11 +527,7 @@
   // When the experiment is over, move this back to render(),
   // removing this function.
   private renderAiCommentAutocompletion() {
-    if (
-      !this.flagsService.isEnabled(KnownExperimentId.COMMENT_AUTOCOMPLETION) ||
-      !this.suggestionsProvider
-    )
-      return nothing;
+    if (!this.suggestionsProvider) return nothing;
     return html`
       <section id="allowAiCommentAutocompletionSection">
         <div class="title">
@@ -528,16 +536,15 @@
           >
         </div>
         <span class="value">
-          <input
+          <md-checkbox
             id="allowAiCommentAutocompletion"
-            type="checkbox"
-            ?checked=${this.prefs?.allow_autocompleting_comments}
+            ?checked=${!!this.prefs?.allow_autocompleting_comments}
             @change=${() => {
               this.prefs!.allow_autocompleting_comments =
                 this.allowAiCommentAutocompletion!.checked;
               this.requestUpdate();
             }}
-          />
+          ></md-checkbox>
         </span>
       </section>
     `;
@@ -554,21 +561,23 @@
     //   <section>
     //     <span class="title">Default Base For Merges</span>
     //     <span class="value">
-    //       <gr-select
-    //         .bindValue=${convertToString(
-    //           this.prefs?.default_base_for_merges
-    //         )}
-    //         @change=${() => {
-    //           this.prefs!.default_base_for_merges = this
-    //             .defaultBaseForMergesSelect.value as DefaultBase;
+    //       <md-outlined-select
+    //         .value=${convertToString(
+    //            this.prefs?.default_base_for_merges
+    //          )}
+    //         @change=${(e: Event) => {
+    //           const select = e.target as HTMLSelectElement;
+    //           this.prefs!.default_base_for_merges = select.value as DefaultBase;
     //           this.requestUpdate();
     //         }}
     //       >
-    //         <select id="defaultBaseForMergesSelect">
-    //           <option value="AUTO_MERGE">Auto Merge</option>
-    //           <option value="FIRST_PARENT">First Parent</option>
-    //         </select>
-    //       </gr-select>
+    //         <md-select-option value="AUTO_MERGE">
+    //           <div slot="headline">Auto Merge</div>
+    //         </md-select-option>
+    //         <md-select-option value="FIRST_PARENT">
+    //           <div slot="headline">First Parent</div>
+    //         </md-select-option>
+    //       </md-outlined-select>
     //     </span>
     //   </section>
     // `;
diff --git a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts
index 26964a0..d407750 100644
--- a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences_test.ts
@@ -16,20 +16,20 @@
 import {PreferencesInfo, TopMenuItemInfo} from '../../../types/common';
 import {
   AppTheme,
+  createDefaultPreferences,
   DateFormat,
   DefaultBase,
   DiffViewMode,
   EmailFormat,
   EmailStrategy,
   TimeFormat,
-  createDefaultPreferences,
 } from '../../../constants/constants';
-import {fixture, html, assert} from '@open-wc/testing';
-import {GrSelect} from '../../shared/gr-select/gr-select';
+import {assert, fixture, html} from '@open-wc/testing';
 import {
   createAccountDetailWithId,
   createPreferences,
 } from '../../../test/test-data-generators';
+import {MdOutlinedSelect} from '@material/web/select/outlined-select';
 
 suite('gr-preferences tests', () => {
   let element: GrPreferences;
@@ -85,13 +85,22 @@
             <section>
               <label class="title" for="themeSelect"> Theme </label>
               <span class="value">
-                <gr-select>
-                  <select id="themeSelect">
-                    <option value="AUTO">Auto (based on OS prefs)</option>
-                    <option value="LIGHT">Light</option>
-                    <option value="DARK">Dark</option>
-                  </select>
-                </gr-select>
+                <md-outlined-select value="LIGHT">
+                  <md-select-option md-menu-item="" tabindex="0" value="AUTO">
+                    <div slot="headline">Auto (based on OS prefs)</div>
+                  </md-select-option>
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="LIGHT"
+                  >
+                    <div slot="headline">Light</div>
+                  </md-select-option>
+                  <md-select-option md-menu-item="" tabindex="-1" value="DARK">
+                    <div slot="headline">Dark</div>
+                  </md-select-option>
+                </md-outlined-select>
               </span>
             </section>
             <section>
@@ -99,14 +108,25 @@
                 Changes per page
               </label>
               <span class="value">
-                <gr-select>
-                  <select id="changesPerPageSelect">
-                    <option value="10">10 rows per page</option>
-                    <option value="25">25 rows per page</option>
-                    <option value="50">50 rows per page</option>
-                    <option value="100">100 rows per page</option>
-                  </select>
-                </gr-select>
+                <md-outlined-select value="25">
+                  <md-select-option md-menu-item="" tabindex="0" value="10">
+                    <div slot="headline">10 rows per page</div>
+                  </md-select-option>
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="25"
+                  >
+                    <div slot="headline">25 rows per page</div>
+                  </md-select-option>
+                  <md-select-option md-menu-item="" tabindex="-1" value="50">
+                    <div slot="headline">50 rows per page</div>
+                  </md-select-option>
+                  <md-select-option md-menu-item="" tabindex="-1" value="100">
+                    <div slot="headline">100 rows per page</div>
+                  </md-select-option>
+                </md-outlined-select>
               </span>
             </section>
             <section>
@@ -114,21 +134,45 @@
                 Date/time format
               </label>
               <span class="value">
-                <gr-select>
-                  <select id="dateTimeFormatSelect">
-                    <option value="STD">Jun 3 ; Jun 3, 2016</option>
-                    <option value="US">06/03 ; 06/03/16</option>
-                    <option value="ISO">06-03 ; 2016-06-03</option>
-                    <option value="EURO">3. Jun ; 03.06.2016</option>
-                    <option value="UK">03/06 ; 03/06/2016</option>
-                  </select>
-                </gr-select>
-                <gr-select aria-label="Time Format">
-                  <select id="timeFormatSelect">
-                    <option value="HHMM_12">4:10 PM</option>
-                    <option value="HHMM_24">16:10</option>
-                  </select>
-                </gr-select>
+                <md-outlined-select value="UK">
+                  <md-select-option md-menu-item="" tabindex="0" value="STD">
+                    <div slot="headline">Jun 3 ; Jun 3, 2016</div>
+                  </md-select-option>
+                  <md-select-option md-menu-item="" tabindex="-1" value="US">
+                    <div slot="headline">06/03 ; 06/03/16</div>
+                  </md-select-option>
+                  <md-select-option md-menu-item="" tabindex="-1" value="ISO">
+                    <div slot="headline">06-03 ; 2016-06-03</div>
+                  </md-select-option>
+                  <md-select-option md-menu-item="" tabindex="-1" value="EURO">
+                    <div slot="headline">3. Jun ; 03.06.2016</div>
+                  </md-select-option>
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="UK"
+                  >
+                    <div slot="headline">03/06 ; 03/06/2016</div>
+                  </md-select-option>
+                </md-outlined-select>
+                <md-outlined-select value="HHMM_12">
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="0"
+                    value="HHMM_12"
+                  >
+                    <div slot="headline">4:10 PM</div>
+                  </md-select-option>
+                  <md-select-option
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="HHMM_24"
+                  >
+                    <div slot="headline">16:10</div>
+                  </md-select-option>
+                </md-outlined-select>
               </span>
             </section>
             <section>
@@ -136,18 +180,32 @@
                 Email notifications
               </label>
               <span class="value">
-                <gr-select>
-                  <select id="emailNotificationsSelect">
-                    <option value="CC_ON_OWN_COMMENTS">Every comment</option>
-                    <option value="ENABLED">
-                      Only comments left by others
-                    </option>
-                    <option value="ATTENTION_SET_ONLY">
+                <md-outlined-select value="ENABLED">
+                  <md-select-option
+                    md-menu-item=""
+                    tabindex="0"
+                    value="CC_ON_OWN_COMMENTS"
+                  >
+                    <div slot="headline">Every comment</div>
+                  </md-select-option>
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="ENABLED"
+                  >
+                    <div slot="headline">Only comments left by others</div>
+                  </md-select-option>
+                  <md-select-option
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="ATTENTION_SET_ONLY"
+                  >
+                    <div slot="headline">
                       Only when I am in the attention set
-                    </option>
-                    <option value="DISABLED">None</option>
-                  </select>
-                </gr-select>
+                    </div>
+                  </md-select-option>
+                </md-outlined-select>
               </span>
             </section>
             <section>
@@ -155,12 +213,23 @@
                 Email format
               </label>
               <span class="value">
-                <gr-select>
-                  <select id="emailFormatSelect">
-                    <option value="HTML_PLAINTEXT">HTML and plaintext</option>
-                    <option value="PLAINTEXT">Plaintext only</option>
-                  </select>
-                </gr-select>
+                <md-outlined-select value="HTML_PLAINTEXT">
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="0"
+                    value="HTML_PLAINTEXT"
+                  >
+                    <div slot="headline">HTML and plaintext</div>
+                  </md-select-option>
+                  <md-select-option
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="PLAINTEXT"
+                  >
+                    <div slot="headline">Plaintext only</div>
+                  </md-select-option>
+                </md-outlined-select>
               </span>
             </section>
             <section id="allowBrowserNotificationsSection">
@@ -173,15 +242,12 @@
                   rel="noopener noreferrer"
                   target="_blank"
                 >
-                  <gr-icon icon="help" title="read documentation"></gr-icon>
+                  <gr-icon icon="help" title="read documentation"> </gr-icon>
                 </a>
               </div>
               <span class="value">
-                <input
-                  checked=""
-                  id="allowBrowserNotifications"
-                  type="checkbox"
-                />
+                <md-checkbox checked="" id="allowBrowserNotifications">
+                </md-checkbox>
               </span>
             </section>
             <section>
@@ -189,18 +255,29 @@
                 Show Relative Dates In Changes Table
               </label>
               <span class="value">
-                <input id="relativeDateInChangeTable" type="checkbox" />
+                <md-checkbox id="relativeDateInChangeTable"> </md-checkbox>
               </span>
             </section>
             <section>
               <span class="title"> Diff view </span>
               <span class="value">
-                <gr-select>
-                  <select id="diffViewSelect">
-                    <option value="SIDE_BY_SIDE">Side by side</option>
-                    <option value="UNIFIED_DIFF">Unified diff</option>
-                  </select>
-                </gr-select>
+                <md-outlined-select value="UNIFIED_DIFF">
+                  <md-select-option
+                    md-menu-item=""
+                    tabindex="0"
+                    value="SIDE_BY_SIDE"
+                  >
+                    <div slot="headline">Side by side</div>
+                  </md-select-option>
+                  <md-select-option
+                    data-aria-selected="true"
+                    md-menu-item=""
+                    tabindex="-1"
+                    value="UNIFIED_DIFF"
+                  >
+                    <div slot="headline">Unified diff</div>
+                  </md-select-option>
+                </md-outlined-select>
               </span>
             </section>
             <section>
@@ -208,7 +285,8 @@
                 Show size bars in file list
               </label>
               <span class="value">
-                <input checked="" id="showSizeBarsInFileList" type="checkbox" />
+                <md-checkbox checked="" id="showSizeBarsInFileList">
+                </md-checkbox>
               </span>
             </section>
             <section>
@@ -216,7 +294,7 @@
                 Publish comments on push
               </label>
               <span class="value">
-                <input id="publishCommentsOnPush" type="checkbox" />
+                <md-checkbox id="publishCommentsOnPush"> </md-checkbox>
               </span>
             </section>
             <section>
@@ -224,7 +302,7 @@
                 Set new changes to "work in progress" by default
               </label>
               <span class="value">
-                <input id="workInProgressByDefault" type="checkbox" />
+                <md-checkbox id="workInProgressByDefault"> </md-checkbox>
               </span>
             </section>
             <section>
@@ -232,7 +310,7 @@
                 Disable all keyboard shortcuts
               </label>
               <span class="value">
-                <input id="disableKeyboardShortcuts" type="checkbox" />
+                <md-checkbox id="disableKeyboardShortcuts"> </md-checkbox>
               </span>
             </section>
             <section>
@@ -240,7 +318,7 @@
                 Disable token highlighting on hover
               </label>
               <span class="value">
-                <input id="disableTokenHighlighting" type="checkbox" />
+                <md-checkbox id="disableTokenHighlighting"> </md-checkbox>
               </span>
             </section>
             <section>
@@ -248,7 +326,7 @@
                 Insert Signed-off-by Footer For Inline Edit Changes
               </label>
               <span class="value">
-                <input id="insertSignedOff" type="checkbox" />
+                <md-checkbox id="insertSignedOff"> </md-checkbox>
               </span>
             </section>
           </div>
@@ -279,14 +357,14 @@
           </label>
           <a
             href="/Documentation/user-attention-set.html#_browser_notifications"
-            target="_blank"
             rel="noopener noreferrer"
+            target="_blank"
           >
             <gr-icon icon="help" title="read documentation"> </gr-icon>
           </a>
         </div>
         <span class="value">
-          <input checked="" id="allowBrowserNotifications" type="checkbox" />
+          <md-checkbox checked="" id="allowBrowserNotifications"> </md-checkbox>
         </span>
       </section>`
     );
@@ -297,62 +375,69 @@
     assert.equal(
       Number(
         (
-          valueOf('Changes per page', 'preferences')!
-            .firstElementChild as GrSelect
-        ).bindValue
+          valueOf('Changes per page', 'preferences')
+            .firstElementChild as MdOutlinedSelect
+        ).value
       ),
       preferences.changes_per_page
     );
     assert.equal(
-      (valueOf('Theme', 'preferences').firstElementChild as GrSelect).bindValue,
+      (valueOf('Theme', 'preferences').firstElementChild as MdOutlinedSelect)
+        .value,
       preferences.theme
     );
     assert.equal(
       (
-        valueOf('Date/time format', 'preferences')!
-          .firstElementChild as GrSelect
-      ).bindValue,
+        valueOf('Date/time format', 'preferences')
+          .firstElementChild as MdOutlinedSelect
+      ).value,
       preferences.date_format
     );
     assert.equal(
-      (valueOf('Date/time format', 'preferences')!.lastElementChild as GrSelect)
-        .bindValue,
+      (
+        valueOf('Date/time format', 'preferences')
+          .lastElementChild as MdOutlinedSelect
+      ).value,
       preferences.time_format
     );
     assert.equal(
       (
-        valueOf('Email notifications', 'preferences')!
-          .firstElementChild as GrSelect
-      ).bindValue,
+        valueOf('Email notifications', 'preferences')
+          .firstElementChild as MdOutlinedSelect
+      ).value,
       preferences.email_strategy
     );
     assert.equal(
-      (valueOf('Email format', 'preferences')!.firstElementChild as GrSelect)
-        .bindValue,
+      (
+        valueOf('Email format', 'preferences')
+          .firstElementChild as MdOutlinedSelect
+      ).value,
       preferences.email_format
     );
     assert.equal(
       (
-        valueOf('Show Relative Dates In Changes Table', 'preferences')!
+        valueOf('Show Relative Dates In Changes Table', 'preferences')
           .firstElementChild as HTMLInputElement
       ).checked,
       false
     );
     assert.equal(
-      (valueOf('Diff view', 'preferences')!.firstElementChild as GrSelect)
-        .bindValue,
+      (
+        valueOf('Diff view', 'preferences')
+          .firstElementChild as MdOutlinedSelect
+      ).value,
       preferences.diff_view
     );
     assert.equal(
       (
-        valueOf('Show size bars in file list', 'preferences')!
+        valueOf('Show size bars in file list', 'preferences')
           .firstElementChild as HTMLInputElement
       ).checked,
       true
     );
     assert.equal(
       (
-        valueOf('Publish comments on push', 'preferences')!
+        valueOf('Publish comments on push', 'preferences')
           .firstElementChild as HTMLInputElement
       ).checked,
       false
@@ -362,13 +447,13 @@
         valueOf(
           'Set new changes to "work in progress" by default',
           'preferences'
-        )!.firstElementChild as HTMLInputElement
+        ).firstElementChild as HTMLInputElement
       ).checked,
       false
     );
     assert.equal(
       (
-        valueOf('Disable token highlighting on hover', 'preferences')!
+        valueOf('Disable token highlighting on hover', 'preferences')
           .firstElementChild as HTMLInputElement
       ).checked,
       false
@@ -378,7 +463,7 @@
         valueOf(
           'Insert Signed-off-by Footer For Inline Edit Changes',
           'preferences'
-        )!.firstElementChild as HTMLInputElement
+        ).firstElementChild as HTMLInputElement
       ).checked,
       false
     );
@@ -390,8 +475,8 @@
     assert.equal(element.prefs?.theme, AppTheme.LIGHT);
 
     const themeSelect = valueOf('Theme', 'preferences')
-      .firstElementChild as GrSelect;
-    themeSelect.bindValue = AppTheme.DARK;
+      .firstElementChild as MdOutlinedSelect;
+    themeSelect.value = AppTheme.DARK;
 
     themeSelect.dispatchEvent(
       new CustomEvent('change', {
@@ -400,7 +485,7 @@
       })
     );
 
-    const publishOnPush = valueOf('Publish comments on push', 'preferences')!
+    const publishOnPush = valueOf('Publish comments on push', 'preferences')
       .firstElementChild! as HTMLSpanElement;
 
     publishOnPush.click();
@@ -437,15 +522,17 @@
     const publishCommentsOnPush = valueOf(
       'Publish comments on push',
       'preferences'
-    )!.firstElementChild! as HTMLSpanElement;
+    ).firstElementChild! as HTMLSpanElement;
     publishCommentsOnPush.click();
 
     assert.isTrue(element.hasUnsavedChanges());
 
-    stubRestApi('savePreferences').callsFake(prefs => {
-      assert.equal(prefs.publish_comments_on_push, true);
-      return Promise.resolve(createDefaultPreferences());
-    });
+    stubRestApi('savePreferences').callsFake(
+      (prefs: Partial<PreferencesInfo>) => {
+        assert.equal(prefs.publish_comments_on_push, true);
+        return Promise.resolve(createDefaultPreferences());
+      }
+    );
 
     // Save the change.
     await element.save();
@@ -458,15 +545,17 @@
     const newChangesWorkInProgress = valueOf(
       'Set new changes to "work in progress" by default',
       'preferences'
-    )!.firstElementChild! as HTMLSpanElement;
+    ).firstElementChild! as HTMLSpanElement;
     newChangesWorkInProgress.click();
 
     assert.isTrue(element.hasUnsavedChanges());
 
-    stubRestApi('savePreferences').callsFake(prefs => {
-      assert.equal(prefs.work_in_progress_by_default, true);
-      return Promise.resolve(createDefaultPreferences());
-    });
+    stubRestApi('savePreferences').callsFake(
+      (prefs: Partial<PreferencesInfo>) => {
+        assert.equal(prefs.work_in_progress_by_default, true);
+        return Promise.resolve(createDefaultPreferences());
+      }
+    );
 
     // Save the change.
     await element.save();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
index 40423e7..f85b029 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
@@ -3,20 +3,20 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../../styles/gr-form-styles';
 import '../../shared/gr-button/gr-button';
-import {ServerInfo, AccountDetailInfo} from '../../../types/common';
+import {AccountDetailInfo, ServerInfo} from '../../../types/common';
 import {EditableAccountField} from '../../../constants/constants';
 import {getAppContext} from '../../../services/app-context';
 import {fire} from '../../../utils/event-util';
-import {LitElement, css, html, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {when} from 'lit/directives/when.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
-import {BindValueChangeEvent} from '../../../types/events';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -70,6 +70,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       sharedStyles,
       grFormStyles,
       css`
@@ -111,6 +112,9 @@
         input {
           width: 20em;
         }
+        md-outlined-text-field {
+          width: 20em;
+        }
       `,
     ];
   }
@@ -132,17 +136,20 @@
           ${when(
             this.nameMutable,
             () => html`<span class="value">
-              <iron-input
-                .bindValue=${this.account.name}
-                @bind-value-changed=${(e: BindValueChangeEvent) => {
+              <md-outlined-text-field
+                id="name"
+                class="showBlueFocusBorder"
+                ?disabled=${this.saving}
+                .value=${this.account.name ?? ''}
+                @input=${(e: InputEvent) => {
+                  const target = e.target as HTMLInputElement;
                   const oldAccount = this.account;
-                  if (!oldAccount || oldAccount.name === e.detail.value) return;
-                  this.account = {...oldAccount, name: e.detail.value};
+                  if (!oldAccount || oldAccount.name === target.value) return;
+                  this.account = {...oldAccount, name: target.value};
                   this.hasNameChange = true;
                 }}
               >
-                <input id="name" ?disabled=${this.saving} />
-              </iron-input>
+              </md-outlined-text-field>
             </span>`,
             () => html`<span class="value">${this.account.name}</span>`
           )}
@@ -150,19 +157,22 @@
         <section>
           <span class="title">Display Name</span>
           <span class="value">
-            <iron-input
-              .bindValue=${this.account.display_name}
-              @bind-value-changed=${(e: BindValueChangeEvent) => {
+            <md-outlined-text-field
+              id="displayName"
+              class="showBlueFocusBorder"
+              ?disabled=${this.saving}
+              .value=${this.account.display_name ?? ''}
+              @input=${(e: InputEvent) => {
+                const target = e.target as HTMLInputElement;
                 const oldAccount = this.account;
-                if (!oldAccount || oldAccount.display_name === e.detail.value) {
+                if (!oldAccount || oldAccount.display_name === target.value) {
                   return;
                 }
-                this.account = {...oldAccount, display_name: e.detail.value};
+                this.account = {...oldAccount, display_name: target.value};
                 this.hasDisplayNameChange = true;
               }}
             >
-              <input id="displayName" ?disabled=${this.saving} />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         ${when(
@@ -172,17 +182,20 @@
             ${when(
               this.usernameMutable,
               () => html` <span class="value">
-                <iron-input
-                  .bindValue=${this.username}
-                  @bind-value-changed=${(e: BindValueChangeEvent) => {
-                    if (!this.usernameInput || this.username === e.detail.value)
+                <md-outlined-text-field
+                  id="username"
+                  class="showBlueFocusBorder"
+                  ?disabled=${this.saving}
+                  .value=${this.username ?? ''}
+                  @input=${(e: InputEvent) => {
+                    const target = e.target as HTMLInputElement;
+                    if (!this.usernameInput || this.username === target.value)
                       return;
-                    this.username = e.detail.value;
+                    this.username = target.value;
                     this.hasUsernameChange = true;
                   }}
                 >
-                  <input id="username" ?disabled=${this.saving} />
-                </iron-input>
+                </md-outlined-text-field>
               </span>`,
               () => html`<span class="value">${this.username}</span>`
             )}
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
index 83ca149..8b18923 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
@@ -13,7 +13,7 @@
   createAccountWithId,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 
 suite('gr-registration-dialog tests', () => {
@@ -36,15 +36,15 @@
         ...account,
       })
     );
-    stubRestApi('setAccountName').callsFake(name => {
+    stubRestApi('setAccountName').callsFake((name: string) => {
       account.name = name;
       return Promise.resolve();
     });
-    stubRestApi('setAccountUsername').callsFake(username => {
+    stubRestApi('setAccountUsername').callsFake((username: string) => {
       account.username = username;
       return Promise.resolve();
     });
-    stubRestApi('setAccountDisplayName').callsFake(displayName => {
+    stubRestApi('setAccountDisplayName').callsFake((displayName: string) => {
       account.display_name = displayName;
       return Promise.resolve();
     });
@@ -120,23 +120,40 @@
         <section>
           <span class="title"> Full Name </span>
           <span class="value">
-            <iron-input>
-              <input id="name">
-            </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="name"
+                inputmode=""
+                type="text"
+              >
+              </md-outlined-text-field>
           </span>
         </section>
         <section>
           <span class="title"> Display Name </span>
           <span class="value">
-            <iron-input> <input id="displayName" /> </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="displayName"
+                inputmode=""
+                type="text"
+              >
+              </md-outlined-text-field>
           </span>
         </section>
         <section>
           <span class="title"> Username </span>
           <span class="value">
-            <iron-input>
-              <input id="username">
-            </iron-input>
+              <md-outlined-text-field
+                autocomplete=""
+                class="showBlueFocusBorder"
+                id="username"
+                inputmode=""
+                type="text"
+              >
+              </md-outlined-text-field>
           </span>
         </section>
         <hr />
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 51c1326..1b5f324 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -3,14 +3,11 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
-import '@polymer/paper-toggle-button/paper-toggle-button';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../gr-change-table-editor/gr-change-table-editor';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-diff-preferences/gr-diff-preferences';
 import '../../shared/gr-page-nav/gr-page-nav';
-import '../../shared/gr-select/gr-select';
 import '../../shared/gr-icon/gr-icon';
 import '../gr-account-info/gr-account-info';
 import '../gr-agreements-list/gr-agreements-list';
@@ -18,7 +15,7 @@
 import '../gr-email-editor/gr-email-editor';
 import '../gr-gpg-editor/gr-gpg-editor';
 import '../gr-group-list/gr-group-list';
-import '../gr-http-password/gr-http-password';
+import '../gr-auth-token/gr-auth-token';
 import '../gr-identities/gr-identities';
 import '../gr-menu-editor/gr-menu-editor';
 import '../gr-preferences/gr-preferences';
@@ -30,21 +27,17 @@
 import {GrGroupList} from '../gr-group-list/gr-group-list';
 import {GrIdentities} from '../gr-identities/gr-identities';
 import {GrDiffPreferences} from '../../shared/gr-diff-preferences/gr-diff-preferences';
-import {
-  AccountDetailInfo,
-  PreferencesInput,
-  ServerInfo,
-} from '../../../types/common';
+import {AccountDetailInfo, ServerInfo} from '../../../types/common';
 import {GrSshEditor} from '../gr-ssh-editor/gr-ssh-editor';
 import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
 import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
 import {fire, fireAlert, fireTitleChange} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
-import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
-import {LitElement, css, html} from 'lit';
+import {ValueChangedEvent} from '../../../types/events';
+import {css, html, LitElement} from 'lit';
 import {customElement, query, queryAsync, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {paperStyles} from '../../../styles/gr-paper-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {when} from 'lit/directives/when.js';
 import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
@@ -53,14 +46,13 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {resolve} from '../../../models/dependency';
 import {settingsViewModelToken} from '../../../models/views/settings';
-import {
-  changeTablePrefs,
-  userModelToken,
-} from '../../../models/user/user-model';
+import {userModelToken} from '../../../models/user/user-model';
 import {modalStyles} from '../../../styles/gr-modal-styles';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {rootUrl} from '../../../utils/url-util';
 import {GrEditPreferences} from '../gr-edit-preferences/gr-edit-preferences';
+import {GrAuthToken} from '../gr-auth-token/gr-auth-token';
+import '@material/web/textfield/outlined-text-field';
 
 const HTTP_AUTH = ['HTTP', 'HTTP_LDAP'];
 
@@ -98,21 +90,16 @@
 
   @queryAsync('#sshEditor') sshEditorPromise!: Promise<GrSshEditor>;
 
+  @queryAsync('#authToken') tokenEditorPromise!: Promise<GrAuthToken>;
+
   @queryAsync('#gpgEditor') gpgEditorPromise!: Promise<GrGpgEditor>;
 
   @query('#emailEditor', true) emailEditor!: GrEmailEditor;
 
-  @state() prefs: PreferencesInput = {};
-
   @state() private accountInfoChanged = false;
 
-  // private but used in test
-  @state() localChangeTableColumns: string[] = [];
-
   @state() private loading = true;
 
-  @state() private changeTableChanged = false;
-
   @state() private diffPrefsChanged = false;
 
   @state() private editPrefsChanged = false;
@@ -180,18 +167,6 @@
         this.account = acc;
       }
     );
-    subscribe(
-      this,
-      () => this.getUserModel().preferences$,
-      prefs => {
-        if (!prefs) {
-          throw new Error('getPreferences returned undefined');
-        }
-        this.prefs = prefs;
-        this.showNumber = !!prefs.legacycid_in_change_table;
-        this.localChangeTableColumns = changeTablePrefs(prefs);
-      }
-    );
   }
 
   // private, but used in tests
@@ -245,6 +220,12 @@
           );
         }
 
+        if (this.showHttpAuth()) {
+          configPromises.push(
+            this.tokenEditorPromise.then(tokenEditor => tokenEditor.loadData())
+          );
+        }
+
         return Promise.all(configPromises);
       })
     );
@@ -260,7 +241,7 @@
   static override get styles() {
     return [
       sharedStyles,
-      paperStyles,
+      materialStyles,
       fontStyles,
       grFormStyles,
       modalStyles,
@@ -422,9 +403,11 @@
               >
                 <div slot="header">Account State:</div>
                 <div slot="main">
-                  <textarea class="account-state-output" readonly>
-${this.accountState}</textarea
-                  >
+                  <textarea
+                    class="account-state-output"
+                    readonly
+                    .value=${this.accountState ?? ''}
+                  ></textarea>
                   <p class="account-state-note">
                     Note: The account state may contain sensitive data (e.g.
                     deadnames). Share it with others only on a need to know
@@ -484,33 +467,8 @@
             >
           </fieldset>
           <gr-menu-editor id="Menu"></gr-menu-editor>
-          <h2
-            id="ChangeTableColumns"
-            class=${this.computeHeaderClass(this.changeTableChanged)}
-          >
-            Change Table Columns
-          </h2>
-          <fieldset id="changeTableColumns">
-            <gr-change-table-editor
-              .showNumber=${this.showNumber}
-              @show-number-changed=${(e: ValueChangedEvent<boolean>) => {
-                this.showNumber = e.detail.value;
-                this.changeTableChanged = true;
-              }}
-              .displayedColumns=${this.localChangeTableColumns}
-              @displayed-columns-changed=${(e: ValueChangedEvent<string[]>) => {
-                this.localChangeTableColumns = e.detail.value;
-                this.changeTableChanged = true;
-              }}
-            >
-            </gr-change-table-editor>
-            <gr-button
-              id="saveChangeTable"
-              @click=${this.handleSaveChangeTable}
-              ?disabled=${!this.changeTableChanged}
-              >Save Changes</gr-button
-            >
-          </fieldset>
+          <gr-change-table-editor id="ChangeTableColumns">
+          </gr-change-table-editor>
           <h2
             id="Notifications"
             class=${this.computeHeaderClass(this.watchedProjectsChanged)}
@@ -562,22 +520,18 @@
             <section>
               <span class="title">New email address</span>
               <span class="value">
-                <iron-input
-                  class="newEmailInput"
-                  .bindValue=${this.newEmail}
-                  @bind-value-changed=${(e: BindValueChangeEvent) => {
-                    this.newEmail = e.detail.value;
+                <md-outlined-text-field
+                  class="newEmailInput showBlueFocusBorder"
+                  placeholder="email@example.com"
+                  ?disabled=${this.addingEmail}
+                  .value=${this.newEmail ?? ''}
+                  @input=${(e: InputEvent) => {
+                    const target = e.target as HTMLInputElement;
+                    this.newEmail = target.value;
                   }}
                   @keydown=${this.handleNewEmailKeydown}
                 >
-                  <input
-                    class="newEmailInput"
-                    type="text"
-                    ?disabled=${this.addingEmail}
-                    @keydown=${this.handleNewEmailKeydown}
-                    placeholder="email@example.com"
-                  />
-                </iron-input>
+                </md-outlined-text-field>
               </span>
             </section>
             <section
@@ -601,7 +555,7 @@
             () => html` <div>
               <h2 id="HTTPCredentials">HTTP Credentials</h2>
               <fieldset>
-                <gr-http-password id="httpPass"></gr-http-password>
+                <gr-auth-token id="authToken"></gr-auth-token>
               </fieldset>
             </div>`
           )}
@@ -688,14 +642,6 @@
     Promise.all([this.accountInfo.loadData()]);
   }
 
-  // private but used in test
-  async handleSaveChangeTable() {
-    this.prefs.change_table = this.localChangeTableColumns;
-    this.prefs.legacycid_in_change_table = this.showNumber;
-    await this.getUserModel().updatePreferences(this.prefs);
-    this.changeTableChanged = false;
-  }
-
   private computeHeaderClass(changed?: boolean) {
     return changed ? 'edited' : '';
   }
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 8031963..6e4ff88 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -9,20 +9,20 @@
 import {GrSettingsView} from './gr-settings-view';
 import {stubRestApi, waitEventLoop} from '../../../test/test-utils';
 import {
-  AuthInfo,
   AccountDetailInfo,
+  AuthInfo,
   EmailAddress,
   PreferencesInfo,
   ServerInfo,
   TopMenuItemInfo,
 } from '../../../types/common';
 import {
+  AppTheme,
   DateFormat,
   DefaultBase,
   DiffViewMode,
   EmailFormat,
   EmailStrategy,
-  AppTheme,
   TimeFormat,
 } from '../../../constants/constants';
 import {
@@ -30,7 +30,7 @@
   createPreferences,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-settings-view tests', () => {
   let element: GrSettingsView;
@@ -177,9 +177,7 @@
                   <textarea
                     class="account-state-output"
                     readonly=""
-                  >
-                    <!----><!---->
-                  </textarea>
+                  ></textarea>
                   <p class="account-state-note">
                     Note: The account state may contain sensitive data (e.g.
                     deadnames). Share it with others only on a need to know
@@ -217,19 +215,7 @@
             </gr-button>
           </fieldset>
           <gr-menu-editor id="Menu"> </gr-menu-editor>
-          <h2 id="ChangeTableColumns">Change Table Columns</h2>
-          <fieldset id="changeTableColumns">
-            <gr-change-table-editor> </gr-change-table-editor>
-            <gr-button
-              aria-disabled="true"
-              disabled=""
-              id="saveChangeTable"
-              role="button"
-              tabindex="-1"
-            >
-              Save Changes
-            </gr-button>
-          </fieldset>
+          <gr-change-table-editor id="ChangeTableColumns"> </gr-change-table-editor>
           <h2 id="Notifications">Notifications</h2>
           <fieldset id="watchedProjects">
             <gr-watched-projects-editor id="watchedProjectsEditor">
@@ -260,13 +246,14 @@
             <section>
               <span class="title"> New email address </span>
               <span class="value">
-                <iron-input class="newEmailInput">
-                  <input
-                    class="newEmailInput"
-                    placeholder="email@example.com"
-                    type="text"
-                  />
-                </iron-input>
+                <md-outlined-text-field
+                  autocomplete=""
+                  class="newEmailInput showBlueFocusBorder"
+                  inputmode=""
+                  placeholder="email@example.com"
+                  type="text"
+                >
+                </md-outlined-text-field>
               </span>
             </section>
             <section hidden="" id="verificationSentMessage">
@@ -377,22 +364,6 @@
     assert.isNotOk(element.lastSentVerificationEmail);
   });
 
-  test('handleSaveChangeTable', () => {
-    let newColumns = ['Owner', 'Project', 'Branch'];
-    element.localChangeTableColumns = newColumns.slice(0);
-    element.showNumber = false;
-    element.handleSaveChangeTable();
-    assert.deepEqual(element.prefs.change_table, newColumns);
-    assert.isNotOk(element.prefs.legacycid_in_change_table);
-
-    newColumns = ['Size'];
-    element.localChangeTableColumns = newColumns;
-    element.showNumber = true;
-    element.handleSaveChangeTable();
-    assert.deepEqual(element.prefs.change_table, newColumns);
-    assert.isTrue(element.prefs.legacycid_in_change_table);
-  });
-
   test('showHttpAuth', async () => {
     const serverConfig: ServerInfo = {
       ...createServerInfo(),
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
index 6d5f484..5f5e1775 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
@@ -3,20 +3,20 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import {SshKeyInfo} from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import {getAppContext} from '../../../services/app-context';
-import {LitElement, css, html, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {fire} from '../../../utils/event-util';
-import {BindValueChangeEvent} from '../../../types/events';
 import {modalStyles} from '../../../styles/gr-modal-styles';
+import {GrAutogrowTextarea} from '../../shared/gr-autogrow-textarea/gr-autogrow-textarea';
+import {formStyles} from '../../../styles/form-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -44,7 +44,7 @@
 
   @query('#addButton') addButton!: GrButton;
 
-  @query('#newKey') newKeyEditor!: IronAutogrowTextareaElement;
+  @query('#newKey') newKeyEditor!: GrAutogrowTextarea;
 
   @query('#viewKeyModal') viewKeyModal!: HTMLDialogElement;
 
@@ -53,6 +53,7 @@
   static override get styles() {
     return [
       grFormStyles,
+      formStyles,
       sharedStyles,
       modalStyles,
       css`
@@ -86,8 +87,13 @@
           min-width: 27em;
           width: auto;
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           background-color: var(--view-background-color);
+          --gr-autogrow-textarea-border-width: 0px;
+          --gr-autogrow-textarea-border-color: var(--border-color);
+        }
+        gr-autogrow-textarea:focus {
+          border: 2px solid var(--input-focus-border-color);
         }
       `,
     ];
@@ -154,20 +160,20 @@
           <section>
             <span class="title">New SSH key</span>
             <span class="value">
-              <iron-autogrow-textarea
+              <gr-autogrow-textarea
                 id="newKey"
                 autocomplete="on"
                 placeholder="New SSH Key"
-                .bindValue=${this.newKey}
-                @bind-value-changed=${(e: BindValueChangeEvent) => {
-                  this.newKey = e.detail.value ?? '';
+                .value=${this.newKey}
+                @input=${(e: InputEvent) => {
+                  const value = (e.target as GrAutogrowTextarea).value ?? '';
+                  this.newKey = value;
                 }}
-              ></iron-autogrow-textarea>
+              ></gr-autogrow-textarea>
             </span>
           </section>
           <gr-button
             id="addButton"
-            link=""
             ?disabled=${!this.newKey.length}
             @click=${() => this.handleAddKey()}
             >Add New SSH Key</gr-button
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
index 9f690e5..dd80296 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.ts
@@ -15,7 +15,7 @@
 import {GrSshEditor} from './gr-ssh-editor';
 import {SshKeyInfo} from '../../../types/common';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-ssh-editor tests', () => {
   let element: GrSshEditor;
@@ -165,20 +165,18 @@
             <section>
               <span class="title"> New SSH key </span>
               <span class="value">
-                <iron-autogrow-textarea
-                  aria-disabled="false"
+                <gr-autogrow-textarea
                   autocomplete="on"
                   id="newKey"
                   placeholder="New SSH Key"
                 >
-                </iron-autogrow-textarea>
+                </gr-autogrow-textarea>
               </span>
             </section>
             <gr-button
               aria-disabled="true"
               disabled=""
               id="addButton"
-              link=""
               role="button"
               tabindex="-1"
             >
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
index a98203b..50833da 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.ts
@@ -3,15 +3,14 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import {customElement, query, state} from 'lit/decorators.js';
 import {
   AutocompleteQuery,
   GrAutocomplete,
-  AutocompleteSuggestion,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {assertIsDefined} from '../../../utils/common-util';
 import {ProjectWatchInfo, RepoName} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
@@ -23,6 +22,10 @@
 import {PropertiesOfType} from '../../../utils/type-util';
 import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {notDeepEqual} from '../../../utils/deep-util';
+import '@material/web/textfield/outlined-text-field';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 type NotificationKey = PropertiesOfType<Required<ProjectWatchInfo>, boolean>;
 
@@ -60,6 +63,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       sharedStyles,
       grFormStyles,
       css`
@@ -86,6 +90,10 @@
         .newFilterInput {
           width: 100%;
         }
+        md-checkbox {
+          --md-checkbox-container-size: 15px;
+          --md-checkbox-icon-size: 15px;
+        }
       `,
     ];
   }
@@ -114,16 +122,16 @@
                 allow-non-suggested-values
                 tab-complete
                 placeholder="Repo"
+                .showBlueFocusBorder=${true}
               ></gr-autocomplete>
             </th>
             <th colspan=${types.length}>
-              <iron-input id="newFilterInput" class="newFilterInput">
-                <input
-                  id="newFilter"
-                  class="newFilterInput"
-                  placeholder="branch:name, or other search expression"
-                />
-              </iron-input>
+              <md-outlined-text-field
+                id="newFilter"
+                class="newFilterInput showBlueFocusBorder"
+                placeholder="branch:name, or other search expression"
+              >
+              </md-outlined-text-field>
             </th>
             <th>
               <gr-button link="" @click=${this.handleAddProject}>Add</gr-button>
@@ -164,12 +172,11 @@
 
   private renderNotifyControl(project: ProjectWatchInfo, key: NotificationKey) {
     return html` <td class="notifControl" @click=${this.handleNotifCellClick}>
-      <input
-        type="checkbox"
+      <md-checkbox
         data-key=${key}
         @change=${(e: Event) => this.handleCheckboxChange(project, key, e)}
         ?checked=${!!project[key]}
-      />
+      ></md-checkbox>
     </td>`;
   }
 
@@ -272,7 +279,7 @@
         (this.projects[i].project === name &&
           this.isFilterDefined(projectFilter) &&
           this.isFilterDefined(filter) &&
-          projectFilter! > filter!)
+          projectFilter > filter)
       ) {
         break;
       }
@@ -313,7 +320,7 @@
     key: NotificationKey,
     e: Event
   ) {
-    const el = e.target as HTMLInputElement;
+    const el = e.target as MdCheckbox;
     const checked = el.checked;
     project[key] = !!checked;
     this.requestUpdate();
@@ -322,7 +329,7 @@
 
   private handleNotifCellClick(e: Event) {
     if (e.target === null) return;
-    const checkbox = (e.target as HTMLElement).querySelector('input');
+    const checkbox = (e.target as HTMLElement).querySelector('md-checkbox');
     if (checkbox) {
       checkbox.click();
     }
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
index e771a9d..b491c70 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
@@ -10,11 +10,11 @@
 import {stubRestApi, waitUntil} from '../../../test/test-utils';
 import {ProjectWatchInfo} from '../../../types/common';
 import {queryAndAssert} from '../../../test/test-utils';
-import {IronInputElement} from '@polymer/iron-input';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-watched-projects-editor tests', () => {
   let element: GrWatchedProjectsEditor;
@@ -45,19 +45,21 @@
     ] as ProjectWatchInfo[];
 
     stubRestApi('getWatchedProjects').returns(Promise.resolve(projects));
-    suggestionStub = stubRestApi('getSuggestedRepos').callsFake(input => {
-      if (input.startsWith('th')) {
-        return Promise.resolve({
-          'the project': {
-            id: 'the project',
-            state: 'ACTIVE',
-            web_links: [],
-          },
-        });
-      } else {
-        return Promise.resolve({});
+    suggestionStub = stubRestApi('getSuggestedRepos').callsFake(
+      (input: string) => {
+        if (input.startsWith('th')) {
+          return Promise.resolve({
+            'the project': {
+              id: 'the project',
+              state: 'ACTIVE',
+              web_links: [],
+            },
+          });
+        } else {
+          return Promise.resolve({});
+        }
       }
-    });
+    );
 
     element = await fixture(
       html`<gr-watched-projects-editor></gr-watched-projects-editor>`
@@ -88,27 +90,25 @@
               <tr>
                 <td>project a</td>
                 <td class="notifControl">
-                  <input data-key="notify_new_changes" type="checkbox" />
+                  <md-checkbox data-key="notify_new_changes"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_new_patch_sets" type="checkbox" />
+                  <md-checkbox data-key="notify_new_patch_sets"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_all_comments" type="checkbox" />
+                  <md-checkbox data-key="notify_all_comments"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input
+                  <md-checkbox
                     checked=""
                     data-key="notify_submitted_changes"
-                    type="checkbox"
-                  />
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input
+                  <md-checkbox
                     checked=""
                     data-key="notify_abandoned_changes"
-                    type="checkbox"
-                  />
+                  ></md-checkbox>
                 </td>
                 <td>
                   <gr-button
@@ -127,23 +127,26 @@
                   <div class="projectFilter">filter 1</div>
                 </td>
                 <td class="notifControl">
-                  <input
+                  <md-checkbox
                     checked=""
                     data-key="notify_new_changes"
-                    type="checkbox"
-                  />
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_new_patch_sets" type="checkbox" />
+                  <md-checkbox data-key="notify_new_patch_sets"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_all_comments" type="checkbox" />
+                  <md-checkbox data-key="notify_all_comments"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_submitted_changes" type="checkbox" />
+                  <md-checkbox
+                    data-key="notify_submitted_changes"
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_abandoned_changes" type="checkbox" />
+                  <md-checkbox
+                    data-key="notify_abandoned_changes"
+                  ></md-checkbox>
                 </td>
                 <td>
                   <gr-button
@@ -162,19 +165,23 @@
                   <div class="projectFilter">filter 2</div>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_new_changes" type="checkbox" />
+                  <md-checkbox data-key="notify_new_changes"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_new_patch_sets" type="checkbox" />
+                  <md-checkbox data-key="notify_new_patch_sets"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_all_comments" type="checkbox" />
+                  <md-checkbox data-key="notify_all_comments"></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_submitted_changes" type="checkbox" />
+                  <md-checkbox
+                    data-key="notify_submitted_changes"
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_abandoned_changes" type="checkbox" />
+                  <md-checkbox
+                    data-key="notify_abandoned_changes"
+                  ></md-checkbox>
                 </td>
                 <td>
                   <gr-button
@@ -190,31 +197,32 @@
               <tr>
                 <td>project c</td>
                 <td class="notifControl">
-                  <input
+                  <md-checkbox
                     checked=""
                     data-key="notify_new_changes"
-                    type="checkbox"
-                  />
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input
+                  <md-checkbox
                     checked=""
                     data-key="notify_new_patch_sets"
-                    type="checkbox"
-                  />
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input
+                  <md-checkbox
                     checked=""
                     data-key="notify_all_comments"
-                    type="checkbox"
-                  />
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_submitted_changes" type="checkbox" />
+                  <md-checkbox
+                    data-key="notify_submitted_changes"
+                  ></md-checkbox>
                 </td>
                 <td class="notifControl">
-                  <input data-key="notify_abandoned_changes" type="checkbox" />
+                  <md-checkbox
+                    data-key="notify_abandoned_changes"
+                  ></md-checkbox>
                 </td>
                 <td>
                   <gr-button
@@ -241,13 +249,15 @@
                   </gr-autocomplete>
                 </th>
                 <th colspan="5">
-                  <iron-input class="newFilterInput" id="newFilterInput">
-                    <input
-                      class="newFilterInput"
-                      id="newFilter"
-                      placeholder="branch:name, or other search expression"
-                    />
-                  </iron-input>
+                  <md-outlined-text-field
+                    autocomplete=""
+                    class="newFilterInput showBlueFocusBorder"
+                    id="newFilter"
+                    inputmode=""
+                    placeholder="branch:name, or other search expression"
+                    type="text"
+                  >
+                  </md-outlined-text-field>
                 </th>
                 <th>
                   <gr-button
@@ -295,6 +305,7 @@
     );
 
     repoInput.focus();
+    repoAutocomplete.debounceWait = 10;
     repoAutocomplete.text = 'the';
     await waitUntil(() => suggestionStub.called);
     await repoAutocomplete.updateComplete;
@@ -344,7 +355,7 @@
     assertIsDefined(element.newProject, 'newProject');
     element.newProject.value = 'project d';
     element.newProject.setText('project d');
-    queryAndAssert<IronInputElement>(element, '#newFilterInput').bindValue = '';
+    queryAndAssert<MdOutlinedTextField>(element, '#newFilter').value = '';
 
     element.handleAddProject();
 
@@ -359,7 +370,7 @@
     assertIsDefined(element.newProject, 'newProject');
     element.newProject.value = 'project b';
     element.newProject.setText('project b');
-    queryAndAssert<IronInputElement>(element, '#newFilterInput').bindValue =
+    queryAndAssert<MdOutlinedTextField>(element, '#newFilter').value =
       'filter 1';
     assertIsDefined(element.newFilter, 'newFilter');
     element.newFilter.value = 'filter 1';
diff --git a/polygerrit-ui/app/elements/shared/gr-a11y-announcer/gr-a11y-announcer.ts b/polygerrit-ui/app/elements/shared/gr-a11y-announcer/gr-a11y-announcer.ts
new file mode 100644
index 0000000..bca6b47
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-a11y-announcer/gr-a11y-announcer.ts
@@ -0,0 +1,90 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {css, html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+
+/**
+ * `gr-a11y-announcer` is a singleton element that allows screen reader
+ * announcements via dispatching a bubbling `iron-announce` event with a
+ * `text` detail payload.
+ */
+@customElement('gr-a11y-announcer')
+export class GrA11yAnnouncer extends LitElement {
+  /**
+   * The `aria-live` mode: 'off', 'polite', or 'assertive'.
+   */
+  @property({type: String}) mode: 'off' | 'polite' | 'assertive' = 'polite';
+
+  /**
+   * Time (ms) to wait before setting the text, to ensure screen readers re-announce.
+   */
+  @property({type: Number}) timeout = 150;
+
+  @state()
+  private _text = '';
+
+  static instance: GrA11yAnnouncer | null = null;
+
+  static override styles = css`
+    :host {
+      display: inline-block;
+      position: fixed;
+      clip: rect(0, 0, 0, 0);
+    }
+  `;
+
+  override connectedCallback() {
+    super.connectedCallback();
+
+    if (!GrA11yAnnouncer.instance) {
+      GrA11yAnnouncer.instance = this;
+    }
+
+    document.addEventListener('iron-announce', this._onIronAnnounce);
+  }
+
+  override disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener('iron-announce', this._onIronAnnounce);
+  }
+
+  override render() {
+    return html`<div aria-live=${this.mode}>${this._text}</div>`;
+  }
+
+  private _onIronAnnounce = (event: Event) => {
+    const customEvent = event as CustomEvent<{text?: string}>;
+    if (customEvent.detail?.text) {
+      this.announce(customEvent.detail.text);
+    }
+  };
+
+  /**
+   * Causes the given text to be announced by screen readers.
+   */
+  announce(text: string) {
+    // Clear first to allow repeated announcements of the same text.
+    this._text = '';
+    setTimeout(() => {
+      this._text = text;
+    }, this.timeout);
+  }
+
+  /**
+   * Ensures the singleton announcer element is available in the document.
+   */
+  static requestAvailability(): GrA11yAnnouncer {
+    if (!GrA11yAnnouncer.instance) {
+      const announcer = document.createElement(
+        'gr-a11y-announcer'
+      ) as GrA11yAnnouncer;
+      document.body.appendChild(announcer);
+      GrA11yAnnouncer.instance = announcer;
+    }
+    return GrA11yAnnouncer.instance;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 9709df2..2f9acf1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -13,7 +13,7 @@
   LabelInfo,
 } from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {ClassInfo, classMap} from 'lit/directives/class-map.js';
 import {getLabelStatus, hasVoted, LabelStatus} from '../../../utils/label-util';
@@ -59,6 +59,9 @@
   @property({type: Boolean, reflect: true})
   removable = false;
 
+  @property({type: Boolean, reflect: true})
+  override draggable = false;
+
   /**
    * Should attention set related features be shown in the component? Note
    * that the information whether the user is in the attention set or not is
@@ -140,9 +143,9 @@
           --account-label-padding-right: 3px;
           --account-label-circle-padding-right: 3px;
         }
-        gr-button.remove::part(paper-button),
-        gr-button.remove:hover::part(paper-button),
-        gr-button.remove:focus::part(paper-button) {
+        gr-button.remove::part(md-text-button),
+        gr-button.remove:hover::part(md-text-button),
+        gr-button.remove:focus::part(md-text-button) {
           border-top-width: 0;
           border-right-width: 0;
           border-bottom-width: 0;
@@ -167,6 +170,7 @@
         container: true,
         closeShown: this.removable,
       })}
+      ?draggable=${this.draggable}
     >
       <div>
         <gr-account-label
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts
index 8f1e169..f0361e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-account-chip';
 import {GrAccountChip} from './gr-account-chip';
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
index 2249b5d..9fe856b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.ts
@@ -9,7 +9,7 @@
   GrAutocomplete,
 } from '../gr-autocomplete/gr-autocomplete';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {
   AddAccountEvent,
@@ -17,8 +17,8 @@
   BindValueChangeEvent,
 } from '../../../types/events';
 import {SuggestedReviewerInfo} from '../../../types/common';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
 import {fire} from '../../../utils/event-util';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 /**
  * gr-account-entry is an element for entering account
@@ -81,7 +81,7 @@
     }
   }
 
-  get focusStart(): PaperInputElement | undefined {
+  get focusStart(): MdOutlinedTextField | undefined {
     return this.input!.focusStart;
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
index e8d7dae..dbaf609 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
@@ -7,10 +7,10 @@
 import '../../../test/common-test-setup';
 import './gr-account-entry';
 import {GrAccountEntry} from './gr-account-entry';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {queryAndAssert, waitUntil} from '../../../test/test-utils';
 import {GrAutocomplete} from '../gr-autocomplete/gr-autocomplete';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-account-entry tests', () => {
   let element: GrAccountEntry;
@@ -72,7 +72,7 @@
 
     const input = queryAndAssert<GrAutocomplete>(element, '#input');
     assert.equal(
-      queryAndAssert<PaperInputElement>(input, '#input').value,
+      queryAndAssert<MdOutlinedTextField>(input, '#input').value,
       'test text'
     );
     assert.isFalse(suggestStub.called);
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 7b2d3b0..d39d0f4 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
@@ -15,11 +15,11 @@
   isSelf,
   isServiceUser,
 } from '../../../utils/account-util';
-import {ChangeInfo, AccountInfo, ServerInfo} from '../../../types/common';
+import {AccountInfo, ChangeInfo, ServerInfo} from '../../../types/common';
 import {hasOwnProperty} from '../../../utils/common-util';
 import {fire} from '../../../utils/event-util';
 import {isInvolved} from '../../../utils/change-util';
-import {LitElement, css, html, TemplateResult} from 'lit';
+import {css, html, LitElement, TemplateResult} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {classMap} from 'lit/directives/class-map.js';
 import {getRemovedByIconClickReason} from '../../../utils/attention-set-util';
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
index 8b49cdd..268bb9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
@@ -20,7 +20,7 @@
   createPluginConfig,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
 
 suite('gr-account-label tests', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index e4490a2..18fd4e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -7,14 +7,15 @@
 import '../gr-account-entry/gr-account-entry';
 import {getAppContext} from '../../../services/app-context';
 import {
-  ChangeInfo,
-  Suggestion,
   AccountInfo,
-  GroupInfo,
+  ChangeInfo,
   EmailAddress,
+  GroupInfo,
+  isAccount,
+  isGroup,
   SuggestedReviewerGroupInfo,
   SuggestedReviewerInfo,
-  isGroup,
+  Suggestion,
 } from '../../../types/common';
 import {ReviewerSuggestionsProvider} from '../../../services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
 import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
@@ -28,19 +29,17 @@
   isSuggestedReviewerGroupInfo,
   RawAccountInput,
 } from '../../../utils/account-util';
-import {LitElement, css, html, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {classMap} from 'lit/directives/class-map.js';
 import {
   AutocompleteQuery,
-  AutocompleteSuggestion,
   GrAutocomplete,
 } from '../gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {ValueChangedEvent} from '../../../types/events';
 import {difference, queryAndAssert} from '../../../utils/common-util';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
-import {IronInputElement} from '@polymer/iron-input';
 import {ReviewerState} from '../../../api/rest-api';
 import {repeat} from 'lit/directives/repeat.js';
 
@@ -49,6 +48,7 @@
 
 declare global {
   interface HTMLElementEventMap {
+    'account-drag-start': CustomEvent<{account: AccountInfo}>;
     'accounts-changed': ValueChangedEvent<(AccountInfo | GroupInfo)[]>;
     'pending-confirmation-changed': ValueChangedEvent<SuggestedReviewerGroupInfo | null>;
     'account-added': CustomEvent<{account: AccountInfo | GroupInfo}>;
@@ -113,6 +113,9 @@
   @property({type: Array})
   removableValues?: AccountInput[];
 
+  @property({type: Boolean})
+  chipDraggable = false;
+
   /**
    * Returns suggestion items
    */
@@ -175,7 +178,9 @@
                 ),
               })}
               ?removable=${this.computeRemovable(account)}
+              ?draggable=${this.chipDraggable}
               @keydown=${this.handleChipKeydown}
+              @dragstart=${this.handleChipDragStart}
               tabindex="-1"
             >
             </gr-account-chip>
@@ -302,12 +307,13 @@
       return false;
     }
     if (this.removableValues) {
-      for (let i = 0; i < this.removableValues.length; i++) {
-        if (getUserId(this.removableValues[i]) === getUserId(account)) {
-          return true;
-        }
-      }
-      return isAccountNewlyAdded(account, this.reviewerState, this.change);
+      const removable = this.removableValues.some(
+        value => getUserId(value) === getUserId(account)
+      );
+      return (
+        removable ||
+        isAccountNewlyAdded(account, this.reviewerState, this.change)
+      );
     }
     return true;
   }
@@ -322,28 +328,23 @@
     if (!toRemove || !this.computeRemovable(toRemove)) {
       return false;
     }
-    for (let i = 0; i < this.accounts.length; i++) {
-      if (getUserId(toRemove) === getUserId(this.accounts[i])) {
-        this.accounts.splice(i, 1);
-        this.reporting.reportInteraction(`Remove from ${this.id}`);
-        this.requestUpdate();
-        fire(this, 'accounts-changed', {value: this.accounts.slice()});
-        return true;
-      }
+    const index = this.accounts.findIndex(
+      account => getUserId(toRemove) === getUserId(account)
+    );
+    if (index !== -1) {
+      this.accounts.splice(index, 1);
+      this.reporting.reportInteraction(`Remove from ${this.id}`);
+      this.requestUpdate();
+      fire(this, 'accounts-changed', {value: this.accounts.slice()});
+      return true;
     }
     return false;
   }
 
-  // private but used in test
-  getOwnNativeInput(paperInput: PaperInputElement) {
-    return (paperInput.inputElement as IronInputElement)
-      .inputElement as HTMLInputElement;
-  }
-
   private handleInputKeydown(e: KeyboardEvent) {
     const target = e.target as GrAccountEntry;
     const entryInput = queryAndAssert<GrAutocomplete>(target, '#input');
-    const input = this.getOwnNativeInput(entryInput.input!);
+    const input = entryInput.nativeInput;
     if (
       input.selectionStart !== input.selectionEnd ||
       input.selectionStart !== 0
@@ -362,6 +363,19 @@
     }
   }
 
+  private handleChipDragStart(e: DragEvent) {
+    const chip = e.target as GrAccountChip;
+    const account = chip.account;
+    if (!account || !isAccount(account)) return;
+
+    if (e.dataTransfer) {
+      e.dataTransfer.setData('text/plain', getUserId(account) as string);
+      e.dataTransfer.effectAllowed = 'move';
+    }
+
+    fire(this, 'account-drag-start', {account});
+  }
+
   private handleChipKeydown(e: KeyboardEvent) {
     const chip = e.target as GrAccountChip;
     const chips = this.accountChips;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 887ef96..40c1774 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -24,14 +24,12 @@
   waitUntil,
 } from '../../../test/test-utils';
 import {ReviewerSuggestionsProvider} from '../../../services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
-import {
-  AutocompleteSuggestion,
-  GrAutocomplete,
-} from '../gr-autocomplete/gr-autocomplete';
+import {GrAutocomplete} from '../gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
 import {createChange} from '../../../test/test-data-generators';
 import {ReviewerState} from '../../../api/rest-api';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {AccountInfoInput, RawAccountInput} from '../../../utils/account-util';
 
 class MockSuggestionsProvider implements ReviewerSuggestionsProvider {
@@ -448,16 +446,16 @@
       await element.updateComplete;
       // Next line is a workaround for Firefox not moving cursor
       // on input field update
-      assert.equal(element.getOwnNativeInput(input.input!).selectionStart, 0);
+      assert.equal(input.nativeInput.selectionStart, 0);
       input.text = 'test';
       input.input!.focus();
       await element.updateComplete;
       assert.equal(element.accounts.length, 2);
-      pressKey(element.getOwnNativeInput(input.input!), 'Backspace');
+      pressKey(input.nativeInput, 'Backspace');
       await waitUntil(() => element.accounts.length === 2);
       input.text = '';
       await input.updateComplete;
-      pressKey(element.getOwnNativeInput(input.input!), 'Backspace');
+      pressKey(input.nativeInput, 'Backspace');
       await waitUntil(() => element.accounts.length === 1);
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index 9342715..7059228 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -6,7 +6,7 @@
 import '../gr-button/gr-button';
 import '../../../styles/shared-styles';
 import {ErrorType} from '../../../types/types';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {DependencyRequestEvent} from '../../../models/dependency';
@@ -88,7 +88,7 @@
         <gr-button
           link=""
           class="action"
-          ?hidden=${this._hideActionButton}
+          ?hidden=${!!this._hideActionButton}
           @click=${this._handleActionTap}
           >${actionText}
         </gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index 4b27948..b73e038 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -175,6 +175,7 @@
 
   override disconnectedCallback() {
     this.cursor.unsetCursor();
+    this.cleanUpGlobalEventListeners();
     super.disconnectedCallback();
   }
 
@@ -192,10 +193,31 @@
     ) {
       if (!this.isHidden) {
         this.computeCursorStopsAndRefit();
+        this.setUpGlobalEventListeners();
+      } else {
+        this.cleanUpGlobalEventListeners();
       }
     }
   }
 
+  private setUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.addEventListener('resize', this.onWindowResize, passiveOptions);
+    window.addEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private cleanUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.removeEventListener('resize', this.onWindowResize, passiveOptions);
+    window.removeEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private readonly onWindowResize = () => {
+    this.fitController?.refit();
+  };
+
   private renderStatus() {
     return html`
       <li
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index b9063ba..f23a5b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -18,7 +18,7 @@
   waitUntil,
 } from '../../../test/test-utils';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {Key} from '../../../utils/dom-util';
 
 suite('gr-autocomplete-dropdown', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 210b91c..cd6745b 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -3,7 +3,6 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-input/paper-input';
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import '../gr-cursor-manager/gr-cursor-manager';
 import '../../../styles/shared-styles';
@@ -22,11 +21,13 @@
 import {PropertyType} from '../../../types/common';
 import {modifierPressed} from '../../../utils/dom-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {ValueChangedEvent} from '../../../types/events';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
-import {IronInputElement} from '@polymer/iron-input';
+import '@material/web/textfield/outlined-text-field';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 
 const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
 const DEBOUNCE_WAIT_MS = 200;
@@ -45,13 +46,6 @@
   }
 }
 
-export interface AutocompleteSuggestion<T = string> {
-  name?: string;
-  label?: string;
-  value?: T;
-  text?: string;
-}
-
 @customElement('gr-autocomplete')
 export class GrAutocomplete extends LitElement {
   /**
@@ -82,7 +76,7 @@
   @property({type: Object})
   query?: AutocompleteQuery = () => Promise.resolve([]);
 
-  @query('#input') input?: PaperInputElement;
+  @query('#input') input?: MdOutlinedTextField;
 
   @query('#suggestions') suggestionsDropdown?: GrAutocompleteDropdown;
 
@@ -149,11 +143,22 @@
   showBlueFocusBorder = false;
 
   /**
-   * Invisible label for input element. This label is exposed to
-   * screen readers by paper-input
+   * When true, the selection of the item will not trigger a commit.
+   * When used by GrSearchAutocomplete for example, we don't want the user to navigate to the results page after selecting an item.
    */
+
+  @property({type: Boolean, attribute: 'skip-commit-on-item-select'})
+  skipCommitOnItemSelect = false;
+
+  /**
+   * Debounce wait time in milliseconds for updating suggestions.
+   * Default is 200ms.
+   */
+  @property({type: Number})
+  debounceWait = DEBOUNCE_WAIT_MS;
+
   @property({type: String})
-  label = '';
+  label? = '';
 
   @state() suggestions: AutocompleteSuggestion[] = [];
 
@@ -197,63 +202,37 @@
   }
 
   get nativeInput() {
-    return (this.input!.inputElement as IronInputElement)
-      .inputElement as HTMLInputElement;
+    return this.input!.shadowRoot!.querySelector('input')!;
   }
 
   static override get styles() {
     return [
+      materialStyles,
       sharedStyles,
       css`
-        paper-input.borderless {
+        md-outlined-text-field {
+          height: var(--gr-autocomplete-height);
+          width: 100%;
+          border-radius: var(
+            --gr-autocomplete-text-field-border-radius,
+            var(--border-radius)
+          );
+          --md-outlined-text-field-container-shape: var(
+            --gr-autocomplete-text-field-border-radius,
+            var(--border-radius)
+          );
+        }
+        md-outlined-text-field.borderless {
+          --md-outlined-text-field-outline-width: 0;
+          --md-outlined-text-field-hover-outline-width: 0;
+          --md-outlined-text-field-focus-outline-width: 0;
           border: none;
           padding: 0;
         }
-        paper-input {
-          background-color: var(--view-background-color);
-          color: var(--primary-text-color);
-          border: 1px solid var(--prominent-border-color, var(--border-color));
-          border-radius: var(--border-radius);
-          padding: var(--spacing-s);
-          --paper-input-container_-_padding: 0;
-          --paper-input-container-input_-_font-size: var(--font-size-normal);
-          --paper-input-container-input_-_line-height: var(
-            --line-height-normal
-          );
-          /* This is a hack for not being able to set height:0 on the underline
-            of a paper-input 2.2.3 element. All the underline fixes below only
-            actually work in 3.x.x, so the height must be adjusted directly as
-            a workaround until we are on Polymer 3. */
-          height: var(--line-height-normal);
-          --paper-input-container-underline-height: 0;
-          --paper-input-container-underline-wrapper-height: 0;
-          --paper-input-container-underline-focus-height: 0;
-          --paper-input-container-underline-legacy-height: 0;
-          --paper-input-container-underline_-_height: 0;
-          --paper-input-container-underline_-_display: none;
-          --paper-input-container-underline-focus_-_height: 0;
-          --paper-input-container-underline-focus_-_display: none;
-          --paper-input-container-underline-disabled_-_height: 0;
-          --paper-input-container-underline-disabled_-_display: none;
-          /* Hide label for input. The label is still visible for
-           screen readers. Workaround found at:
-           https://github.com/PolymerElements/paper-input/issues/478 */
-          --paper-input-container-label_-_display: none;
-        }
-        paper-input.showBlueFocusBorder:focus {
-          border: 2px solid var(--input-focus-border-color);
-          /*
-         * The goal is to have a thicker blue border when focused and a thinner
-         * gray border when blurred. To avoid shifting neighboring elements
-         * around when the border size changes, a negative margin is added to
-         * compensate. box-sizing: border-box; will not work since there is
-         * important padding to add around the content.
-         */
-          margin: -1px;
-        }
-        paper-input.warnUncommitted {
-          --paper-input-container-input_-_color: var(--error-text-color);
-          --paper-input-container-input_-_font-size: inherit;
+        md-outlined-text-field.warnUncommitted {
+          --md-outlined-text-field-input-text-size: 16px;
+          --md-sys-color-primary: var(--error-text-color);
+          --md-sys-color-on-surface: var(--error-text-color);
         }
       `,
     ];
@@ -288,16 +267,28 @@
     }
   }
 
+  /**
+   * There's an issue where if an element doesn't use leading-icon/trailing-icon
+   * then a blank space is inserted where such icon should be.
+   * This function checks that an element contains either leading-icon or trailing-icon.
+   * @param name string
+   * @return bool
+   */
+  private hasNamedSlot(name: string): boolean {
+    const slot = this.querySelectorAll(`[slot="${name}"]`);
+    return !!slot && slot.length > 0;
+  }
+
   override render() {
     return html`
-      <paper-input
-        .noLabelFloat=${true}
+      <md-outlined-text-field
         id="input"
         class=${this.computeClass()}
         ?disabled=${this.disabled}
         .value=${this.text}
-        @value-changed=${(e: ValueChangedEvent) => {
-          this.text = e.detail.value;
+        @input=${(e: InputEvent) => {
+          const target = e.target as HTMLInputElement;
+          this.text = target.value;
         }}
         .placeholder=${this.placeholder}
         @keydown=${this.handleKeydown}
@@ -305,15 +296,23 @@
         @blur=${this.onInputBlur}
         autocomplete="off"
         .label=${this.label}
+        aria-label=${this.label || this.placeholder || nothing}
       >
-        <div slot="prefix">
-          <slot name="prefix"></slot>
-        </div>
-
-        <div slot="suffix">
-          <slot name="suffix"></slot>
-        </div>
-      </paper-input>
+        ${this.hasNamedSlot('leading-icon')
+          ? html`
+              <div slot="leading-icon">
+                <slot name="leading-icon"></slot>
+              </div>
+            `
+          : nothing}
+        ${this.hasNamedSlot('trailing-icon')
+          ? html`
+              <div slot="trailing-icon">
+                <slot name="trailing-icon"></slot>
+              </div>
+            `
+          : nothing}
+      </md-outlined-text-field>
       <gr-autocomplete-dropdown
         .verticalOffset=${this.verticalOffset}
         id="suggestions"
@@ -360,7 +359,7 @@
   private handleItemSelectEnter(
     e: CustomEvent<ItemSelectedEventDetail> | KeyboardEvent
   ) {
-    this.handleInputCommit();
+    this.handleInputCommit(this.skipCommitOnItemSelect);
     e.stopPropagation();
     e.preventDefault();
     this.focusWithoutDisplayingSuggestions();
@@ -369,7 +368,7 @@
   handleItemSelect(e: CustomEvent<ItemSelectedEventDetail>) {
     if (e.detail.trigger === 'click') {
       this.selected = e.detail.selected;
-      this._commit();
+      this.commit();
       e.stopPropagation();
       e.preventDefault();
       this.focusWithoutDisplayingSuggestions();
@@ -405,8 +404,6 @@
     this.setFocus(true);
     this.updateSuggestions();
     this.input?.classList.remove('warnUncommitted');
-    // Needed so that --paper-input-container-input updated style is applied.
-    this.requestUpdate();
   }
 
   onInputBlur() {
@@ -414,8 +411,6 @@
       'warnUncommitted',
       this.warnUncommitted && !!this.text.length && !this.focused
     );
-    // Needed so that --paper-input-container-input updated style is applied.
-    this.requestUpdate();
   }
 
   updateSuggestions() {
@@ -454,7 +449,7 @@
     this.updateSuggestionsTask = debounce(
       this.updateSuggestionsTask,
       this.createUpdateTask(queryId, this.query, this.text),
-      DEBOUNCE_WAIT_MS
+      this.debounceWait
     );
   }
 
@@ -553,12 +548,12 @@
           const queryId = this.activeQueryId;
           this.latestSuggestionUpdateComplete?.then(() => {
             if (queryId === this.activeQueryId) {
-              this.handleInputCommit(/* _tabComplete=*/ true);
+              this.handleInputCommit(/* tabComplete= */ true);
             }
           });
         } else if (this.suggestions.length > 0 && this.tabComplete) {
           e.preventDefault();
-          this.handleInputCommit(/* _tabComplete=*/ true);
+          this.handleInputCommit(/* tabComplete= */ true);
           this.focus();
         } else {
           this.setFocus(false);
@@ -615,7 +610,7 @@
     }
   }
 
-  handleInputCommit(_tabComplete?: boolean) {
+  handleInputCommit(tabComplete?: boolean) {
     // Nothing to do if no suggestions.
     if (
       !this.allowNonSuggestedValues &&
@@ -625,7 +620,7 @@
     }
 
     this.selected = this.suggestionsDropdown?.getCursorTarget() ?? null;
-    this._commit(_tabComplete);
+    this.commit(tabComplete);
   }
 
   updateValue(
@@ -670,8 +665,10 @@
    * @param silent Allows for silent committing of an
    * autocomplete suggestion in order to handle cases like tab-to-complete
    * without firing the commit event.
+   *
+   * Private but used in tests.
    */
-  async _commit(silent?: boolean) {
+  async commit(silent?: boolean) {
     // Allow values that are not in suggestion list iff suggestions are empty.
     if (this.suggestions.length > 0) {
       this.updateValue(this.selected, this.suggestions);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index bf3ba66..29dd506 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -6,7 +6,8 @@
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-autocomplete';
-import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
+import {GrAutocomplete} from './gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {
   assertFails,
   mockPromise,
@@ -18,9 +19,9 @@
   AutocompleteQueryStatusType,
   GrAutocompleteDropdown,
 } from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {Key, Modifier} from '../../../utils/dom-util';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-autocomplete tests', () => {
   let element: GrAutocomplete;
@@ -36,31 +37,21 @@
 
   setup(async () => {
     element = await fixture(html`<gr-autocomplete></gr-autocomplete>`);
+    element.debounceWait = 10;
   });
 
   test('renders', () => {
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <paper-input
-          aria-disabled="false"
+        <md-outlined-text-field
           autocomplete="off"
           id="input"
-          tabindex="0"
+          inputmode=""
+          type="text"
         >
-          <div slot="prefix">
-            <slot name="prefix"> </slot>
-          </div>
-          <div slot="suffix">
-            <slot name="suffix"> </slot>
-          </div>
-        </paper-input>
-        <gr-autocomplete-dropdown
-          id="suggestions"
-          is-hidden=""
-          role="listbox"
-          style="position: fixed; top: 300px; left: 392.5px; box-sizing: border-box; max-height: 600px; max-width: 785px;"
-        >
+        </md-outlined-text-field>
+        <gr-autocomplete-dropdown id="suggestions" is-hidden="" role="listbox">
         </gr-autocomplete-dropdown>
       `,
       {
@@ -92,19 +83,13 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <paper-input
-          aria-disabled="false"
+        <md-outlined-text-field
           autocomplete="off"
           id="input"
-          tabindex="0"
+          inputmode=""
+          type="text"
         >
-          <div slot="prefix">
-            <slot name="prefix"> </slot>
-          </div>
-          <div slot="suffix">
-            <slot name="suffix"> </slot>
-          </div>
-        </paper-input>
+        </md-outlined-text-field>
         <gr-autocomplete-dropdown id="suggestions" role="listbox">
         </gr-autocomplete-dropdown>
       `,
@@ -131,19 +116,13 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <paper-input
-          aria-disabled="false"
+        <md-outlined-text-field
           autocomplete="off"
           id="input"
-          tabindex="0"
+          inputmode=""
+          type="text"
         >
-          <div slot="prefix">
-            <slot name="prefix"> </slot>
-          </div>
-          <div slot="suffix">
-            <slot name="suffix"> </slot>
-          </div>
-        </paper-input>
+        </md-outlined-text-field>
         <gr-autocomplete-dropdown id="suggestions" role="listbox">
         </gr-autocomplete-dropdown>
       `,
@@ -323,6 +302,44 @@
     });
   });
 
+  test('does not fire commit on item select when skipCommitOnItemSelect = true ', async () => {
+    const queryStub = sinon.spy((input: string) =>
+      Promise.resolve([
+        {name: input + ' 0', value: '0'},
+        {name: input + ' 1', value: '1'},
+        {name: input + ' 2', value: '2'},
+        {name: input + ' 3', value: '3'},
+        {name: input + ' 4', value: '4'},
+      ] as AutocompleteSuggestion[])
+    );
+    element.query = queryStub;
+    element.skipCommitOnItemSelect = true;
+    await element.updateComplete;
+    assert.isTrue(suggestionsEl().isHidden);
+    assert.equal(suggestionsEl().cursor.index, -1);
+    element.setFocus(true);
+
+    element.text = 'blah';
+    await element.updateComplete;
+
+    await element.latestSuggestionUpdateComplete;
+
+    await waitUntil(() => !suggestionsEl().isHidden);
+
+    const commitHandler = sinon.spy(element, 'commit');
+
+    assert.equal(suggestionsEl().cursor.index, 0);
+
+    pressKey(inputEl(), Key.ENTER);
+    await element.updateComplete;
+
+    await waitUntil(() => element.value === '0');
+
+    assert.isTrue(commitHandler.called);
+    // called with silent=true
+    assert.equal(commitHandler.getCall(0).args[0], true);
+  });
+
   test('clear-on-commit behavior (off)', async () => {
     let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
     const queryStub = sinon.spy(() => {
@@ -542,10 +559,10 @@
       ] as AutocompleteSuggestion[]);
 
     // commitHandler checks for the commit event, whereas commitSpy checks for
-    // the _commit function of the element.
+    // the commit function of the element.
     const commitHandler = sinon.spy();
     element.addEventListener('commit', commitHandler);
-    const commitSpy = sinon.spy(element, '_commit');
+    const commitSpy = sinon.spy(element, 'commit');
     element.setFocus(true);
     element.tabComplete = false;
     element.text = 'text1';
@@ -577,7 +594,10 @@
   test('focused flag properly triggered', async () => {
     await element.updateComplete;
     assert.isFalse(element.focused);
-    const input = queryAndAssert<PaperInputElement>(element, 'paper-input');
+    const input = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field'
+    );
     input.focus();
     assert.isTrue(element.focused);
   });
@@ -608,7 +628,7 @@
     'handleInputCommit with autocomplete hidden does nothing without' +
       ' allowNonSuggestedValues',
     () => {
-      const commitStub = sinon.stub(element, '_commit');
+      const commitStub = sinon.stub(element, 'commit');
       suggestionsEl().isHidden = true;
       element.handleInputCommit();
       assert.isFalse(commitStub.called);
@@ -619,7 +639,7 @@
     'handleInputCommit with query error does nothing without' +
       ' allowNonSuggestedValues',
     () => {
-      const commitStub = sinon.stub(element, '_commit');
+      const commitStub = sinon.stub(element, 'commit');
       element.queryStatus = {
         type: AutocompleteQueryStatusType.ERROR,
         message: 'Error',
@@ -634,7 +654,7 @@
     'handleInputCommit with autocomplete hidden with' +
       'allowNonSuggestedValues',
     () => {
-      const commitStub = sinon.stub(element, '_commit');
+      const commitStub = sinon.stub(element, 'commit');
       element.allowNonSuggestedValues = true;
       suggestionsEl().isHidden = true;
       element.handleInputCommit();
@@ -645,7 +665,7 @@
   test(
     'handleInputCommit with query error with' + 'allowNonSuggestedValues',
     () => {
-      const commitStub = sinon.stub(element, '_commit');
+      const commitStub = sinon.stub(element, 'commit');
       element.allowNonSuggestedValues = true;
       element.queryStatus = {
         type: AutocompleteQueryStatusType.ERROR,
@@ -658,7 +678,7 @@
   );
 
   test('handleInputCommit with autocomplete open calls commit', () => {
-    const commitStub = sinon.stub(element, '_commit');
+    const commitStub = sinon.stub(element, 'commit');
     suggestionsEl().isHidden = false;
     element.suggestions = [{name: 'first suggestion'}];
     element.handleInputCommit();
@@ -669,7 +689,7 @@
     'handleInputCommit with autocomplete open calls commit' +
       'with allowNonSuggestedValues',
     () => {
-      const commitStub = sinon.stub(element, '_commit');
+      const commitStub = sinon.stub(element, 'commit');
       element.allowNonSuggestedValues = true;
       suggestionsEl().isHidden = false;
       element.handleInputCommit();
@@ -697,6 +717,7 @@
     assert.isTrue(keydownSpy.calledOnce);
 
     pressKey(inputEl(), Key.ENTER);
+    inputEl().dispatchEvent(new Event('input'));
     await element.updateComplete;
     assert.isTrue(keydownSpy.calledTwice);
 
@@ -917,7 +938,7 @@
     let focusSpy: sinon.SinonSpy;
 
     setup(() => {
-      commitSpy = sinon.spy(element, '_commit');
+      commitSpy = sinon.spy(element, 'commit');
     });
 
     test('enter in input does not re-render suggestions', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-autogrow-textarea/gr-autogrow-textarea.ts b/polygerrit-ui/app/elements/shared/gr-autogrow-textarea/gr-autogrow-textarea.ts
new file mode 100644
index 0000000..2aa769a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autogrow-textarea/gr-autogrow-textarea.ts
@@ -0,0 +1,357 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {
+  customElement,
+  property,
+  query,
+  queryAsync,
+  state,
+} from 'lit/decorators.js';
+import {ifDefined} from 'lit/directives/if-defined.js';
+import {
+  CursorPositionChangeEventDetail,
+  GrAutogrowTextarea as GrAutogrowTextareaApi,
+} from '../../../api/embed';
+
+/**
+ * Waits for the next animation frame.
+ */
+async function animationFrame(): Promise<void> {
+  return new Promise(resolve => {
+    requestAnimationFrame(() => {
+      resolve();
+    });
+  });
+}
+
+/**
+ * A custom textarea component which allows autocomplete functionality.
+ *
+ * Example usage:
+ * <gr-autogrow-textarea></gr-autogrow-textarea>
+ */
+@customElement('gr-autogrow-textarea')
+export class GrAutogrowTextarea
+  extends LitElement
+  implements GrAutogrowTextareaApi
+{
+  // editableTextAreaElement is available right away where it may be undefined. This
+  // is used for calls for scrollTop as if it is undefined then we can fallback
+  // to 0. For other usecases use editableTextArea.
+  @query('.editableTextArea')
+  private readonly editableTextAreaElement?: HTMLTextAreaElement;
+
+  @queryAsync('.editableTextArea')
+  private readonly editableTextArea?: Promise<HTMLTextAreaElement>;
+
+  @property({type: Boolean, reflect: true}) disabled = false;
+
+  @property({type: String, reflect: true}) placeholder: string | undefined;
+
+  @property({type: String}) value: string = '';
+
+  @property({type: Number}) rows = 1;
+
+  @property({type: Number}) maxRows = 0;
+
+  /**
+   * Sets cursor at the end of content on focus.
+   */
+  @property({type: Boolean}) putCursorAtEndOnFocus = false;
+
+  @property({type: String}) autocomplete = 'off';
+
+  @property({type: String}) inputmode?: string;
+
+  @property({type: String}) readonly?: string;
+
+  @property({type: Boolean}) required = false;
+
+  @property({type: Number}) minlength?: number;
+
+  @property({type: Number}) maxlength?: number;
+
+  @property({type: String}) label?: string;
+
+  /*
+   * Is textarea focused. This is a readonly property.
+   */
+  get isFocused(): boolean {
+    return !!this.focused;
+  }
+
+  /**
+   * Native element for editable div.
+   */
+  get nativeElement() {
+    return this.editableTextAreaElement;
+  }
+
+  /**
+   * Scroll Top for editable div.
+   */
+  override get scrollTop() {
+    return this.editableTextAreaElement?.scrollTop ?? 0;
+  }
+
+  private focused = false;
+
+  @state() private tokens: string[] = [];
+
+  static override get styles() {
+    return [
+      css`
+        :host {
+          display: inline-block;
+          position: relative;
+          width: 400px;
+          border: var(--gr-autogrow-textarea-border-width, 2px) solid
+            var(--gr-textarea-border-color, white);
+          border-radius: 4px;
+          padding: 2px;
+          -moz-appearance: textarea;
+          -webkit-appearance: textarea;
+          appearance: textarea;
+          overflow: hidden;
+        }
+        :host([disabled]) {
+          textarea {
+            background-color: var(--gr-autogrow-textarea-disabled-color);
+            color: var(--text-disabled, black);
+            cursor: default;
+          }
+        }
+        .mirror-text {
+          visibility: hidden;
+          word-wrap: break-word;
+          padding: var(--gr-autogrow-textarea-padding);
+          box-sizing: var(--gr-autogrow-textarea-box-sizing);
+        }
+        .textarea-container {
+          position: absolute;
+          top: 0;
+          left: 0;
+          right: 0;
+          bottom: 0;
+        }
+        textarea {
+          position: relative;
+          outline: none;
+          border: none;
+          resize: none;
+          background: inherit;
+          color: inherit;
+          width: 100%;
+          height: 100%;
+          font-size: inherit;
+          font-family: inherit;
+          line-height: inherit;
+          text-align: inherit;
+          padding: var(--gr-autogrow-textarea-padding);
+          box-sizing: var(--gr-autogrow-textarea-box-sizing);
+        }
+        textarea::placeholder {
+          color: var(--deemphasized-text-color);
+        }
+        textarea:focus-visible {
+          border-color: var(--gr-textarea-focus-outline-color, black);
+          outline: none;
+        }
+      `,
+    ];
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (
+      changedProperties.has('value') ||
+      changedProperties.has('rows') ||
+      changedProperties.has('maxRows')
+    ) {
+      this.tokens = this.constrain(this.tokenize(this.value));
+    }
+  }
+
+  override render() {
+    return html` <div id="mirror" class="mirror-text" aria-hidden="true">
+        ${this.tokens.length === 1 && this.tokens[0] === ''
+          ? html`&#160;`
+          : this.tokens.map((t, i) =>
+              i === this.tokens.length - 1 ? html`${t}&#160;` : html`${t}<br />`
+            )}
+      </div>
+      <div class="textarea-container">
+        <textarea
+          class="editableTextArea"
+          .value=${this.value}
+          aria-label=${ifDefined(this.label)}
+          aria-disabled=${this.disabled}
+          aria-multiline="true"
+          aria-placeholder=${ifDefined(this.placeholder)}
+          autocomplete=${this.autocomplete}
+          ?autofocus=${this.autofocus}
+          autocapitalize=${this.autocapitalize}
+          inputmode=${ifDefined(this.inputmode)}
+          placeholder=${ifDefined(this.placeholder)}
+          ?disabled=${this.disabled}
+          rows=${this.rows}
+          minlength=${ifDefined(this.minlength)}
+          maxlength=${ifDefined(this.maxlength)}
+          spellcheck=${this.spellcheck}
+          @input=${this.onInput}
+          @focus=${this.onFocus}
+          @blur=${this.onBlur}
+          @keyup=${this.handleKeyUp}
+          @mouseup=${this.handleMouseUp}
+        ></textarea>
+      </div>`;
+  }
+
+  override updated(changed: Map<string, unknown>) {
+    if (changed.has('value')) {
+      if (
+        this.editableTextAreaElement &&
+        this.editableTextAreaElement.value !== this.value
+      ) {
+        this.editableTextAreaElement.value = this.value ?? '';
+      }
+    }
+  }
+
+  override async focus() {
+    const editableTextAreaElement = await this.editableTextArea;
+    const isFocused = this.isFocused;
+    editableTextAreaElement?.focus?.();
+    // If already focused, do not change the cursor position.
+    if (this.putCursorAtEndOnFocus && !isFocused) {
+      await this.putCursorAtEnd();
+    }
+  }
+
+  /**
+   * Puts the cursor at the end of existing content.
+   * Scrolls the content of textarea towards the end.
+   */
+  async putCursorAtEnd() {
+    const editableTextAreaElement = await this.editableTextArea;
+    if (!editableTextAreaElement) return;
+
+    const length = this.value.length;
+    editableTextAreaElement.selectionStart = length;
+    editableTextAreaElement.selectionEnd = length;
+    editableTextAreaElement.focus();
+  }
+
+  /**
+   * Sets cursor position to given position and scrolls the content to cursor
+   * position.
+   *
+   * If position is out of bounds of value of textarea then cursor is places at
+   * end of content of textarea.
+   */
+  public setCursorPosition(position: number) {
+    if (!this.editableTextAreaElement) return;
+
+    this.editableTextAreaElement.selectionStart = position;
+    this.editableTextAreaElement.selectionEnd = position;
+
+    this.onCursorPositionChange();
+  }
+
+  /**
+   * Replaces text from start and end cursor position.
+   */
+  async setRangeText(replacement: string, start: number, end: number) {
+    const pre = this.value?.substring(0, start) ?? '';
+    const post = this.value?.substring(end, this.value?.length ?? 0) ?? '';
+    this.value = pre + replacement + post;
+
+    await animationFrame();
+
+    this.setCursorPosition(pre.length + replacement.length);
+  }
+
+  private onInput(event: Event) {
+    event.preventDefault();
+    event.stopImmediatePropagation();
+
+    const target = event.target as HTMLTextAreaElement;
+    this.value = target.value;
+
+    this.fire('input', {value: this.value});
+  }
+
+  private onFocus() {
+    this.focused = true;
+    this.onCursorPositionChange();
+  }
+
+  private onBlur() {
+    this.focused = false;
+    this.onCursorPositionChange();
+  }
+
+  private handleKeyUp() {
+    this.onCursorPositionChange();
+  }
+
+  private handleMouseUp() {
+    this.onCursorPositionChange();
+  }
+
+  private fire<T>(type: string, detail?: T) {
+    this.dispatchEvent(
+      new CustomEvent(type, {detail, bubbles: true, composed: true})
+    );
+  }
+
+  private onCursorPositionChange() {
+    const cursorPosition = this.getCursorPosition();
+    this.fire('cursorPositionChange', {position: cursorPosition});
+  }
+
+  public getCursorPosition() {
+    return this.editableTextAreaElement?.selectionStart ?? -1;
+  }
+
+  public async getCursorPositionAsync() {
+    const editableTextAreaElement = await this.editableTextArea;
+    return editableTextAreaElement?.selectionStart ?? -1;
+  }
+
+  private tokenize(val: string): string[] {
+    return val
+      ? val
+          .replace(/&/g, '&amp;')
+          .replace(/"/g, '&quot;')
+          .replace(/'/g, '&#39;')
+          .replace(/</g, '&lt;')
+          .replace(/>/g, '&gt;')
+          .split('\n')
+      : [''];
+  }
+
+  private constrain(tokens: string[]): string[] {
+    let result = tokens.slice();
+    if (this.maxRows > 0 && result.length > this.maxRows) {
+      result = result.slice(0, this.maxRows);
+    }
+    while (this.rows > 0 && result.length < this.rows) {
+      result.push('');
+    }
+    return result;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-autogrow-textarea': GrAutogrowTextarea;
+  }
+  interface HTMLElementEventMap {
+    // prettier-ignore
+    'cursorPositionChange': CustomEvent<CursorPositionChangeEventDetail>;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-autogrow-textarea/gr-autogrow-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-autogrow-textarea/gr-autogrow-textarea_test.ts
new file mode 100644
index 0000000..9b33189
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-autogrow-textarea/gr-autogrow-textarea_test.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrAutogrowTextarea} from './gr-autogrow-textarea';
+import './gr-autogrow-textarea';
+
+suite('gr-autogrow-textarea tests', () => {
+  let element: GrAutogrowTextarea;
+
+  setup(async () => {
+    element = await fixture(
+      html`<gr-autogrow-textarea></gr-autogrow-textarea>`
+    );
+  });
+
+  test('render', async () => {
+    // prettier and shadowDom string disagree about wrapping in <p> tag.
+    assert.shadowDom.equal(
+      element,
+      /* prettier-ignore */ /* HTML */ `
+      <div
+        aria-hidden="true"
+        class="mirror-text"
+        id="mirror"
+      >
+      </div>
+      <div class="textarea-container">
+        <textarea
+          aria-disabled="false"
+          aria-multiline="true"
+          autocapitalize=""
+          autocomplete="off"
+          class="editableTextArea"
+          rows="1"
+          spellcheck="true"
+        >
+        </textarea>
+      </div>
+    `
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
index b445f75..11df7d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
@@ -6,7 +6,7 @@
 import './gr-avatar';
 import '../gr-hovercard-account/gr-hovercard-account';
 import {AccountInfo, ServerInfo} from '../../../types/common';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {
   isDetailedAccount,
@@ -97,7 +97,7 @@
     );
   }
 
-  override updated(changedProperties: PropertyValues) {
+  override willUpdate(changedProperties: PropertyValues) {
     if (changedProperties.has('accounts')) {
       if (
         this.forceFetch &&
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts
index a427924..dc162cf 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts
@@ -9,7 +9,7 @@
   createAccountWithId,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {stubRestApi} from '../../../test/test-utils';
 import {LitElement} from 'lit';
 import {AccountId, Timestamp} from '../../../api/rest-api';
@@ -33,7 +33,6 @@
             {
               url: `https://a.b.c/photo${i}.jpg`,
               height: 32,
-              width: 32,
             },
           ],
         });
@@ -45,7 +44,6 @@
             // Account with duplicate avatar will be skipped.
             url: 'https://a.b.c/photo1.jpg',
             height: 32,
-            width: 32,
           },
         ],
       });
@@ -92,7 +90,6 @@
             {
               url: `https://a.b.c/photo${i}.jpg`,
               height: 32,
-              width: 32,
             },
           ],
         });
@@ -104,7 +101,6 @@
             // Account with duplicate avatar will be skipped.
             url: 'https://a.b.c/photo1.jpg',
             height: 32,
-            width: 32,
           },
         ],
       });
@@ -152,7 +148,6 @@
           {
             url: 'https://a.b.c/photo0.jpg',
             height: 32,
-            width: 32,
           },
         ],
         registered_on: '1234' as Timestamp,
@@ -217,7 +212,6 @@
             {
               url: `https://a.b.c/photo${i}.jpg`,
               height: 32,
-              width: 32,
             },
           ],
         });
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
index 1ea2a64..6bc19cf 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -6,7 +6,7 @@
 import {getBaseUrl} from '../../../utils/url-util';
 import {AccountInfo} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
 import {resolve} from '../../../models/dependency';
@@ -72,9 +72,10 @@
     this.hidden = false;
 
     const url = this.buildAvatarURL(this.account);
-    if (url) {
-      this.style.backgroundImage = `url("${url}")`;
-    }
+    // Fallback to empty string to make sure that the user,
+    // which doesn't have an avatar set, does not reuse
+    // someone elses.
+    this.style.backgroundImage = url ? `url("${url}")` : '';
   }
 
   private getAccounts(account: AccountInfo) {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
index aa341ff..4a2175d 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
@@ -12,7 +12,7 @@
   createAccountWithId,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {isVisible, stubRestApi} from '../../../test/test-utils';
 
 suite('gr-avatar tests', () => {
@@ -21,7 +21,6 @@
     {
       url: 'https://cdn.example.com/s12-p/photo.jpg',
       height: 12,
-      width: 0,
     },
   ];
 
@@ -149,17 +148,14 @@
           {
             url: 'https://cdn.example.com/s12-p/photo.jpg',
             height: 12,
-            width: 0,
           },
           {
             url: 'https://cdn.example.com/s16-p/photo.jpg',
             height: 16,
-            width: 0,
           },
           {
             url: 'https://cdn.example.com/s100-p/photo.jpg',
             height: 100,
-            width: 0,
           },
         ],
       };
@@ -181,7 +177,6 @@
           {
             url: 'https://cdn.example.com/s95-p/photo.jpg',
             height: 95,
-            width: 0,
           },
         ],
       };
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 35d0e5d..78b9ea4 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -4,7 +4,6 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../gr-icon/gr-icon';
-import '@polymer/paper-button/paper-button';
 import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {votingStyles} from '../../../styles/gr-voting-styles';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
@@ -13,6 +12,8 @@
 import {getAppContext} from '../../../services/app-context';
 import {classMap} from 'lit/directives/class-map.js';
 import {Interaction} from '../../../constants/reporting';
+import '@material/web/button/elevated-button';
+import '@material/web/button/text-button';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -26,12 +27,9 @@
  */
 @customElement('gr-button')
 export class GrButton extends LitElement {
-  // Private but used in tests.
-  readonly reporting = getAppContext().reportingService;
-
   /**
    * Should this button be rendered as a vote chip? Then we are applying
-   * the .voteChip class (see gr-voting-styles) to the paper-button.
+   * the .voteChip class (see gr-voting-styles) to the md-text-button/md-elevated-button.
    */
   @property({type: Boolean, reflect: true})
   voteChip = false;
@@ -56,6 +54,9 @@
   @property({type: Boolean, reflect: true})
   disabled: boolean | null = null;
 
+  // Private but used in tests.
+  readonly reporting = getAppContext().reportingService;
+
   static override get styles() {
     return [
       votingStyles,
@@ -77,43 +78,69 @@
         :host([hidden]) {
           display: none;
         }
-        :host paper-button {
+        :host md-text-button,
+        :host md-elevated-button {
+          --md-text-button-label-text-font: var(--header-font-family);
+          --md-text-button-label-text-weight: var(--font-weight-medium);
+          --md-elevated-button-label-text-font: var(--header-font-family);
+          --md-elevated-button-label-text-weight: var(--font-weight-medium);
           text-transform: none;
+          /* This is also set in the button-label-(font|weight) css vars above. We keep this incase it is also needed. */
           font-weight: var(--font-weight-medium);
           font-family: var(--header-font-family);
+          color: var(--text-color);
+          --md-text-button-container-color: var(--text-color);
         }
-        paper-button {
-          /* paper-button sets this to anti-aliased, which appears different than
-            bold font elsewhere on macOS. */
-          -webkit-font-smoothing: initial;
+
+        md-text-button,
+        md-elevated-button {
+          --md-text-button-label-text-font: var(--font-family, inherit);
+          --md-text-button-label-text-weight: var(
+            --font-weight-normal,
+            inherit
+          );
+          --md-elevated-button-label-text-font: var(--font-family, inherit);
+          --md-elevated-button-label-text-weight: var(
+            --font-weight-normal,
+            inherit
+          );
+          --md-text-button-container-color: var(--background-color);
+          --md-text-button-hover-container-color: var(--background-color);
+          --md-elevated-button-container-color: var(--background-color);
+          --md-elevated-button-hover-container-color: var(--background-color);
+          --md-sys-color-primary: var(--text-color);
+          /* We set this to 0 which defaults to 12px under text-button.
+            This is because for some reason it makes the size of it bigger
+            which makes some of the buttons look wrong. E.g. attention set has
+            a bigger width and thus a lot of space. This fixes it. */
+          --md-text-button-leading-space: 0;
+          --md-text-button-trailing-space: 0;
+          /* Brings back the round corners we had with paper-button */
+          --md-text-button-container-shape: 4px;
+          --md-elevated-button-container-shape: 4px;
+          /* We have a variable for setting the text colour when it is disabled */
+          --md-elevated-button-disabled-label-text-color: var(--text-color);
+          --md-text-button-disabled-label-text-color: var(--text-color);
           align-items: center;
           background-color: var(--background-color);
           color: var(--text-color);
-          display: flex;
+          /* paper-button set this but md-(elevated|text)-button does not. So we set it. */
+          font: inherit;
+          /* This is also set in the button-label-(font|weight) css vars above. We keep this incase it is also needed. */
           font-family: var(--font-family, inherit);
-          /** Without this '.keyboard-focus' buttons will get bolded. */
           font-weight: var(--font-weight-normal, inherit);
+          display: flex;
           justify-content: center;
           margin: var(--margin, 0);
           min-width: var(--border, 0);
           padding: var(--gr-button-padding, var(--spacing-s) var(--spacing-m));
+          /* Needed to resize properly */
+          --md-elevated-button-container-height: none;
+          --md-text-button-container-height: none;
+          cursor: pointer;
         }
-        paper-button[elevation='1'] {
-          box-shadow: var(--elevation-level-1);
-        }
-        paper-button[elevation='2'] {
-          box-shadow: var(--elevation-level-2);
-        }
-        paper-button[elevation='3'] {
-          box-shadow: var(--elevation-level-3);
-        }
-        paper-button[elevation='4'] {
-          box-shadow: var(--elevation-level-4);
-        }
-        paper-button[elevation='5'] {
-          box-shadow: var(--elevation-level-5);
-        }
-        paper-button:hover {
+        :host md-text-button:hover,
+        :host md-elevated-button:hover {
           background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
             var(--background-color);
         }
@@ -159,27 +186,59 @@
           box-shadow: none;
           box-sizing: border-box;
           min-width: 3em;
-          color: var(--vote-text-color);
+          --md-sys-color-primary: var(--vote-text-color);
+        }
+        .loadingSpin {
+          display: inline-block;
+        }
+        .flex {
+          display: flex;
+          align-items: center;
         }
       `,
     ];
   }
 
   override render() {
-    return html`<paper-button
-      ?raised=${!this.link && !this.flatten}
-      ?disabled=${this.disabled || this.loading}
-      role="button"
-      tabindex="-1"
-      part="paper-button"
-      class=${classMap({
-        newVoteChip: this.voteChip,
-      })}
-    >
-      ${this.loading ? html`<span class="loadingSpin"></span>` : ''}
-      <slot></slot>
-      ${this.renderArrowIcon()}
-    </paper-button>`;
+    const buttonClass = classMap({
+      newVoteChip: this.voteChip,
+    });
+
+    if (!this.link && !this.flatten) {
+      return html`
+        <md-elevated-button
+          class=${buttonClass}
+          ?disabled=${this.disabled || this.loading}
+          part="md-elevated-button"
+          touch-target="none"
+          role="button"
+          tabindex="-1"
+        >
+          <div class="flex">
+            ${this.loading ? html`<span class="loadingSpin"></span>` : ''}
+            <slot></slot>
+            ${this.renderArrowIcon()}
+          </div>
+        </md-elevated-button>
+      `;
+    }
+
+    return html`
+      <md-text-button
+        class=${buttonClass}
+        ?disabled=${this.disabled || this.loading}
+        part="md-text-button"
+        touch-target="none"
+        role="button"
+        tabindex="-1"
+      >
+        <div class="flex">
+          ${this.loading ? html`<span class="loadingSpin"></span>` : ''}
+          <slot></slot>
+          ${this.renderArrowIcon()}
+        </div>
+      </md-text-button>
+    `;
   }
 
   renderArrowIcon() {
@@ -190,7 +249,7 @@
   constructor() {
     super();
     this.initialTabindex = this.getAttribute('tabindex') || '0';
-    this.addEventListener('click', e => this._handleAction(e));
+    this.addEventListener('click', e => this.handleAction(e));
     addShortcut(this, {key: Key.ENTER}, () => this.click());
     addShortcut(this, {key: Key.SPACE}, () => this.click());
   }
@@ -220,7 +279,7 @@
     }
   }
 
-  _handleAction(e: MouseEvent) {
+  private handleAction(e: MouseEvent) {
     if (this.disabled || this.loading) {
       e.preventDefault();
       e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
index f695ffa..ec48933 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
@@ -7,10 +7,10 @@
 import '../../../test/common-test-setup';
 import './gr-button';
 import {addListener} from '@polymer/polymer/lib/utils/gestures';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from './gr-button';
 import {pressKey, queryAndAssert} from '../../../test/test-utils';
-import {PaperButtonElement} from '@polymer/paper-button';
+import {MdTextButton} from '@material/web/button/text-button';
 import {Key, Modifier} from '../../../utils/dom-util';
 
 suite('gr-button tests', () => {
@@ -35,16 +35,17 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <paper-button
-          animated=""
-          aria-disabled="false"
-          elevation="1"
-          part="paper-button"
-          raised=""
-          role="button"
+        <md-elevated-button
+          data-role="button"
+          part="md-elevated-button"
           tabindex="-1"
-          ><slot></slot>
-        </paper-button>
+          touch-target="none"
+          value=""
+        >
+          <div class="flex">
+            <slot></slot>
+          </div>
+        </md-elevated-button>
       `
     );
   });
@@ -62,17 +63,17 @@
   });
 
   test('disabled is set by disabled', async () => {
-    const paperBtn = queryAndAssert<PaperButtonElement>(
+    const mdTextBtn = queryAndAssert<MdTextButton>(
       element,
-      'paper-button'
+      'md-elevated-button'
     );
-    assert.isFalse(paperBtn.disabled);
+    assert.isFalse(mdTextBtn.disabled);
     element.disabled = true;
     await element.updateComplete;
-    assert.isTrue(paperBtn.disabled);
+    assert.isTrue(mdTextBtn.disabled);
     element.disabled = false;
     await element.updateComplete;
-    assert.isFalse(paperBtn.disabled);
+    assert.isFalse(mdTextBtn.disabled);
   });
 
   test('loading set from listener', async () => {
@@ -82,18 +83,18 @@
       target.setAttribute('loading', 'true');
       resolve = () => target.removeAttribute('loading');
     });
-    const paperBtn = queryAndAssert<PaperButtonElement>(
+    const mdTextBtn = queryAndAssert<MdTextButton>(
       element,
-      'paper-button'
+      'md-elevated-button'
     );
-    assert.isFalse(paperBtn.disabled);
+    assert.isFalse(mdTextBtn.disabled);
     element.click();
     await element.updateComplete;
-    assert.isTrue(paperBtn.disabled);
+    assert.isTrue(mdTextBtn.disabled);
     assert.isTrue(element.hasAttribute('loading'));
     resolve!();
     await element.updateComplete;
-    assert.isFalse(paperBtn.disabled);
+    assert.isFalse(mdTextBtn.disabled);
     assert.isFalse(element.hasAttribute('loading'));
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index d00d54f..972a580 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -10,7 +10,7 @@
   ShortcutSection,
 } from '../../../services/shortcuts/shortcuts-config';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {resolve} from '../../../models/dependency';
 import {shortcutsServiceToken} from '../../../services/shortcuts/shortcuts-service';
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
index 75237f4..4c5fcdb 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -8,7 +8,7 @@
 import {GrChangeStar} from './gr-change-star';
 import './gr-change-star';
 import {createChange} from '../../../test/test-data-generators';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-change-star tests', () => {
   let element: GrChangeStar;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index ffd2d1f..9c694bf 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -8,7 +8,7 @@
 import '../../../styles/shared-styles';
 import {ChangeInfo, ChangeStates, WebLinkInfo} from '../../../types/common';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {createSearchUrl} from '../../../models/views/search';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
index 89d30ee..4106672 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
@@ -11,7 +11,7 @@
 import './gr-change-status';
 import {GrChangeStatus, WIP_TOOLTIP} from './gr-change-status';
 import {MERGE_CONFLICT_TOOLTIP} from './gr-change-status';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {queryAndAssert} from '../../../test/test-utils';
 import {ChangeStates} from '../../../types/common';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
index 9e112cb..f282514 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
@@ -11,7 +11,7 @@
 import {select} from '../../../utils/observable-util';
 import {Comment} from '../../../types/common';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import {NumericChangeId, isBase64FileContent} from '../../../api/rest-api';
+import {isBase64FileContent, NumericChangeId} from '../../../api/rest-api';
 import {assert, assertIsDefined} from '../../../utils/common-util';
 import {getContentInCommentRange} from '../../../utils/comment-util';
 import {SpecialFilePath} from '../../../constants/constants';
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index b08e9a5..ce87129 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -9,7 +9,7 @@
 import '../gr-icon/gr-icon';
 import '../../../embed/diff/gr-diff/gr-diff';
 import '../gr-copy-clipboard/gr-copy-clipboard';
-import {css, html, nothing, LitElement, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {
   customElement,
   property,
@@ -19,12 +19,12 @@
 } from 'lit/decorators.js';
 import {
   computeDiffFromContext,
-  getLastComment,
-  getFirstComment,
   createNewReply,
-  NEWLINE_PATTERN,
-  id,
+  getFirstComment,
+  getLastComment,
   hasUserSuggestion,
+  id,
+  NEWLINE_PATTERN,
 } from '../../../utils/comment-util';
 import {ChangeMessageId, FixSuggestionInfo} from '../../../api/rest-api';
 import {getAppContext} from '../../../services/app-context';
@@ -38,7 +38,6 @@
   Comment,
   CommentThread,
   isDraft,
-  isRobot,
   NumericChangeId,
   RepoName,
   UrlEncodedCommentId,
@@ -49,14 +48,14 @@
 import {
   CommentRangeLayer,
   DiffLayer,
-  Side,
   FILE,
   RenderPreferences,
+  Side,
 } from '../../../api/diff';
 import {
   assert,
   assertIsDefined,
-  copyToClipbard,
+  copyToClipboard,
   uuid,
 } from '../../../utils/common-util';
 import {fire, fireAlert} from '../../../utils/event-util';
@@ -84,7 +83,6 @@
 import {userModelToken} from '../../../models/user/user-model';
 import {highlightServiceToken} from '../../../services/highlight/highlight-service';
 import {noAwait, waitUntil} from '../../../utils/async-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {
   ReportSource,
   suggestionsServiceToken,
@@ -284,8 +282,6 @@
     () => getAppContext().reportingService
   );
 
-  private readonly flagsService = getAppContext().flagsService;
-
   constructor() {
     super();
     this.shortcuts.addGlobal({key: 'e'}, () => this.handleExpandShortcut());
@@ -404,9 +400,6 @@
         .comment-box.unresolved {
           background-color: var(--unresolved-comment-background-color);
         }
-        .comment-box.robotComment {
-          background-color: var(--robot-comment-background-color);
-        }
         #actionsContainer {
           display: flex;
         }
@@ -492,7 +485,6 @@
   override render() {
     if (!this.thread) return;
     const dynamicBoxClasses = {
-      robotComment: this.isRobotComment(),
       unresolved: this.unresolved,
       saving: this.saving,
     };
@@ -557,7 +549,6 @@
 
   private renderComment(comment?: Comment) {
     if (!comment) return nothing;
-    const robotButtonDisabled = !this.account || this.isDraft();
     const isFirstComment = this.getFirstComment() === comment;
     const initiallyCollapsed =
       !isDraft(comment) &&
@@ -570,7 +561,6 @@
         .comment=${comment}
         .comments=${this.thread!.comments}
         ?initially-collapsed=${initiallyCollapsed}
-        ?robot-button-disabled=${robotButtonDisabled}
         ?show-patchset=${this.showPatchset}
         ?show-ported-comment=${this.showPortedComment && isFirstComment}
         @reply-to-comment=${this.handleReplyToComment}
@@ -598,7 +588,7 @@
   }
 
   renderActions() {
-    if (!this.account || this.isDraft() || this.isRobotComment()) return;
+    if (!this.account || this.isDraft()) return;
     return html`
       <div id="actionsContainer">
         <span id="unresolvedLabel">${
@@ -625,22 +615,6 @@
           ${
             this.unresolved
               ? html`
-                  <gr-button
-                    id="ackBtn"
-                    link
-                    class="action ack"
-                    ?disabled=${this.saving}
-                    @click=${this.handleCommentAck}
-                    >Ack</gr-button
-                  >
-                  <gr-button
-                    id="doneBtn"
-                    link
-                    class="action done"
-                    ?disabled=${this.saving}
-                    @click=${this.handleCommentDone}
-                    >Done</gr-button
-                  >
                   ${this.shouldShowAIFixButton()
                     ? html`
                         <gr-button
@@ -657,6 +631,22 @@
                         >
                       `
                     : nothing}
+                  <gr-button
+                    id="ackBtn"
+                    link
+                    class="action ack"
+                    ?disabled=${this.saving}
+                    @click=${this.handleCommentAck}
+                    >Ack</gr-button
+                  >
+                  <gr-button
+                    id="doneBtn"
+                    link
+                    class="action done"
+                    ?disabled=${this.saving}
+                    @click=${this.handleCommentDone}
+                    >Done</gr-button
+                  >
                 `
               : ''
           }
@@ -864,7 +854,7 @@
       });
     }
     assertIsDefined(url, 'url for comment');
-    copyToClipbard(generateAbsoluteUrl(url), 'Link');
+    copyToClipboard(generateAbsoluteUrl(url), 'Link');
   }
 
   private getDisplayPath() {
@@ -881,10 +871,6 @@
     return '';
   }
 
-  private isRobotComment() {
-    return isRobot(this.getLastComment());
-  }
-
   private getFirstComment() {
     assertIsDefined(this.thread);
     return getFirstComment(this.thread);
@@ -1014,13 +1000,13 @@
       fireAlert(this, 'No suitable AI fix could be found');
       return;
     }
+    // Description is used to identify the suggestion source in logs.
+    suggestion.description =
+      ReportSource.GET_AI_FIX_FOR_COMMENT + ' ' + suggestion.description;
     this.suggestion = suggestion;
   }
 
   private shouldShowAIFixButton(): boolean {
-    if (!this.flagsService.isEnabled(KnownExperimentId.GET_AI_FIX)) {
-      return false;
-    }
     if (!this.thread || !this.account) return false;
     if (this.thread.comments.length !== 1) return false;
     const comment = this.thread.comments[0];
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_screenshot_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_screenshot_test.ts
new file mode 100644
index 0000000..5bd04d8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_screenshot_test.ts
@@ -0,0 +1,99 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {visualDiffDarkTheme} from '../../../test/test-utils';
+import {GrCommentThread} from './gr-comment-thread';
+import './gr-comment-thread';
+import {
+  CommentInfo,
+  DraftInfo,
+  NumericChangeId,
+  RepoName,
+  SavingState,
+  Timestamp,
+  UrlEncodedCommentId,
+} from '../../../types/common';
+import {
+  createAccountDetailWithId,
+  createThread,
+} from '../../../test/test-data-generators';
+import {
+  ChangeChildView,
+  changeViewModelToken,
+} from '../../../models/views/change';
+import {GerritView} from '../../../services/router/router-model';
+import {testResolver} from '../../../test/common-test-setup';
+
+const c1: CommentInfo = {
+  author: createAccountDetailWithId(1),
+  id: 'the-root' as UrlEncodedCommentId,
+  message: 'start the conversation',
+  updated: '2021-11-01 10:11:12.000000000' as Timestamp,
+};
+
+const c2: CommentInfo = {
+  author: createAccountDetailWithId(2),
+  id: 'the-reply' as UrlEncodedCommentId,
+  message: 'keep it going',
+  updated: '2021-11-02 10:11:12.000000000' as Timestamp,
+  in_reply_to: 'the-root' as UrlEncodedCommentId,
+};
+
+const c3: DraftInfo = {
+  author: createAccountDetailWithId(1),
+  id: 'the-draft' as UrlEncodedCommentId,
+  message: 'stop it',
+  updated: '2021-11-03 10:11:12.000000000' as Timestamp,
+  in_reply_to: 'the-reply' as UrlEncodedCommentId,
+  savingState: SavingState.OK,
+};
+
+suite('gr-comment-thread screenshot tests', () => {
+  let element: GrCommentThread;
+
+  setup(async () => {
+    testResolver(changeViewModelToken).setState({
+      view: GerritView.CHANGE,
+      childView: ChangeChildView.OVERVIEW,
+      changeNum: 1 as NumericChangeId,
+      repo: 'test-repo-name' as RepoName,
+    });
+    element = await fixture(html`<gr-comment-thread></gr-comment-thread>`);
+    element.changeNum = 1 as NumericChangeId;
+    element.showFileName = true;
+    element.showFilePath = true;
+    element.repoName = 'test-repo-name' as RepoName;
+    await element.updateComplete;
+  });
+
+  test('unresolved', async () => {
+    element.thread = createThread(c1, {...c2, unresolved: true});
+    await element.updateComplete;
+
+    await visualDiff(element, 'gr-comment-thread-unresolved');
+    await visualDiffDarkTheme(element, 'gr-comment-thread-unresolved');
+  });
+
+  test('resolved', async () => {
+    element.thread = createThread(c1, c2);
+    await element.updateComplete;
+
+    await visualDiff(element, 'gr-comment-thread-resolved');
+    await visualDiffDarkTheme(element, 'gr-comment-thread-resolved');
+  });
+
+  test('with draft', async () => {
+    element.thread = createThread(c1, c2, c3);
+    await element.updateComplete;
+
+    await visualDiff(element, 'gr-comment-thread-with-draft');
+    await visualDiffDarkTheme(element, 'gr-comment-thread-with-draft');
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 44f4f84..61c23d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -8,36 +8,35 @@
 import './gr-comment-thread';
 import {sortComments} from '../../../utils/comment-util';
 import {GrCommentThread} from './gr-comment-thread';
-import {getAppContext} from '../../../services/app-context';
 import {
-  NumericChangeId,
-  UrlEncodedCommentId,
-  Timestamp,
   CommentInfo,
-  RepoName,
-  DraftInfo,
-  SavingState,
   CommentThread,
+  DraftInfo,
+  NumericChangeId,
+  RepoName,
   RevisionPatchSetNum,
+  SavingState,
+  Timestamp,
+  UrlEncodedCommentId,
 } from '../../../types/common';
 import {
+  MockPromise,
   mockPromise,
+  query,
   queryAndAssert,
   stubRestApi,
-  waitUntilCalled,
   waitUntil,
-  MockPromise,
-  query,
+  waitUntilCalled,
 } from '../../../test/test-utils';
 import {
   createAccountDetailWithId,
-  createThread,
-  createNewDraft,
   createComment,
   createFixSuggestionInfo,
+  createNewDraft,
+  createThread,
 } from '../../../test/test-data-generators';
 import {SinonStubbedMember} from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
 import {SpecialFilePath} from '../../../constants/constants';
 import {GrIcon} from '../gr-icon/gr-icon';
@@ -53,7 +52,6 @@
 import {GerritView} from '../../../services/router/router-model';
 import {GrComment} from '../gr-comment/gr-comment';
 import {GrSuggestionTextarea} from '../gr-suggestion-textarea/gr-suggestion-textarea';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {suggestionsServiceToken} from '../../../services/suggestions/suggestions-service';
 
 const c1: CommentInfo = {
@@ -134,20 +132,14 @@
             <gr-comment
               collapsed=""
               initially-collapsed=""
-              robot-button-disabled=""
               show-patchset=""
             ></gr-comment>
             <gr-comment
               collapsed=""
               initially-collapsed=""
-              robot-button-disabled=""
               show-patchset=""
             ></gr-comment>
-            <gr-comment
-              class="draft"
-              robot-button-disabled=""
-              show-patchset=""
-            ></gr-comment>
+            <gr-comment class="draft" show-patchset=""></gr-comment>
           </div>
         </div>
       `
@@ -172,11 +164,7 @@
         <div id="container">
           <h3 class="assistive-tech-only">Draft Comment thread by Yoda</h3>
           <div class="comment-box" tabindex="0">
-            <gr-comment
-              class="draft"
-              robot-button-disabled=""
-              show-patchset=""
-            ></gr-comment>
+            <gr-comment class="draft" show-patchset=""></gr-comment>
           </div>
         </div>
       `
@@ -395,6 +383,7 @@
       queryAndAssert<GrButton>(element, '#quoteBtn').click();
       assert.isTrue(stubAdd.called);
       assert.equal(stubAdd.lastCall.firstArg.message, '');
+      // @ts-ignore
       assert.isTrue(addQuoteStub.calledWith('> keep it going\n\n'));
     });
 
@@ -611,11 +600,6 @@
 
   suite('Get AI fix button', () => {
     setup(async () => {
-      const flagsService = getAppContext().flagsService;
-      sinon
-        .stub(flagsService, 'isEnabled')
-        .callsFake(id => id === KnownExperimentId.GET_AI_FIX);
-
       const suggestionsService = testResolver(suggestionsServiceToken);
       sinon
         .stub(suggestionsService, 'isGeneratedSuggestedFixEnabled')
@@ -662,6 +646,16 @@
                   </gr-button>
                   <gr-button
                     aria-disabled="false"
+                    class="action ai-fix"
+                    id="aiFixBtn"
+                    link=""
+                    role="button"
+                    tabindex="0"
+                  >
+                    Get AI Fix
+                  </gr-button>
+                  <gr-button
+                    aria-disabled="false"
                     class="action ack"
                     id="ackBtn"
                     link=""
@@ -680,16 +674,6 @@
                   >
                     Done
                   </gr-button>
-                  <gr-button
-                    aria-disabled="false"
-                    class="action ai-fix"
-                    id="aiFixBtn"
-                    link=""
-                    role="button"
-                    tabindex="0"
-                  >
-                    Get AI Fix
-                  </gr-button>
                   <gr-icon
                     icon="link"
                     class="copy link-icon"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 5774d02..2881ac5 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -23,17 +23,15 @@
 import {GrSuggestionTextarea} from '../gr-suggestion-textarea/gr-suggestion-textarea';
 import {
   AccountDetailInfo,
+  Comment,
+  CommentInput,
   DraftInfo,
+  isDraft,
+  isError,
+  isNew,
+  isSaving,
   NumericChangeId,
   RepoName,
-  RobotCommentInfo,
-  Comment,
-  isRobot,
-  isSaving,
-  isError,
-  isDraft,
-  isNew,
-  CommentInput,
 } from '../../../types/common';
 import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
 import {
@@ -43,7 +41,6 @@
   hasUserSuggestion,
   id,
   isFileLevelComment,
-  NEWLINE_PATTERN,
   USER_SUGGESTION_START_PATTERN,
 } from '../../../utils/comment-util';
 import {
@@ -52,7 +49,7 @@
   ValueChangedEvent,
 } from '../../../types/events';
 import {fire} from '../../../utils/event-util';
-import {assertIsDefined, assert, uuid} from '../../../utils/common-util';
+import {assert, assertIsDefined, uuid} from '../../../utils/common-util';
 import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -96,8 +93,10 @@
   ReportSource,
   suggestionsServiceToken,
 } from '../../../services/suggestions/suggestions-service';
-import {SuggestionsProvider} from '../../../api/suggestions';
+import {ResponseCode, SuggestionsProvider} from '../../../api/suggestions';
 import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 // visible for testing
 export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
@@ -178,7 +177,7 @@
   comment?: Comment;
 
   // TODO: Move this out of gr-comment. gr-comment should not have a comments
-  // property. This is only used for hasHumanReply at the moment.
+  // property.
   @property({type: Array})
   comments?: Comment[];
 
@@ -202,9 +201,6 @@
   @property({type: Boolean, reflect: true})
   collapsed?: boolean;
 
-  @property({type: Boolean, attribute: 'robot-button-disabled'})
-  robotButtonDisabled = false;
-
   @property({type: String})
   messagePlaceholder?: string;
 
@@ -273,6 +269,9 @@
   @state()
   suggestionLoading = false;
 
+  @state()
+  private wasSuggestionEdited = false;
+
   @property({type: Boolean, attribute: 'show-patchset'})
   showPatchset = false;
 
@@ -452,16 +451,18 @@
         this.autocompleteComment();
       }
     );
-    subscribe(
-      this,
-      () =>
-        this.generateSuggestionTrigger$.pipe(
-          debounceTime(GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS)
-        ),
-      () => {
-        this.generateSuggestEdit();
-      }
-    );
+    if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)) {
+      subscribe(
+        this,
+        () =>
+          this.generateSuggestionTrigger$.pipe(
+            debounceTime(GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS)
+          ),
+        () => {
+          this.generateSuggestEdit();
+        }
+      );
+    }
     subscribe(
       this,
       () => this.getUserModel().preferences$,
@@ -473,6 +474,14 @@
         ) {
           this.generateSuggestion = !!prefs.allow_suggest_code_while_commenting;
         }
+        if (
+          this.flagsService.isEnabled(
+            KnownExperimentId.ML_SUGGESTED_EDIT_UNCHECK_BY_DEFAULT
+          ) &&
+          this.generateSuggestion
+        ) {
+          this.generateSuggestion = false;
+        }
       }
     );
     subscribe(
@@ -510,6 +519,7 @@
       formStyles,
       sharedStyles,
       modalStyles,
+      materialStyles,
       css`
         :host {
           display: block;
@@ -558,18 +568,11 @@
         span.date:hover {
           text-decoration: underline;
         }
-        .actions,
-        .robotActions {
+        .actions {
           display: flex;
           justify-content: space-between;
           padding-top: 0;
         }
-        .robotActions {
-          /* Better than the negative margin would be to remove the gr-button
-       * padding, but then we would also need to fix the buttons that are
-       * inserted by plugins. :-/ */
-          margin: 4px 0 -4px;
-        }
         .action {
           margin-left: var(--spacing-l);
         }
@@ -590,18 +593,8 @@
         .show-hide {
           margin-left: var(--spacing-s);
         }
-        .robotId {
-          color: var(--deemphasized-text-color);
-          margin-bottom: var(--spacing-m);
-        }
-        .robotRun {
-          margin-left: var(--spacing-m);
-        }
-        .robotRunLink {
-          margin-left: var(--spacing-m);
-        }
         /* just for a11y */
-        input.show-hide {
+        md-checkbox.show-hide {
           display: none;
         }
         label.show-hide {
@@ -734,10 +727,9 @@
         <div id="container" class=${classMap(classes)}>
           ${this.renderHeader()}
           <div class="body">
-            ${this.renderRobotAuthor()} ${this.renderEditingTextarea()}
-            ${this.renderCommentMessage()}
+            ${this.renderEditingTextarea()} ${this.renderCommentMessage()}
             <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
-            ${this.renderHumanActions()} ${this.renderRobotActions()}
+            ${this.renderHumanActions()}
           </div>
           ${/* if this.editing */ this.renderGeneratedSuggestionPreview()}
           ${/* if !this.editing */ this.renderFixSuggestionPreview()}
@@ -760,19 +752,15 @@
           ${this.renderDraftLabel()}
         </div>
         <div class="headerMiddle">${this.renderCollapsedContent()}</div>
-        ${this.renderSuggestEditButton()} ${this.renderRunDetails()}
-        ${this.renderDeleteButton()} ${this.renderPatchset()}
-        ${this.renderSeparator()} ${this.renderDate()} ${this.renderToggle()}
+        ${this.renderSuggestEditButton()} ${this.renderDeleteButton()}
+        ${this.renderPatchset()} ${this.renderSeparator()} ${this.renderDate()}
+        ${this.renderToggle()}
       </div>
     `;
   }
 
   private renderAuthor() {
     if (isDraft(this.comment)) return;
-    if (isRobot(this.comment)) {
-      const id = this.comment.robot_id;
-      return html`<span class="robotName">${id}</span>`;
-    }
     return html`
       <gr-account-label .account=${this.comment?.author ?? this.account}>
       </gr-account-label>
@@ -822,26 +810,12 @@
     `;
   }
 
-  private renderRunDetails() {
-    if (!isRobot(this.comment)) return;
-    if (!this.comment?.url || this.collapsed) return;
-    return html`
-      <div class="runIdMessage message">
-        <div class="runIdInformation">
-          <a class="robotRunLink" href=${this.comment.url}>
-            <span class="robotRun link">Run Details</span>
-          </a>
-        </div>
-      </div>
-    `;
-  }
-
   /**
    * Deleting a comment is an admin feature. It means more than just discarding
    * a draft. It is an action applied to published comments.
    */
   private renderDeleteButton() {
-    if (!this.isAdmin || isDraft(this.comment) || isRobot(this.comment)) return;
+    if (!this.isAdmin || isDraft(this.comment)) return;
     if (this.collapsed) return;
     return html`
       <gr-button
@@ -903,23 +877,17 @@
     return html`
       <div class="show-hide" tabindex="0">
         <label class="show-hide" aria-label=${ariaLabel}>
-          <input
-            type="checkbox"
+          <md-checkbox
             class="show-hide"
-            ?checked=${this.collapsed}
+            ?checked=${!!this.collapsed}
             @change=${() => (this.collapsed = !this.collapsed)}
-          />
+          ></md-checkbox>
           <gr-icon icon=${icon} id="icon"></gr-icon>
         </label>
       </div>
     `;
   }
 
-  private renderRobotAuthor() {
-    if (!isRobot(this.comment) || this.collapsed) return;
-    return html`<div class="robotId">${this.comment.author?.name}</div>`;
-  }
-
   private renderEditingTextarea() {
     if (!this.editing || this.collapsed) return;
     return html`
@@ -930,7 +898,7 @@
         code=""
         rows="4"
         .placeholder=${this.messagePlaceholder}
-        text=${this.messageText}
+        .text=${this.messageText}
         autocompleteHint=${this.autocompleteHint?.commentCompletion ?? ''}
         @text-changed=${this.handleTextChanged}
         @hintShown=${this.handleHintShown}
@@ -1053,19 +1021,18 @@
   }
 
   private renderHumanActions() {
-    if (!this.account || isRobot(this.comment)) return;
+    if (!this.account) return;
     if (this.collapsed || !isDraft(this.comment)) return;
     return html`
       <div class="actions">
         <div class="leftActions">
           <div class="action resolve">
             <label>
-              <input
-                type="checkbox"
+              <md-checkbox
                 id="resolvedCheckbox"
-                .checked=${!this.unresolved}
+                ?checked=${!this.unresolved}
                 @change=${this.handleToggleResolved}
-              />
+              ></md-checkbox>
               Resolved
             </label>
           </div>
@@ -1154,21 +1121,19 @@
   }
 
   private renderFixSuggestionPreview() {
-    if (
-      !this.comment?.fix_suggestions ||
-      this.editing ||
-      isRobot(this.comment) ||
-      this.collapsed
-    )
+    if (!this.comment?.fix_suggestions || this.editing || this.collapsed) {
       return nothing;
+    }
     return html`<gr-fix-suggestions
       .comment=${this.comment}
+      @generated-suggestion-changed=${this.handleFixSuggestionChanged}
     ></gr-fix-suggestions>`;
   }
 
   // private but used in test
   showGeneratedSuggestion() {
     return (
+      this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2) &&
       this.getSuggestionsService().isGeneratedSuggestedFixEnabledForComment(
         this.comment
       ) &&
@@ -1194,7 +1159,7 @@
         .uuid=${this.generatedSuggestionId}
         .fixSuggestionInfo=${this.generatedFixSuggestion}
         .patchSet=${this.comment?.patch_set}
-        .commentId=${this.comment?.id}
+        .commentId=${id(this.comment)}
         @preview-loaded=${(event: CustomEvent<PreviewLoadedDetail>) =>
           (this.previewedGeneratedFixSuggestion =
             event.detail.previewLoadedFor)}
@@ -1212,19 +1177,21 @@
       'Select to show a generated suggestion based on your comment for commented text. This suggestion can be inserted as a code block in your comment.';
     return html`
       <div class="action">
-        <label title=${tooltip}>
-          <input
-            type="checkbox"
+        <label title=${tooltip} class="suggestEdit">
+          <md-checkbox
             id="generateSuggestCheckbox"
             ?checked=${this.generateSuggestion}
             @change=${() => {
               this.generateSuggestion = !this.generateSuggestion;
+              // Reset so suggestion can be re-generated.
+              this.wasSuggestionEdited = false;
               if (this.comment?.id) {
                 this.getStorage().setEditableContentItem(
                   ENABLE_GENERATE_SUGGESTION_STORAGE_KEY + this.comment.id,
                   this.generateSuggestion.toString()
                 );
               }
+              const suggestionToReport = this.generatedFixSuggestion;
               if (this.generateSuggestion) {
                 this.generateSuggestionTrigger$.next();
               } else {
@@ -1234,10 +1201,18 @@
               this.reporting.reportInteraction(
                 this.generateSuggestion
                   ? Interaction.GENERATE_SUGGESTION_ENABLED
-                  : Interaction.GENERATE_SUGGESTION_DISABLED
+                  : Interaction.GENERATE_SUGGESTION_DISABLED,
+                {
+                  commentId: id(this.comment!),
+                  fileExtension: getFileExtension(this.comment?.path ?? ''),
+                  uuid: this.generatedSuggestionId,
+                  replacement: suggestionToReport?.replacements
+                    ?.map(r => r.replacement)
+                    .join('\n'),
+                }
               );
             }}
-          />
+          ></md-checkbox>
           Attach AI-suggested fix
           ${when(
             this.suggestionLoading,
@@ -1264,9 +1239,6 @@
   }
 
   private getNumberOfSuggestions() {
-    if (!this.generateSuggestion) {
-      return '';
-    }
     if (this.generatedFixSuggestion) {
       return '(1)';
     } else {
@@ -1274,11 +1246,13 @@
     }
   }
 
-  private async generateSuggestEdit() {
+  // private used in test
+  async generateSuggestEdit() {
     if (
+      !this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2) ||
       !this.showGeneratedSuggestion() ||
-      !this.generateSuggestion ||
-      this.messageText.length === 0
+      this.messageText.length === 0 ||
+      this.wasSuggestionEdited
     )
       return;
     this.generatedSuggestionId = uuid();
@@ -1308,13 +1282,9 @@
     }
   }
 
-  private async autocompleteComment() {
-    const enabled = this.flagsService.isEnabled(
-      KnownExperimentId.COMMENT_AUTOCOMPLETION
-    );
-    if (!enabled || !this.autocompleteEnabled) {
-      return;
-    }
+  // private but visible for testing
+  async autocompleteComment() {
+    if (!this.autocompleteEnabled) return;
     const commentText = this.messageText;
     const context = await this.getSuggestionsService().autocompleteComment(
       this.comment,
@@ -1326,6 +1296,8 @@
       Interaction.COMMENT_COMPLETION_SUGGESTION_FETCHED,
       {...context, hasDraftChanged: this.messageText !== commentText}
     );
+    if (!context.commentCompletion) return;
+    if (context.responseCode !== ResponseCode.OK) return;
     // Note that we are setting the cache value for `commentText` and getting the value
     // for `this.messageText`.
     this.autocompleteCache.set(context);
@@ -1341,51 +1313,6 @@
     };
   }
 
-  private renderRobotActions() {
-    if (!this.account || !isRobot(this.comment)) return;
-    const endpoint = html`
-      <gr-endpoint-decorator name="robot-comment-controls">
-        <gr-endpoint-param name="comment" .value=${this.comment}>
-        </gr-endpoint-param>
-      </gr-endpoint-decorator>
-    `;
-    return html`
-      <div class="robotActions">
-        ${this.renderCopyLinkIcon()} ${endpoint} ${this.renderShowFixButton()}
-        ${this.renderPleaseFixButton()}
-      </div>
-    `;
-  }
-
-  private renderShowFixButton() {
-    const fix_suggestions = (this.comment as RobotCommentInfo)?.fix_suggestions;
-    if (!fix_suggestions || fix_suggestions.length === 0) return;
-    return html`
-      <gr-button
-        link
-        secondary
-        class="action show-fix"
-        @click=${() => this.handleShowFix()}
-      >
-        Show Fix
-      </gr-button>
-    `;
-  }
-
-  private renderPleaseFixButton() {
-    if (this.hasHumanReply()) return;
-    return html`
-      <gr-button
-        link
-        ?disabled=${this.robotButtonDisabled}
-        class="action fix"
-        @click=${this.handlePleaseFix}
-      >
-        Please Fix
-      </gr-button>
-    `;
-  }
-
   private renderConfirmDialog() {
     return html`
       <dialog id="confirmDeleteModal" tabindex="-1">
@@ -1502,15 +1429,6 @@
     this.messageText = quote + this.messageText;
   }
 
-  // TODO: Move this out of gr-comment. gr-comment should not have a comments
-  // property.
-  private hasHumanReply() {
-    if (!this.comment || !this.comments) return false;
-    return this.comments.some(
-      c => c.in_reply_to && c.in_reply_to === this.comment?.id && !isRobot(c)
-    );
-  }
-
   // private, but visible for testing
   async createFixPreview(
     replacement?: string
@@ -1546,23 +1464,6 @@
         ],
       };
     }
-    if (
-      isRobot(this.comment) &&
-      this.comment.fix_suggestions &&
-      this.comment.fix_suggestions.length > 0
-    ) {
-      const id = this.comment.robot_id;
-      return {
-        fixSuggestions: this.comment.fix_suggestions.map(s => {
-          return {
-            ...s,
-            description: `${id ?? ''} - ${s.description ?? ''}`,
-          };
-        }),
-        patchNum: this.comment.patch_set,
-        onCloseFixPreviewCallbacks: [],
-      };
-    }
     throw new Error('unable to create preview fix event');
   }
 
@@ -1619,19 +1520,6 @@
     }
   }
 
-  private handlePleaseFix() {
-    const message = this.comment?.message;
-    assert(!!message, 'empty message');
-    const quoted = message.replace(NEWLINE_PATTERN, '\n> ');
-    const eventDetail: ReplyToCommentEventDetail = {
-      content: `> ${quoted}\n\nPlease fix.`,
-      userWantsToEdit: false,
-      unresolved: true,
-    };
-    // Handled by <gr-comment-thread>.
-    fire(this, 'reply-to-comment', eventDetail);
-  }
-
   private handleAppliedFix() {
     const message = this.comment?.message;
     assert(!!message, 'empty message');
@@ -1769,6 +1657,8 @@
   }
 
   getFixSuggestions(): FixSuggestionInfo[] | undefined {
+    if (!this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2))
+      return undefined;
     if (!this.generateSuggestion) return undefined;
     if (!this.generatedFixSuggestion) return undefined;
     // Disable fix suggestions when the comment already has a user suggestion
@@ -1779,9 +1669,31 @@
     return undefined;
   }
 
+  private handleFixSuggestionChanged(
+    e: CustomEvent<{suggestions: FixSuggestionInfo[]}>
+  ) {
+    if (!this.comment || !isDraft(this.comment)) return;
+    if (!this.previewedGeneratedFixSuggestion) return;
+    // Since it's user edited suggestion, it's already previewed.
+    this.previewedGeneratedFixSuggestion = e.detail.suggestions?.[0];
+    this.generatedFixSuggestion = e.detail.suggestions?.[0];
+    this.wasSuggestionEdited = true;
+    this.rawSave({showToast: false});
+  }
+
   private handleToggleResolved() {
     this.unresolved = !this.unresolved;
-    if (!this.editing) {
+    if (this.editing) {
+      if (
+        this.messageText.trimEnd() === '' &&
+        this.comment?.path !== SpecialFilePath.PATCHSET_LEVEL_COMMENTS
+      ) {
+        this.messageText = this.unresolved
+          ? 'Marked as unresolved.'
+          : 'Marked as resolved.';
+        this.autoSaveTrigger$.next();
+      }
+    } else {
       // messageText is only assigned a value if the comment reaches editing
       // state, however it is possible that the user toggles the resolved state
       // without editing the comment in which case we assign the correct value
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 41c7c4e..1f5a9eb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -8,48 +8,48 @@
 import './gr-comment';
 import {AUTO_SAVE_DEBOUNCE_DELAY_MS, GrComment} from './gr-comment';
 import {
-  queryAndAssert,
-  stubRestApi,
-  query,
-  pressKey,
-  listenOnce,
-  mockPromise,
-  waitUntilCalled,
   dispatch,
   MockPromise,
+  mockPromise,
+  pressKey,
+  query,
+  queryAndAssert,
   stubFlags,
+  stubRestApi,
   waitUntil,
+  waitUntilCalled,
 } from '../../../test/test-utils';
 import {
   AccountId,
   DraftInfo,
-  SavingState,
   EmailAddress,
+  FixId,
   NumericChangeId,
   PatchSetNum,
+  SavingState,
   Timestamp,
   UrlEncodedCommentId,
-  FixId,
 } from '../../../types/common';
 import {
   createComment,
   createDraft,
-  createRobotComment,
   createNewDraft,
 } from '../../../test/test-data-generators';
-import {ReplyToCommentEvent} from '../../../types/events';
 import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
 import {assertIsDefined} from '../../../utils/common-util';
 import {Key, Modifier} from '../../../utils/dom-util';
 import {SinonStub, SinonStubbedMember} from 'sinon';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
 import {testResolver} from '../../../test/common-test-setup';
 import {
   CommentsModel,
   commentsModelToken,
 } from '../../../models/comments/comments-model';
+import {KnownExperimentId} from '../../../services/flags/flags';
 import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {ResponseCode} from '../../../api/suggestions';
+import {suggestionsServiceToken} from '../../../services/suggestions/suggestions-service';
 
 suite('gr-comment tests', () => {
   let element: GrComment;
@@ -98,30 +98,30 @@
         initiallyCollapsedElement,
         /* HTML */ `
           <gr-endpoint-decorator name="comment">
-            <gr-endpoint-param name="comment"></gr-endpoint-param>
-            <gr-endpoint-param name="editing"></gr-endpoint-param>
-            <gr-endpoint-param name="message"></gr-endpoint-param>
-            <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+            <gr-endpoint-param name="comment"> </gr-endpoint-param>
+            <gr-endpoint-param name="editing"> </gr-endpoint-param>
+            <gr-endpoint-param name="message"> </gr-endpoint-param>
+            <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
             <div class="container" id="container">
               <div class="header" id="header">
                 <div class="headerLeft">
-                  <gr-account-label deselected=""></gr-account-label>
+                  <gr-account-label deselected=""> </gr-account-label>
                 </div>
                 <div class="headerMiddle">
                   <span class="collapsedContent">
                     This is the test comment message.
                   </span>
                 </div>
-                <span class="patchset-text">Patchset 1</span>
+                <span class="patchset-text"> Patchset 1 </span>
                 <div class="show-hide" tabindex="0">
                   <label aria-label="Expand" class="show-hide">
-                    <input checked="" class="show-hide" type="checkbox" />
-                    <gr-icon id="icon" icon="expand_more"></gr-icon>
+                    <md-checkbox checked="" class="show-hide"> </md-checkbox>
+                    <gr-icon icon="expand_more" id="icon"> </gr-icon>
                   </label>
                 </div>
               </div>
               <div class="body">
-                <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+                <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
               </div>
             </div>
           </gr-endpoint-decorator>
@@ -140,107 +140,31 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="comment">
-            <gr-endpoint-param name="comment"></gr-endpoint-param>
-            <gr-endpoint-param name="editing"></gr-endpoint-param>
-            <gr-endpoint-param name="message"></gr-endpoint-param>
-            <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+            <gr-endpoint-param name="comment"> </gr-endpoint-param>
+            <gr-endpoint-param name="editing"> </gr-endpoint-param>
+            <gr-endpoint-param name="message"> </gr-endpoint-param>
+            <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
             <div class="container" id="container">
               <div class="header" id="header">
                 <div class="headerLeft">
-                  <gr-account-label deselected=""></gr-account-label>
+                  <gr-account-label deselected=""> </gr-account-label>
                 </div>
                 <div class="headerMiddle"></div>
-                <span class="patchset-text">Patchset 1</span>
-                <span class="separator"></span>
+                <span class="patchset-text"> Patchset 1 </span>
+                <span class="separator"> </span>
                 <span class="date" tabindex="0">
-                  <gr-date-formatter withtooltip=""></gr-date-formatter>
+                  <gr-date-formatter withtooltip=""> </gr-date-formatter>
                 </span>
                 <div class="show-hide" tabindex="0">
                   <label aria-label="Collapse" class="show-hide">
-                    <input class="show-hide" type="checkbox" />
-                    <gr-icon id="icon" icon="expand_less"></gr-icon>
+                    <md-checkbox class="show-hide"> </md-checkbox>
+                    <gr-icon icon="expand_less" id="icon"> </gr-icon>
                   </label>
                 </div>
               </div>
               <div class="body">
-                <gr-formatted-text class="message"></gr-formatted-text>
-                <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
-              </div>
-            </div>
-          </gr-endpoint-decorator>
-          <dialog id="confirmDeleteModal" tabindex="-1">
-            <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
-            </gr-confirm-delete-comment-dialog>
-          </dialog>
-        `
-      );
-    });
-
-    test('renders expanded robot', async () => {
-      element.initiallyCollapsed = false;
-      element.comment = createRobotComment();
-      await element.updateComplete;
-      assert.shadowDom.equal(
-        element,
-        /* HTML */ `
-          <gr-endpoint-decorator name="comment">
-            <gr-endpoint-param name="comment"></gr-endpoint-param>
-            <gr-endpoint-param name="editing"></gr-endpoint-param>
-            <gr-endpoint-param name="message"></gr-endpoint-param>
-            <gr-endpoint-param name="isDraft"></gr-endpoint-param>
-            <div class="container" id="container">
-              <div class="header" id="header">
-                <div class="headerLeft">
-                  <span class="robotName">robot-id-123</span>
-                </div>
-                <div class="headerMiddle"></div>
-                <span class="patchset-text">Patchset 1</span>
-                <span class="separator"></span>
-                <span class="date" tabindex="0">
-                  <gr-date-formatter withtooltip=""></gr-date-formatter>
-                </span>
-                <div class="show-hide" tabindex="0">
-                  <label aria-label="Collapse" class="show-hide">
-                    <input class="show-hide" type="checkbox" />
-                    <gr-icon id="icon" icon="expand_less"></gr-icon>
-                  </label>
-                </div>
-              </div>
-              <div class="body">
-                <div class="robotId"></div>
-                <gr-formatted-text class="message"></gr-formatted-text>
-                <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
-                <div class="robotActions">
-                  <gr-icon
-                    icon="link"
-                    class="copy link-icon"
-                    role="button"
-                    tabindex="0"
-                    title="Copy link to this comment"
-                  ></gr-icon>
-                  <gr-endpoint-decorator name="robot-comment-controls">
-                    <gr-endpoint-param name="comment"></gr-endpoint-param>
-                  </gr-endpoint-decorator>
-                  <gr-button
-                    aria-disabled="false"
-                    class="action show-fix"
-                    link=""
-                    role="button"
-                    secondary=""
-                    tabindex="0"
-                  >
-                    Show Fix
-                  </gr-button>
-                  <gr-button
-                    aria-disabled="false"
-                    class="action fix"
-                    link=""
-                    role="button"
-                    tabindex="0"
-                  >
-                    Please Fix
-                  </gr-button>
-                </div>
+                <gr-formatted-text class="message"> </gr-formatted-text>
+                <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
               </div>
             </div>
           </gr-endpoint-decorator>
@@ -282,10 +206,10 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="comment">
-            <gr-endpoint-param name="comment"></gr-endpoint-param>
-            <gr-endpoint-param name="editing"></gr-endpoint-param>
-            <gr-endpoint-param name="message"></gr-endpoint-param>
-            <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+            <gr-endpoint-param name="comment"> </gr-endpoint-param>
+            <gr-endpoint-param name="editing"> </gr-endpoint-param>
+            <gr-endpoint-param name="message"> </gr-endpoint-param>
+            <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
             <div class="container draft" id="container">
               <div class="header" id="header">
                 <div class="headerLeft">
@@ -295,31 +219,32 @@
                     max-width="20em"
                     title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
                   >
-                    <gr-icon filled icon="rate_review"></gr-icon>
-                    <span class="draftLabel">Draft</span>
+                    <gr-icon filled="" icon="rate_review"> </gr-icon>
+                    <span class="draftLabel"> Draft </span>
                   </gr-tooltip-content>
                 </div>
                 <div class="headerMiddle"></div>
-                <span class="patchset-text">Patchset 1</span>
-                <span class="separator"></span>
+                <span class="patchset-text"> Patchset 1 </span>
+                <span class="separator"> </span>
                 <span class="date" tabindex="0">
-                  <gr-date-formatter withtooltip=""></gr-date-formatter>
+                  <gr-date-formatter withtooltip=""> </gr-date-formatter>
                 </span>
                 <div class="show-hide" tabindex="0">
                   <label aria-label="Collapse" class="show-hide">
-                    <input class="show-hide" type="checkbox" />
-                    <gr-icon id="icon" icon="expand_less"></gr-icon>
+                    <md-checkbox class="show-hide"> </md-checkbox>
+                    <gr-icon icon="expand_less" id="icon"> </gr-icon>
                   </label>
                 </div>
               </div>
               <div class="body">
-                <gr-formatted-text class="message"></gr-formatted-text>
-                <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+                <gr-formatted-text class="message"> </gr-formatted-text>
+                <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
                 <div class="actions">
                   <div class="leftActions">
                     <div class="action resolve">
                       <label>
-                        <input id="resolvedCheckbox" type="checkbox" />
+                        <md-checkbox checked="" id="resolvedCheckbox">
+                        </md-checkbox>
                         Resolved
                       </label>
                     </div>
@@ -367,10 +292,10 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="comment">
-            <gr-endpoint-param name="comment"></gr-endpoint-param>
-            <gr-endpoint-param name="editing"></gr-endpoint-param>
-            <gr-endpoint-param name="message"></gr-endpoint-param>
-            <gr-endpoint-param name="isDraft"></gr-endpoint-param>
+            <gr-endpoint-param name="comment"> </gr-endpoint-param>
+            <gr-endpoint-param name="editing"> </gr-endpoint-param>
+            <gr-endpoint-param name="message"> </gr-endpoint-param>
+            <gr-endpoint-param name="isDraft"> </gr-endpoint-param>
             <div class="container draft" id="container">
               <div class="header" id="header">
                 <div class="headerLeft">
@@ -380,8 +305,8 @@
                     max-width="20em"
                     title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
                   >
-                    <gr-icon filled icon="rate_review"></gr-icon>
-                    <span class="draftLabel">Draft</span>
+                    <gr-icon filled="" icon="rate_review"> </gr-icon>
+                    <span class="draftLabel"> Draft </span>
                   </gr-tooltip-content>
                 </div>
                 <div class="headerMiddle"></div>
@@ -396,15 +321,15 @@
                   <gr-icon filled="" icon="edit" id="icon"> </gr-icon>
                   Suggest Edit
                 </gr-button>
-                <span class="patchset-text">Patchset 1</span>
-                <span class="separator"></span>
+                <span class="patchset-text"> Patchset 1 </span>
+                <span class="separator"> </span>
                 <span class="date" tabindex="0">
-                  <gr-date-formatter withtooltip=""></gr-date-formatter>
+                  <gr-date-formatter withtooltip=""> </gr-date-formatter>
                 </span>
                 <div class="show-hide" tabindex="0">
                   <label aria-label="Collapse" class="show-hide">
-                    <input class="show-hide" type="checkbox" />
-                    <gr-icon id="icon" icon="expand_less"></gr-icon>
+                    <md-checkbox class="show-hide"> </md-checkbox>
+                    <gr-icon icon="expand_less" id="icon"> </gr-icon>
                   </label>
                 </div>
               </div>
@@ -416,15 +341,15 @@
                   code=""
                   id="editTextarea"
                   rows="4"
-                  text="This is the test comment message."
                 >
                 </gr-suggestion-textarea>
-                <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+                <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
                 <div class="actions">
                   <div class="leftActions">
                     <div class="action resolve">
                       <label>
-                        <input id="resolvedCheckbox" type="checkbox" />
+                        <md-checkbox checked="" id="resolvedCheckbox">
+                        </md-checkbox>
                         Resolved
                       </label>
                     </div>
@@ -755,6 +680,33 @@
       assert.isTrue(saveStub.called);
     });
 
+    test('clicking resolved checkbox fills message text', async () => {
+      element.comment = {
+        ...createComment(),
+        savingState: SavingState.OK,
+        unresolved: false,
+      };
+      element.editing = true;
+      await element.updateComplete;
+
+      element.messageText = '';
+      await element.updateComplete;
+
+      let checkbox = queryAndAssert<HTMLInputElement>(
+        element,
+        '#resolvedCheckbox'
+      );
+      assert.isTrue(checkbox.checked);
+      assert.equal(element.messageText, '');
+
+      checkbox.click();
+      await element.updateComplete;
+
+      checkbox = queryAndAssert<HTMLInputElement>(element, '#resolvedCheckbox');
+      assert.isFalse(checkbox.checked);
+      assert.equal(element.messageText, 'Marked as unresolved.');
+    });
+
     test('saving empty text calls discard()', async () => {
       const saveStub = sinon.stub(commentsModel, 'saveDraft');
       const discardStub = sinon.stub(commentsModel, 'discardDraft');
@@ -784,41 +736,6 @@
       assert.isTrue(discardStub.called);
       assert.isFalse(saveStub.called);
     });
-
-    test('handlePleaseFix fires reply-to-comment event', async () => {
-      const listener = listenOnce<ReplyToCommentEvent>(
-        element,
-        'reply-to-comment'
-      );
-      element.comment = createRobotComment();
-      element.comments = [element.comment];
-      await element.updateComplete;
-
-      queryAndAssert<GrButton>(element, '.fix').click();
-
-      const e = await listener;
-      assert.equal(e.detail.unresolved, true);
-      assert.equal(e.detail.userWantsToEdit, false);
-      assert.isTrue(e.detail.content.includes('Please fix.'));
-    });
-
-    test('do not show Please Fix button if human reply exists', async () => {
-      element.initiallyCollapsed = false;
-      const robotComment = createRobotComment();
-      element.comment = robotComment;
-      await element.updateComplete;
-
-      let actions = query(element, '.robotActions gr-button.fix');
-      assert.isOk(actions);
-
-      element.comments = [
-        robotComment,
-        {...createComment(), in_reply_to: robotComment.id},
-      ];
-      await element.updateComplete;
-      actions = query(element, '.robotActions gr-button.fix');
-      assert.isNotOk(actions);
-    });
   });
 
   suite('auto saving', () => {
@@ -1005,6 +922,44 @@
         },
       ],
     };
+    setup(async () => {
+      stubFlags('isEnabled')
+        .withArgs(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
+        .returns(true);
+    });
+
+    test('shows suggestion count when unchecked', async () => {
+      const comment: DraftInfo = {
+        ...createDraft(),
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com' as EmailAddress,
+        },
+        line: 5,
+        path: 'test',
+        savingState: SavingState.OK,
+        message: 'hello world',
+      };
+      element = await fixture(
+        html`<gr-comment
+          .account=${account}
+          .showPatchset=${true}
+          .comment=${comment}
+          .initiallyCollapsed=${false}
+        ></gr-comment>`
+      );
+      element.editing = true;
+      sinon.stub(element, 'showGeneratedSuggestion').returns(true);
+      element.generateSuggestion = false;
+      element.generatedFixSuggestion = generatedFixSuggestion;
+      await element.updateComplete;
+
+      const label = queryAndAssert<HTMLLabelElement>(
+        element,
+        'label.suggestEdit'
+      );
+      assert.include(label.textContent, '(1)');
+    });
 
     test('renders suggestions in comment', async () => {
       const comment = {
@@ -1197,4 +1152,49 @@
       });
     });
   });
+
+  suite('autocompleteComment', () => {
+    setup(async () => {
+      element.autocompleteEnabled = true;
+      element.comment = createDraft();
+      element.messageText = 'test message';
+    });
+
+    test('sets cache when response code is OK', async () => {
+      const setStub = sinon
+        .stub(element.autocompleteCache, 'set')
+        .callThrough();
+      const suggestionsService = testResolver(suggestionsServiceToken);
+      const ctx = {
+        draftContent: 'test message',
+        commentCompletion: 'suggested completion',
+        responseCode: ResponseCode.OK,
+      };
+      sinon.stub(suggestionsService, 'autocompleteComment').resolves(ctx);
+      element.messageText = 'test message';
+      await element.updateComplete;
+      await element.autocompleteComment();
+
+      assert.isTrue(setStub.calledWith(ctx));
+      assert.deepInclude(element.autocompleteHint, ctx);
+    });
+
+    test('does not set cache when response code is not OK', async () => {
+      const setStub = sinon
+        .stub(element.autocompleteCache, 'set')
+        .callThrough();
+      const suggestionsService = testResolver(suggestionsServiceToken);
+      const ctx = {
+        draftContent: 'test message',
+        commentCompletion: 'suggested completion',
+        responseCode: ResponseCode.OK_LOW_CONFIDENCE,
+      };
+      sinon.stub(suggestionsService, 'autocompleteComment').resolves(ctx);
+
+      await element.autocompleteComment();
+
+      assert.isFalse(setStub.called);
+      assert.isUndefined(element.autocompleteHint);
+    });
+  });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
index e8ab172..c011476 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
@@ -5,13 +5,12 @@
  */
 import '../gr-dialog/gr-dialog';
 import {css, html, LitElement} from 'lit';
-import {property, query, customElement} from 'lit/decorators.js';
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
+import {customElement, property, query} from 'lit/decorators.js';
+import '../gr-autogrow-textarea/gr-autogrow-textarea';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {assertIsDefined} from '../../../utils/common-util';
-import {BindValueChangeEvent} from '../../../types/events';
 import {fireNoBubble} from '../../../utils/event-util';
+import {GrAutogrowTextarea} from '../gr-autogrow-textarea/gr-autogrow-textarea';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -34,7 +33,7 @@
    */
 
   @query('#messageInput')
-  messageInput?: IronAutogrowTextareaElement;
+  messageInput?: GrAutogrowTextarea;
 
   @property({type: String})
   message = '';
@@ -63,7 +62,7 @@
           display: block;
           width: 100%;
         }
-        iron-autogrow-textarea {
+        gr-autogrow-textarea {
           font-family: var(--monospace-font-family);
           font-size: var(--font-size-mono);
           line-height: var(--line-height-mono);
@@ -87,23 +86,24 @@
           circumstances.
         </p>
         <label for="messageInput">Enter comment delete reason</label>
-        <iron-autogrow-textarea
+        <gr-autogrow-textarea
           id="messageInput"
           class="message"
           autocomplete="on"
           placeholder="&lt;Insert reasoning here&gt;"
-          .bindValue=${this.message}
-          @bind-value-changed=${(e: BindValueChangeEvent) => {
-            this.message = e.detail.value ?? '';
+          .value=${this.message}
+          @input=${(e: InputEvent) => {
+            const value = (e.target as GrAutogrowTextarea).value ?? '';
+            this.message = value;
           }}
-        ></iron-autogrow-textarea>
+        ></gr-autogrow-textarea>
       </div>
     </gr-dialog>`;
   }
 
   resetFocus() {
     assertIsDefined(this.messageInput, 'messageInput');
-    this.messageInput.textarea.focus();
+    this.messageInput.focus();
   }
 
   private handleConfirmTap(e: Event) {
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
index b7551c1..366edbd 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrConfirmDeleteCommentDialog} from './gr-confirm-delete-comment-dialog';
 import './gr-confirm-delete-comment-dialog';
 import {GrDialog} from '../gr-dialog/gr-dialog';
@@ -34,14 +34,13 @@
           circumstances.
           </p>
           <label for="messageInput"> Enter comment delete reason </label>
-          <iron-autogrow-textarea
-            aria-disabled="false"
+          <gr-autogrow-textarea
             autocomplete="on"
             class="message"
             id="messageInput"
             placeholder="<Insert reasoning here>"
           >
-          </iron-autogrow-textarea>
+          </gr-autogrow-textarea>
         </div>
       </gr-dialog>
     `
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 1a536d7..8e2a791 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -3,18 +3,17 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../gr-button/gr-button';
 import '../gr-icon/gr-icon';
 import '../gr-tooltip-content/gr-tooltip-content';
 import {
   assertIsDefined,
-  copyToClipbard,
+  copyToClipboard,
   queryAndAssert,
 } from '../../../utils/common-util';
 import {classMap} from 'lit/directives/class-map.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query} from 'lit/decorators.js';
 import {GrButton} from '../gr-button/gr-button';
 import {GrIcon} from '../gr-icon/gr-icon';
@@ -23,6 +22,8 @@
 import {when} from 'lit/directives/when.js';
 import {formStyles} from '../../../styles/form-styles';
 import {fire} from '../../../utils/event-util';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const COPY_TIMEOUT_MS = 1000;
 
@@ -31,6 +32,12 @@
     'gr-copy-clipboard': GrCopyClipboard;
   }
 }
+
+/**
+ * A copy-to-clipboard element.
+ *
+ * @attr {Boolean} nowrap - If true, the text will not wrap.
+ */
 @customElement('gr-copy-clipboard')
 export class GrCopyClipboard extends LitElement {
   @property({type: String})
@@ -58,6 +65,9 @@
   @property({type: Boolean})
   multiline = false;
 
+  @property({type: Boolean, reflect: true})
+  nowrap = false;
+
   @query('#icon')
   iconEl!: GrIcon;
 
@@ -65,6 +75,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       formStyles,
       css`
         .text {
@@ -99,6 +110,27 @@
           background-color: var(--view-background-color);
           color: var(--primary-text-color);
         }
+        textarea#input {
+          font-family: var(--monospace-font-family);
+          font-size: var(--font-size-mono);
+          line-height: var(--line-height-mono);
+          width: 100%;
+          min-height: 100px;
+          resize: vertical;
+          background-color: var(--view-background-color);
+          color: var(--primary-text-color);
+          border: 1px solid var(--border-color);
+          border-radius: var(--border-radius);
+          padding: var(--spacing-s);
+        }
+        textarea#input:focus {
+          outline: none;
+          box-shadow: none;
+          border-color: var(--prominent-border-color, var(--border-color));
+        }
+        textarea#input::placeholder {
+          color: var(--deemphasized-text-color);
+        }
         gr-icon {
           color: var(
             --gr-copy-clipboard-icon-color,
@@ -121,36 +153,27 @@
           this.label,
           () => html`<label for="input">${this.label}</label>`
         )}
-        <iron-input
-          class="copyText"
-          @click=${this._handleInputClick}
-          .bindValue=${this.text ?? ''}
-          part="text-container-wrapper-style"
-        >
-          ${when(
-            this.multiline,
-            () => html`<textarea
-              id="input"
-              is="iron-input"
-              class=${classMap({hideInput: this.hideInput})}
-              @click=${this._handleInputClick}
-              readonly=""
-              .value=${this.text ?? ''}
-              part="text-container-style"
-            >
-            </textarea>`,
-            () => html`<input
-              id="input"
-              is="iron-input"
-              class=${classMap({hideInput: this.hideInput})}
-              type="text"
-              @click=${this._handleInputClick}
-              readonly=""
-              .value=${this.text ?? ''}
-              part="text-container-style"
-            />`
-          )}
-        </iron-input>
+        ${when(
+          this.multiline,
+          () => html`<textarea
+            id="input"
+            class=${classMap({hideInput: this.hideInput})}
+            @click=${this.handleInputClick}
+            readonly=""
+            .value=${this.text ?? ''}
+            part="text-container-wrapper-style"
+          >
+          </textarea>`,
+          () => html` <md-outlined-text-field
+            id="input"
+            class="copyText ${classMap({hideInput: this.hideInput})}"
+            .value=${this.text ?? ''}
+            ?readOnly=${true}
+            part="text-container-wrapper-style"
+            @click=${this.handleInputClick}
+          >
+          </md-outlined-text-field>`
+        )}
         ${when(
           this.shortcut,
           () => html`<span class="shortcut">${this.shortcut}</span>`
@@ -163,7 +186,7 @@
             id="copy-clipboard-button"
             link=""
             class="copyToClipboard"
-            @click=${this._copyToClipboard}
+            @click=${this.copyToClipboard}
             aria-label="copy"
             aria-description="Click to copy to clipboard"
           >
@@ -180,13 +203,13 @@
     queryAndAssert<GrButton>(this, '#copy-clipboard-button').focus();
   }
 
-  _handleInputClick(e: MouseEvent) {
+  private handleInputClick(e: MouseEvent) {
     e.preventDefault();
     const rootTarget = e.composedPath()[0];
     (rootTarget as HTMLInputElement).select();
   }
 
-  _copyToClipboard(e: MouseEvent) {
+  private copyToClipboard(e: MouseEvent) {
     e.preventDefault();
     e.stopPropagation();
 
@@ -195,7 +218,7 @@
     assertIsDefined(this.text, 'text');
     this.iconEl.icon = 'check';
     this.reporting.time(Timing.COPY_TO_CLIPBOARD);
-    copyToClipbard(this.text, this.copyTargetName ?? 'Link').finally(() => {
+    copyToClipboard(this.text, this.copyTargetName ?? 'Link').finally(() => {
       this.reporting.timeEnd(Timing.COPY_TO_CLIPBOARD, {
         copyTargetName: this.copyTargetName,
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index 7e476eb..83adf10 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -8,8 +8,9 @@
 import './gr-copy-clipboard';
 import {GrCopyClipboard} from './gr-copy-clipboard';
 import {queryAndAssert} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
 
 suite('gr-copy-clipboard tests', () => {
   let element: GrCopyClipboard;
@@ -31,20 +32,21 @@
       element,
       /* HTML */ `
         <div class="text">
-          <iron-input class="copyText" part="text-container-wrapper-style">
-            <input
-              id="input"
-              is="iron-input"
-              part="text-container-style"
-              readonly=""
-              type="text"
-            />
-          </iron-input>
+          <md-outlined-text-field
+            autocomplete=""
+            class="copyText"
+            id="input"
+            inputmode=""
+            part="text-container-wrapper-style"
+            readonly=""
+            type="text"
+          >
+          </md-outlined-text-field>
           <gr-tooltip-content>
             <gr-button
+              aria-description="Click to copy to clipboard"
               aria-disabled="false"
               aria-label="copy"
-              aria-description="Click to copy to clipboard"
               class="copyToClipboard"
               id="copy-clipboard-button"
               link=""
@@ -52,7 +54,7 @@
               tabindex="0"
             >
               <div>
-                <gr-icon icon="content_copy" id="icon" small></gr-icon>
+                <gr-icon icon="content_copy" id="icon" small=""> </gr-icon>
               </div>
             </gr-button>
           </gr-tooltip-content>
@@ -69,22 +71,23 @@
       element,
       /* HTML */ `
         <div class="text">
-          <label for="input">Label</label>
-          <iron-input class="copyText" part="text-container-wrapper-style">
-            <input
-              id="input"
-              is="iron-input"
-              part="text-container-style"
-              readonly=""
-              type="text"
-            />
-          </iron-input>
-          <span class="shortcut">l - l</span>
+          <label for="input"> Label </label>
+          <md-outlined-text-field
+            autocomplete=""
+            class="copyText"
+            id="input"
+            inputmode=""
+            part="text-container-wrapper-style"
+            readonly=""
+            type="text"
+          >
+          </md-outlined-text-field>
+          <span class="shortcut"> l - l </span>
           <gr-tooltip-content>
             <gr-button
+              aria-description="Click to copy to clipboard"
               aria-disabled="false"
               aria-label="copy"
-              aria-description="Click to copy to clipboard"
               class="copyToClipboard"
               id="copy-clipboard-button"
               link=""
@@ -92,7 +95,7 @@
               tabindex="0"
             >
               <div>
-                <gr-icon icon="content_copy" id="icon" small></gr-icon>
+                <gr-icon icon="content_copy" id="icon" small=""> </gr-icon>
               </div>
             </gr-button>
           </gr-tooltip-content>
@@ -113,29 +116,33 @@
     assert.deepEqual(activeElement, button);
   });
 
-  test('_handleInputClick', () => {
+  test('handleInputClick', () => {
     // iron-input as parent should never be hidden as copy won't work
     // on nested hidden elements
-    const ironInputElement = queryAndAssert(element, 'iron-input');
-    assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
+    const mdOutlinedTextField = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field'
+    );
+    assert.notEqual(getComputedStyle(mdOutlinedTextField).display, 'none');
 
-    const inputElement = queryAndAssert<HTMLInputElement>(element, 'input');
-    inputElement.click();
-    assert.equal(inputElement.selectionStart, 0);
-    assert.equal(inputElement.selectionEnd, element.text!.length - 1);
+    mdOutlinedTextField.click();
+    assert.equal(mdOutlinedTextField.selectionStart, 0);
+    assert.equal(mdOutlinedTextField.selectionEnd, element.text!.length - 1);
   });
 
   test('hideInput', async () => {
     // iron-input as parent should never be hidden as copy won't work
     // on nested hidden elements
-    const ironInputElement = queryAndAssert(element, 'iron-input');
-    assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
+    const mdOutlinedTextField = queryAndAssert<MdOutlinedTextField>(
+      element,
+      'md-outlined-text-field'
+    );
+    assert.notEqual(getComputedStyle(mdOutlinedTextField).display, 'none');
 
-    const input = queryAndAssert(element, 'input');
-    assert.notEqual(getComputedStyle(input).display, 'none');
+    assert.notEqual(getComputedStyle(mdOutlinedTextField).display, 'none');
     element.hideInput = true;
     await element.updateComplete;
-    assert.equal(getComputedStyle(input).display, 'none');
+    assert.equal(getComputedStyle(mdOutlinedTextField).display, 'none');
   });
 
   test('stop events propagation', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 35567be..a593298 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -45,6 +45,11 @@
   cursorTargetClass: string | null = null;
 
   /**
+   * The attribute to apply to the current target. Use null for no attribute.
+   */
+  cursorTargetAttribute: string | null = null;
+
+  /**
    * The scroll behavior for the cursor. Values are 'never' and
    * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
    * the viewport.
@@ -350,12 +355,20 @@
     if (this.target && this.cursorTargetClass) {
       this.target.classList.add(this.cursorTargetClass);
     }
+
+    if (this.target && this.cursorTargetAttribute) {
+      this.target.setAttribute(this.cursorTargetAttribute, '');
+    }
   }
 
   _unDecorateTarget() {
     if (this.target && this.cursorTargetClass) {
       this.target.classList.remove(this.cursorTargetClass);
     }
+
+    if (this.target && this.cursorTargetAttribute) {
+      this.target.removeAttribute(this.cursorTargetAttribute);
+    }
   }
 
   _updateIndex() {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts
index b03a421..dad0350 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.ts
@@ -5,7 +5,7 @@
  */
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {AbortStop, CursorMoveResult} from '../../../api/core';
 import {GrCursorManager} from './gr-cursor-manager';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index 05b7fb5..fdbfdd5 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -7,16 +7,16 @@
 import {css, html, LitElement} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {
-  parseDate,
-  fromNow,
+  formatDate,
   isValidDate,
   isWithinDay,
   isWithinHalfYear,
-  formatDate,
+  parseDate,
+  relative,
   utcOffsetString,
   wasYesterday,
 } from '../../../utils/date-util';
-import {TimeFormat, DateFormat} from '../../../constants/constants';
+import {DateFormat, TimeFormat} from '../../../constants/constants';
 import {assertNever} from '../../../utils/common-util';
 import {PreferencesInfo, Timestamp} from '../../../types/common';
 import {resolve} from '../../../models/dependency';
@@ -192,7 +192,7 @@
       return '';
     }
     if (this.relative) {
-      return fromNow(date, this.relativeOptionNoAgo);
+      return relative(date, this.relativeOptionNoAgo);
     }
     const now = new Date();
     let format = this.dateFormat.full;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
index d6a01ab..76196c3 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
@@ -8,7 +8,7 @@
 import './gr-date-formatter';
 import {GrDateFormatter} from './gr-date-formatter';
 import {parseDate} from '../../../utils/date-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {query, queryAndAssert} from '../../../test/test-utils';
 import {GrTooltipContent} from '../gr-tooltip-content/gr-tooltip-content';
 import {Timestamp} from '../../../api/rest-api';
@@ -99,6 +99,16 @@
       );
     });
 
+    test('Within one week', async () => {
+      await testDates(
+        '2015-07-22 03:34:14.985000000',
+        '2015-07-29 02:25:14.985000000',
+        'Jul 29',
+        'Jul 29 02:25',
+        'Wednesday, Jul 29, 2015, 02:25:14'
+      );
+    });
+
     test('More than 24 hours but less than six months', async () => {
       await testDates(
         '2015-07-29 20:34:14.985000000',
@@ -147,6 +157,16 @@
       );
     });
 
+    test('Within one week', async () => {
+      await testDates(
+        '2015-07-22 03:34:14.985000000',
+        '2015-07-29 02:25:14.985000000',
+        '07/29',
+        '07/29 02:25',
+        'Wednesday, 07/29/15, 02:25:14'
+      );
+    });
+
     test('More than 24 hours but less than six months', async () => {
       await testDates(
         '2015-07-29 20:34:14.985000000',
@@ -185,6 +205,16 @@
       );
     });
 
+    test('Within one week', async () => {
+      await testDates(
+        '2015-07-22 03:34:14.985000000',
+        '2015-07-29 02:25:14.985000000',
+        '07-29',
+        '07-29 02:25',
+        'Wednesday, 2015-07-29, 02:25:14'
+      );
+    });
+
     test('More than 24 hours but less than six months', async () => {
       await testDates(
         '2015-07-29 20:34:14.985000000',
@@ -223,6 +253,16 @@
       );
     });
 
+    test('Within one week', async () => {
+      await testDates(
+        '2015-07-22 03:34:14.985000000',
+        '2015-07-29 02:25:14.985000000',
+        '29. Jul',
+        '29. Jul 02:25',
+        'Wednesday, 29.07.2015, 02:25:14'
+      );
+    });
+
     test('More than 24 hours but less than six months', async () => {
       await testDates(
         '2015-07-29 20:34:14.985000000',
@@ -261,6 +301,16 @@
       );
     });
 
+    test('Within one week', async () => {
+      await testDates(
+        '2015-07-22 03:34:14.985000000',
+        '2015-07-29 02:25:14.985000000',
+        '29/07',
+        '29/07 02:25',
+        'Wednesday, 29/07/2015, 02:25:14'
+      );
+    });
+
     test('More than 24 hours but less than six months', async () => {
       await testDates(
         '2015-07-29 20:34:14.985000000',
@@ -270,6 +320,16 @@
         'Monday, 15/06/2015, 03:25:14'
       );
     });
+
+    test('More than six months', async () => {
+      await testDates(
+        '2015-07-29 20:34:14.985000000',
+        '2016-08-15 03:25:14.985000000',
+        '15/08/2016',
+        '15/08/2016 03:25',
+        'Monday, 15/08/2016, 03:25:14'
+      );
+    });
   });
 
   suite('STD + 12 hours time format preference', () => {
@@ -373,7 +433,7 @@
       sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
-    test('Within 24 hours on same day', async () => {
+    test('Within 24 hours on same day (Past)', async () => {
       await testDates(
         '2015-07-29 20:34:14.985000000',
         '2015-07-29 15:34:14.985000000',
@@ -383,7 +443,17 @@
       );
     });
 
-    test('More than six months', async () => {
+    test('Within 24 hours on same day (Future)', async () => {
+      await testDates(
+        '2015-07-29 20:34:14.985000000',
+        '2015-07-29 23:34:14.985000000',
+        'in 3 hours',
+        'in 3 hours',
+        'Wednesday, Jul 29, 2015, 11:34:14 PM'
+      );
+    });
+
+    test('More than six months (Past)', async () => {
       await testDates(
         '2015-09-15 20:34:00.000000000',
         '2015-01-15 03:25:00.000000000',
@@ -392,6 +462,16 @@
         'Thursday, Jan 15, 2015, 3:25:00 AM'
       );
     });
+
+    test('More than six months (Future)', async () => {
+      await testDates(
+        '2015-01-15 20:34:00.000000000',
+        '2015-09-15 03:25:00.000000000',
+        'in 8 months',
+        'in 8 months',
+        'Tuesday, Sep 15, 2015, 3:25:00 AM'
+      );
+    });
   });
 
   suite('logged in', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index 93201c8..58614e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -38,6 +38,7 @@
   @query('#confirm')
   confirmButton?: GrButton;
 
+  // Supplying an empty confirm label will hide the button completely.
   @property({type: String, attribute: 'confirm-label'})
   confirmLabel = 'Confirm';
 
@@ -84,11 +85,10 @@
           display: flex;
           flex-direction: column;
           max-height: 90vh;
-          padding: var(--spacing-xl);
         }
         header {
           flex-shrink: 0;
-          padding-bottom: var(--spacing-xl);
+          padding: var(--spacing-xl);
         }
         main {
           display: flex;
@@ -101,11 +101,12 @@
         main .overflow-container {
           flex: 1;
           overflow: auto;
+          padding: 0 var(--spacing-xl);
         }
         footer {
           display: flex;
           flex-shrink: 0;
-          padding-top: var(--spacing-xl);
+          padding: var(--spacing-xl);
           align-items: center;
         }
         .flex-space {
@@ -114,9 +115,6 @@
         gr-button {
           margin-left: var(--spacing-l);
         }
-        .hidden {
-          display: none;
-        }
         .loadingSpin {
           width: 18px;
           height: 18px;
@@ -153,26 +151,36 @@
             `
           )}
           <slot name="footer"></slot>
-          <div class="flex-space"></div>
-          <gr-button
-            id="cancel"
-            class=${this.cancelLabel.length ? '' : 'hidden'}
-            link
-            ?disabled=${this.disableCancel}
-            @click=${(e: Event) => this.handleCancelTap(e)}
-          >
-            ${this.cancelLabel}
-          </gr-button>
-          <gr-button
-            id="confirm"
-            link
-            primary
-            @click=${this._handleConfirm}
-            ?disabled=${this.disabled}
-            title=${this.confirmTooltip ?? ''}
-          >
-            ${this.confirmLabel}
-          </gr-button>
+          ${when(
+            this.confirmLabel || this.cancelLabel,
+            () => html`
+              <div class="flex-space"></div>
+              ${when(
+                this.cancelLabel,
+                () => html`<gr-button
+                  id="cancel"
+                  link
+                  ?disabled=${this.disableCancel}
+                  @click=${(e: Event) => this.handleCancelTap(e)}
+                >
+                  ${this.cancelLabel}
+                </gr-button>`
+              )}
+              ${when(
+                this.confirmLabel,
+                () => html`<gr-button
+                  id="confirm"
+                  link
+                  primary
+                  @click=${this._handleConfirm}
+                  ?disabled=${this.disabled}
+                  title=${this.confirmTooltip ?? ''}
+                >
+                  ${this.confirmLabel}
+                </gr-button>`
+              )}
+            `
+          )}
         </footer>
       </div>
     `;
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
index c4928d9..4945740 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.ts
@@ -8,12 +8,11 @@
 import './gr-dialog';
 import {GrDialog} from './gr-dialog';
 import {
-  isHidden,
   pressKey,
   queryAndAssert,
   waitEventLoop,
 } from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
 
 suite('gr-dialog tests', () => {
@@ -161,12 +160,17 @@
     });
   });
 
-  test('empty cancel label hides cancel btn', async () => {
-    const cancelButton = queryAndAssert(element, '#cancel');
-    assert.isFalse(isHidden(cancelButton));
+  test('empty labels hide buttons and flex-space', async () => {
+    assert.isNotNull(element.shadowRoot!.querySelector('#cancel'));
+    assert.isNotNull(element.shadowRoot!.querySelector('#confirm'));
+    assert.isNotNull(element.shadowRoot!.querySelector('.flex-space'));
+
     element.cancelLabel = '';
+    element.confirmLabel = '';
     await element.updateComplete;
 
-    assert.isTrue(isHidden(cancelButton));
+    assert.isNull(element.shadowRoot!.querySelector('#cancel'));
+    assert.isNull(element.shadowRoot!.querySelector('#confirm'));
+    assert.isNull(element.shadowRoot!.querySelector('.flex-space'));
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 408efbe..53f0be5 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -3,50 +3,50 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../../../styles/shared-styles';
 import '../gr-button/gr-button';
-import '../gr-select/gr-select';
 import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
 import {subscribe} from '../../lit/subscription-controller';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {convertToString} from '../../../utils/string-util';
 import {fire} from '../../../utils/event-util';
 import {ValueChangedEvent} from '../../../types/events';
-import {GrSelect} from '../gr-select/gr-select';
 import {resolve} from '../../../models/dependency';
 import {userModelToken} from '../../../models/user/user-model';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
+import '@material/web/checkbox/checkbox';
+import {MdCheckbox} from '@material/web/checkbox/checkbox';
+import '@material/web/select/outlined-select';
+import '@material/web/select/select-option';
+import {MdOutlinedSelect} from '@material/web/select/outlined-select';
 
 @customElement('gr-diff-preferences')
 export class GrDiffPreferences extends LitElement {
-  @query('#contextLineSelect') private contextLineSelect?: HTMLInputElement;
-
   @query('#columnsInput') private columnsInput?: HTMLInputElement;
 
   @query('#tabSizeInput') private tabSizeInput?: HTMLInputElement;
 
   @query('#fontSizeInput') private fontSizeInput?: HTMLInputElement;
 
-  @query('#lineWrappingInput') private lineWrappingInput?: HTMLInputElement;
+  @query('#lineWrappingInput') private lineWrappingInput?: MdCheckbox;
 
-  @query('#showTabsInput') private showTabsInput?: HTMLInputElement;
+  @query('#showTabsInput') private showTabsInput?: MdCheckbox;
 
   @query('#showTrailingWhitespaceInput')
-  private showTrailingWhitespaceInput?: HTMLInputElement;
+  private showTrailingWhitespaceInput?: MdCheckbox;
 
   @query('#automaticReviewInput')
-  private automaticReviewInput?: HTMLInputElement;
+  private automaticReviewInput?: MdCheckbox;
 
   @query('#syntaxHighlightInput')
-  private syntaxHighlightInput?: HTMLInputElement;
-
-  @query('#ignoreWhiteSpace') private ignoreWhiteSpace?: HTMLInputElement;
+  private syntaxHighlightInput?: MdCheckbox;
 
   // Used in gr-diff-preferences-dialog
-  @query('#contextSelect') contextSelect?: GrSelect;
+  @query('#contextSelect') contextSelect?: MdOutlinedSelect;
 
   @state() diffPrefs?: DiffPreferencesInfo;
 
@@ -68,7 +68,16 @@
   }
 
   static override get styles() {
-    return [sharedStyles, grFormStyles];
+    return [
+      materialStyles,
+      sharedStyles,
+      grFormStyles,
+      css`
+        md-outlined-text-field {
+          max-width: 25em;
+        }
+      `,
+    ];
   }
 
   override render() {
@@ -77,79 +86,129 @@
         <section>
           <label for="contextLineSelect" class="title">Context</label>
           <span class="value">
-            <gr-select
+            <md-outlined-select
               id="contextSelect"
-              .bindValue=${convertToString(this.diffPrefs?.context)}
-              @change=${this.handleDiffContextChanged}
+              value=${convertToString(this.diffPrefs?.context)}
+              @change=${(e: Event) => {
+                const select = e.target as HTMLSelectElement;
+                this.diffPrefs!.context = Number(select.value);
+                this.requestUpdate();
+                fire(this, 'has-unsaved-changes-changed', {
+                  value: this.hasUnsavedChanges(),
+                });
+              }}
             >
-              <select id="contextLineSelect">
-                <option value="3">3 lines</option>
-                <option value="10">10 lines</option>
-                <option value="25">25 lines</option>
-                <option value="50">50 lines</option>
-                <option value="75">75 lines</option>
-                <option value="100">100 lines</option>
-                <option value="-1">Whole file</option>
-              </select>
-            </gr-select>
+              <md-select-option value="3">
+                <div slot="headline">3 lines</div>
+              </md-select-option>
+              <md-select-option value="10">
+                <div slot="headline">10 lines</div>
+              </md-select-option>
+              <md-select-option value="25">
+                <div slot="headline">25 lines</div>
+              </md-select-option>
+              <md-select-option value="50">
+                <div slot="headline">50 lines</div>
+              </md-select-option>
+              <md-select-option value="75">
+                <div slot="headline">75 lines</div>
+              </md-select-option>
+              <md-select-option value="100">
+                <div slot="headline">100 lines</div>
+              </md-select-option>
+              <md-select-option value="-1">
+                <div slot="headline">Whole file</div>
+              </md-select-option>
+            </md-outlined-select>
           </span>
         </section>
         <section>
           <label for="lineWrappingInput" class="title">Fit to screen</label>
           <span class="value">
-            <input
+            <md-checkbox
               id="lineWrappingInput"
-              type="checkbox"
-              ?checked=${this.diffPrefs?.line_wrapping}
+              ?checked=${!!this.diffPrefs?.line_wrapping}
               @change=${this.handleLineWrappingTap}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
           <label for="columnsInput" class="title">Diff width</label>
           <span class="value">
-            <iron-input
-              .allowedPattern=${'[0-9]'}
-              .bindValue=${convertToString(this.diffPrefs?.line_length)}
-              @change=${this.handleDiffLineLengthChanged}
+            <md-outlined-text-field
+              id="columnsInput"
+              class="showBlueFocusBorder"
+              type="number"
+              step="1"
+              .value=${convertToString(this.diffPrefs?.line_length)}
+              @input=${this.handleDiffLineLengthInput}
+              @beforeinput=${(e: InputEvent) => {
+                // In iron-input we had allowedPattern, but this is not supported
+                // in md-outlined-text-field. Which uses native input functionality.
+                // We workaround this.
+                const data = e.data;
+                if (data && !/^[0-9]*$/.test(data)) {
+                  e.preventDefault();
+                }
+              }}
             >
-              <input id="columnsInput" type="number" />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
           <label for="tabSizeInput" class="title">Tab width</label>
           <span class="value">
-            <iron-input
-              .allowedPattern=${'[0-9]'}
-              .bindValue=${convertToString(this.diffPrefs?.tab_size)}
-              @change=${this.handleDiffTabSizeChanged}
+            <md-outlined-text-field
+              id="tabSizeInput"
+              class="showBlueFocusBorder"
+              type="number"
+              step="1"
+              .value=${convertToString(this.diffPrefs?.tab_size)}
+              @input=${this.handleDiffTabSizeInput}
+              @beforeinput=${(e: InputEvent) => {
+                // In iron-input we had allowedPattern, but this is not supported
+                // in md-outlined-text-field. Which uses native input functionality.
+                // We workaround this.
+                const data = e.data;
+                if (data && !/^[0-9]*$/.test(data)) {
+                  e.preventDefault();
+                }
+              }}
             >
-              <input id="tabSizeInput" type="number" />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
           <label for="fontSizeInput" class="title">Font size</label>
           <span class="value">
-            <iron-input
-              .allowedPattern=${'[0-9]'}
-              .bindValue=${convertToString(this.diffPrefs?.font_size)}
-              @change=${this.handleDiffFontSizeChanged}
+            <md-outlined-text-field
+              id="fontSizeInput"
+              class="showBlueFocusBorder"
+              type="number"
+              step="1"
+              .value=${convertToString(this.diffPrefs?.font_size)}
+              @input=${this.handleDiffFontSizeInput}
+              @beforeinput=${(e: InputEvent) => {
+                // In iron-input we had allowedPattern, but this is not supported
+                // in md-outlined-text-field. Which uses native input functionality.
+                // We workaround this.
+                const data = e.data;
+                if (data && !/^[0-9]*$/.test(data)) {
+                  e.preventDefault();
+                }
+              }}
             >
-              <input id="fontSizeInput" type="number" />
-            </iron-input>
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
           <label for="showTabsInput" class="title">Show tabs</label>
           <span class="value">
-            <input
+            <md-checkbox
               id="showTabsInput"
-              type="checkbox"
-              ?checked=${this.diffPrefs?.show_tabs}
+              ?checked=${!!this.diffPrefs?.show_tabs}
               @change=${this.handleShowTabsTap}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
@@ -157,12 +216,11 @@
             >Show trailing whitespace</label
           >
           <span class="value">
-            <input
+            <md-checkbox
               id="showTrailingWhitespaceInput"
-              type="checkbox"
-              ?checked=${this.diffPrefs?.show_whitespace_errors}
+              ?checked=${!!this.diffPrefs?.show_whitespace_errors}
               @change=${this.handleShowTrailingWhitespaceTap}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
@@ -170,12 +228,11 @@
             >Syntax highlighting</label
           >
           <span class="value">
-            <input
+            <md-checkbox
               id="syntaxHighlightInput"
-              type="checkbox"
-              ?checked=${this.diffPrefs?.syntax_highlighting}
+              ?checked=${!!this.diffPrefs?.syntax_highlighting}
               @change=${this.handleSyntaxHighlightTap}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
@@ -183,12 +240,11 @@
             >Automatically mark viewed files reviewed</label
           >
           <span class="value">
-            <input
+            <md-checkbox
               id="automaticReviewInput"
-              type="checkbox"
               ?checked=${!this.diffPrefs?.manual_review}
               @change=${this.handleAutomaticReviewTap}
-            />
+            ></md-checkbox>
           </span>
         </section>
         <section>
@@ -197,19 +253,32 @@
               >Ignore Whitespace</label
             >
             <span class="value">
-              <gr-select
-                .bindValue=${convertToString(this.diffPrefs?.ignore_whitespace)}
-                @change=${this.handleDiffIgnoreWhitespaceChanged}
+              <md-outlined-select
+                id="contextSelect"
+                value=${convertToString(this.diffPrefs?.ignore_whitespace)}
+                @change=${(e: Event) => {
+                  const select = e.target as HTMLSelectElement;
+                  this.diffPrefs!.ignore_whitespace =
+                    select.value as IgnoreWhitespaceType;
+                  this.requestUpdate();
+                  fire(this, 'has-unsaved-changes-changed', {
+                    value: this.hasUnsavedChanges(),
+                  });
+                }}
               >
-                <select id="ignoreWhiteSpace">
-                  <option value="IGNORE_NONE">None</option>
-                  <option value="IGNORE_TRAILING">Trailing</option>
-                  <option value="IGNORE_LEADING_AND_TRAILING">
-                    Leading &amp; trailing
-                  </option>
-                  <option value="IGNORE_ALL">All</option>
-                </select>
-              </gr-select>
+                <md-select-option value="IGNORE_NONE">
+                  <div slot="headline">None</div>
+                </md-select-option>
+                <md-select-option value="IGNORE_TRAILING">
+                  <div slot="headline">Trailing</div>
+                </md-select-option>
+                <md-select-option value="IGNORE_LEADING_AND_TRAILING">
+                  <div slot="headline">Leading &amp; trailing</div>
+                </md-select-option>
+                <md-select-option value="IGNORE_ALL">
+                  <div slot="headline">All</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </div>
         </section>
@@ -217,13 +286,6 @@
     `;
   }
 
-  private readonly handleDiffContextChanged = () => {
-    this.diffPrefs!.context = Number(this.contextLineSelect!.value);
-    fire(this, 'has-unsaved-changes-changed', {
-      value: this.hasUnsavedChanges(),
-    });
-  };
-
   private readonly handleLineWrappingTap = () => {
     this.diffPrefs!.line_wrapping = this.lineWrappingInput!.checked;
     fire(this, 'has-unsaved-changes-changed', {
@@ -231,21 +293,21 @@
     });
   };
 
-  private readonly handleDiffLineLengthChanged = () => {
+  private readonly handleDiffLineLengthInput = () => {
     this.diffPrefs!.line_length = Number(this.columnsInput!.value);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
     });
   };
 
-  private readonly handleDiffTabSizeChanged = () => {
+  private readonly handleDiffTabSizeInput = () => {
     this.diffPrefs!.tab_size = Number(this.tabSizeInput!.value);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
     });
   };
 
-  private readonly handleDiffFontSizeChanged = () => {
+  private readonly handleDiffFontSizeInput = () => {
     this.diffPrefs!.font_size = Number(this.fontSizeInput!.value);
     fire(this, 'has-unsaved-changes-changed', {
       value: this.hasUnsavedChanges(),
@@ -282,14 +344,6 @@
     });
   };
 
-  private readonly handleDiffIgnoreWhitespaceChanged = () => {
-    this.diffPrefs!.ignore_whitespace = this.ignoreWhiteSpace!
-      .value as IgnoreWhitespaceType;
-    fire(this, 'has-unsaved-changes-changed', {
-      value: this.hasUnsavedChanges(),
-    });
-  };
-
   hasUnsavedChanges() {
     // We have to wrap boolean values in Boolean() to ensure undefined values
     // use false rather than undefined.
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
index 97e4221..092c63f 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
@@ -14,9 +14,9 @@
 } from '../../../test/test-utils';
 import {DiffPreferencesInfo} from '../../../types/diff';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
-import {IronInputElement} from '@polymer/iron-input';
-import {GrSelect} from '../gr-select/gr-select';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
+import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
+import {MdOutlinedSelect} from '@material/web/select/outlined-select';
 
 suite('gr-diff-preferences tests', () => {
   let element: GrDiffPreferences;
@@ -49,57 +49,92 @@
   test('renders', () => {
     assert.shadowDom.equal(
       element,
-      /* HTML */ ` <div class="gr-form-styles" id="diffPreferences">
+      /* HTML */ `<div class="gr-form-styles" id="diffPreferences">
         <section>
-          <label class="title" for="contextLineSelect">Context</label>
+          <label class="title" for="contextLineSelect"> Context </label>
           <span class="value">
-            <gr-select id="contextSelect">
-              <select id="contextLineSelect">
-                <option value="3">3 lines</option>
-                <option value="10">10 lines</option>
-                <option value="25">25 lines</option>
-                <option value="50">50 lines</option>
-                <option value="75">75 lines</option>
-                <option value="100">100 lines</option>
-                <option value="-1">Whole file</option>
-              </select>
-            </gr-select>
+            <md-outlined-select id="contextSelect" value="10">
+              <md-select-option md-menu-item="" tabindex="0" value="3">
+                <div slot="headline">3 lines</div>
+              </md-select-option>
+              <md-select-option
+                data-aria-selected="true"
+                md-menu-item=""
+                tabindex="-1"
+                value="10"
+              >
+                <div slot="headline">10 lines</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="25">
+                <div slot="headline">25 lines</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="50">
+                <div slot="headline">50 lines</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="75">
+                <div slot="headline">75 lines</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="100">
+                <div slot="headline">100 lines</div>
+              </md-select-option>
+              <md-select-option md-menu-item="" tabindex="-1" value="-1">
+                <div slot="headline">Whole file</div>
+              </md-select-option>
+            </md-outlined-select>
           </span>
         </section>
         <section>
-          <label class="title" for="lineWrappingInput">Fit to screen</label>
+          <label class="title" for="lineWrappingInput"> Fit to screen </label>
           <span class="value">
-            <input id="lineWrappingInput" type="checkbox" />
+            <md-checkbox id="lineWrappingInput"> </md-checkbox>
           </span>
         </section>
         <section>
-          <label class="title" for="columnsInput">Diff width</label>
+          <label class="title" for="columnsInput"> Diff width </label>
           <span class="value">
-            <iron-input>
-              <input id="columnsInput" type="number" />
-            </iron-input>
+            <md-outlined-text-field
+              autocomplete=""
+              class="showBlueFocusBorder"
+              id="columnsInput"
+              inputmode=""
+              step="1"
+              type="number"
+            >
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
-          <label class="title" for="tabSizeInput">Tab width</label>
+          <label class="title" for="tabSizeInput"> Tab width </label>
           <span class="value">
-            <iron-input>
-              <input id="tabSizeInput" type="number" />
-            </iron-input>
+            <md-outlined-text-field
+              autocomplete=""
+              class="showBlueFocusBorder"
+              id="tabSizeInput"
+              inputmode=""
+              step="1"
+              type="number"
+            >
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
-          <label class="title" for="fontSizeInput">Font size</label>
+          <label class="title" for="fontSizeInput"> Font size </label>
           <span class="value">
-            <iron-input>
-              <input id="fontSizeInput" type="number" />
-            </iron-input>
+            <md-outlined-text-field
+              autocomplete=""
+              class="showBlueFocusBorder"
+              id="fontSizeInput"
+              inputmode=""
+              step="1"
+              type="number"
+            >
+            </md-outlined-text-field>
           </span>
         </section>
         <section>
-          <label class="title" for="showTabsInput">Show tabs</label>
+          <label class="title" for="showTabsInput"> Show tabs </label>
           <span class="value">
-            <input checked="" id="showTabsInput" type="checkbox" />
+            <md-checkbox checked="" id="showTabsInput"> </md-checkbox>
           </span>
         </section>
         <section>
@@ -107,11 +142,8 @@
             Show trailing whitespace
           </label>
           <span class="value">
-            <input
-              checked=""
-              id="showTrailingWhitespaceInput"
-              type="checkbox"
-            />
+            <md-checkbox checked="" id="showTrailingWhitespaceInput">
+            </md-checkbox>
           </span>
         </section>
         <section>
@@ -119,7 +151,7 @@
             Syntax highlighting
           </label>
           <span class="value">
-            <input checked="" id="syntaxHighlightInput" type="checkbox" />
+            <md-checkbox checked="" id="syntaxHighlightInput"> </md-checkbox>
           </span>
         </section>
         <section>
@@ -127,7 +159,7 @@
             Automatically mark viewed files reviewed
           </label>
           <span class="value">
-            <input checked="" id="automaticReviewInput" type="checkbox" />
+            <md-checkbox checked="" id="automaticReviewInput"> </md-checkbox>
           </span>
         </section>
         <section>
@@ -136,16 +168,37 @@
               Ignore Whitespace
             </label>
             <span class="value">
-              <gr-select>
-                <select id="ignoreWhiteSpace">
-                  <option value="IGNORE_NONE">None</option>
-                  <option value="IGNORE_TRAILING">Trailing</option>
-                  <option value="IGNORE_LEADING_AND_TRAILING">
-                    Leading & trailing
-                  </option>
-                  <option value="IGNORE_ALL">All</option>
-                </select>
-              </gr-select>
+              <md-outlined-select id="contextSelect" value="IGNORE_NONE">
+                <md-select-option
+                  data-aria-selected="true"
+                  md-menu-item=""
+                  tabindex="0"
+                  value="IGNORE_NONE"
+                >
+                  <div slot="headline">None</div>
+                </md-select-option>
+                <md-select-option
+                  md-menu-item=""
+                  tabindex="-1"
+                  value="IGNORE_TRAILING"
+                >
+                  <div slot="headline">Trailing</div>
+                </md-select-option>
+                <md-select-option
+                  md-menu-item=""
+                  tabindex="-1"
+                  value="IGNORE_LEADING_AND_TRAILING"
+                >
+                  <div slot="headline">Leading & trailing</div>
+                </md-select-option>
+                <md-select-option
+                  md-menu-item=""
+                  tabindex="-1"
+                  value="IGNORE_ALL"
+                >
+                  <div slot="headline">All</div>
+                </md-select-option>
+              </md-outlined-select>
             </span>
           </div>
         </section>
@@ -156,24 +209,24 @@
   test('renders preferences', () => {
     // Rendered with the expected preferences selected.
     const contextInput = valueOf('Context', 'diffPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(contextInput.bindValue, `${diffPreferences.context}`);
+      .firstElementChild as MdOutlinedSelect;
+    assert.equal(contextInput.value, `${diffPreferences.context}`);
 
     const lineWrappingInput = valueOf('Fit to screen', 'diffPreferences')
       .firstElementChild as HTMLInputElement;
     assert.equal(lineWrappingInput.checked, diffPreferences.line_wrapping);
 
     const lineLengthInput = valueOf('Diff width', 'diffPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(lineLengthInput.bindValue, `${diffPreferences.line_length}`);
+      .firstElementChild as MdOutlinedTextField;
+    assert.equal(lineLengthInput.value, `${diffPreferences.line_length}`);
 
     const tabSizeInput = valueOf('Tab width', 'diffPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(tabSizeInput.bindValue, `${diffPreferences.tab_size}`);
+      .firstElementChild as MdOutlinedTextField;
+    assert.equal(tabSizeInput.value, `${diffPreferences.tab_size}`);
 
     const fontSizeInput = valueOf('Font size', 'diffPreferences')
-      .firstElementChild as IronInputElement;
-    assert.equal(fontSizeInput.bindValue, `${diffPreferences.font_size}`);
+      .firstElementChild as MdOutlinedTextField;
+    assert.equal(fontSizeInput.value, `${diffPreferences.font_size}`);
 
     const showTabsInput = valueOf('Show tabs', 'diffPreferences')
       .firstElementChild as HTMLInputElement;
@@ -206,9 +259,9 @@
     const ignoreWhitespaceInput = valueOf(
       'Ignore Whitespace',
       'diffPreferences'
-    ).firstElementChild as GrSelect;
+    ).firstElementChild as MdOutlinedSelect;
     assert.equal(
-      ignoreWhitespaceInput.bindValue,
+      ignoreWhitespaceInput.value,
       diffPreferences.ignore_whitespace
     );
 
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index ac27ba6..9a7cabc 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -3,24 +3,24 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-tabs/paper-tab';
-import '@polymer/paper-tabs/paper-tabs';
 import '../gr-shell-command/gr-shell-command';
 import {queryAndAssert} from '../../../utils/common-util';
 import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
-import {paperStyles} from '../../../styles/gr-paper-styles';
+import {materialStyles} from '../../../styles/gr-material-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {BindValueChangeEvent} from '../../../types/events';
 import {resolve} from '../../../models/dependency';
 import {userModelToken} from '../../../models/user/user-model';
 import {subscribe} from '../../lit/subscription-controller';
+import '@material/web/tabs/secondary-tab';
+import '@material/web/tabs/tabs';
+import {MdTabs} from '@material/web/tabs/tabs';
 
 declare global {
   interface HTMLElementEventMap {
-    'selected-changed': CustomEvent<{value: number}>;
     'selected-scheme-changed': BindValueChangeEvent;
   }
   interface HTMLElementTagNameMap {
@@ -79,30 +79,16 @@
 
   static override get styles() {
     return [
-      paperStyles,
+      materialStyles,
       sharedStyles,
       css`
-        paper-tabs {
+        md-tabs {
           height: 3rem;
           margin-bottom: var(--spacing-m);
-          --paper-tabs-selection-bar-color: var(--link-color);
         }
-        paper-tab {
+        md-secondary-tab {
           max-width: 15rem;
           text-transform: uppercase;
-          --paper-tab-ink: var(--link-color);
-          --paper-font-common-base_-_font-family: var(--header-font-family);
-          --paper-font-common-base_-_-webkit-font-smoothing: initial;
-          --paper-tab-content_-_margin-bottom: var(--spacing-s);
-          /* paper-tabs uses 700 here, which can look awkward */
-          --paper-tab-content-focused_-_font-weight: var(--font-weight-h3);
-          --paper-tab-content-focused_-_background: var(
-            --gray-background-focus
-          );
-          --paper-tab-content-unselected_-_opacity: 1;
-          --paper-tab-content-unselected_-_color: var(
-            --deemphasized-text-color
-          );
         }
         label,
         input {
@@ -142,15 +128,18 @@
   private renderDownloadTabs() {
     const selectedIndex =
       this.schemes.findIndex(scheme => scheme === this.selectedScheme) || 0;
+    // md-tabs won't work if the index is -1, which happens on initial
+    // page load and then corrects its self.
+    if (selectedIndex < 0) return nothing;
     return html`
-      <paper-tabs
+      <md-tabs
         id="downloadTabs"
         class=${this.computeShowTabs()}
-        .selected=${selectedIndex}
-        @selected-changed=${this.handleTabChange}
+        .activeTabIndex=${selectedIndex}
+        @change=${this.handleTabChange}
       >
-        ${this.schemes.map(scheme => this.renderPaperTab(scheme))}
-      </paper-tabs>
+        ${this.schemes.map(scheme => this.renderMdSecondaryTab(scheme))}
+      </md-tabs>
     `;
   }
 
@@ -159,8 +148,10 @@
     return html`<div class="description">${this.description}</div>`;
   }
 
-  private renderPaperTab(scheme: string) {
-    return html` <paper-tab data-scheme=${scheme}>${scheme}</paper-tab> `;
+  private renderMdSecondaryTab(scheme: string) {
+    return html`
+      <md-secondary-tab data-scheme=${scheme}>${scheme}</md-secondary-tab>
+    `;
   }
 
   private renderCommands() {
@@ -192,8 +183,9 @@
     ).focusOnCopy();
   }
 
-  private handleTabChange = (e: CustomEvent<{value: number}>) => {
-    const scheme = this.schemes[e.detail.value];
+  private handleTabChange(e: Event) {
+    const activeTabIndex = (e.target as MdTabs).activeTabIndex;
+    const scheme = this.schemes[activeTabIndex];
     if (scheme && scheme !== this.selectedScheme) {
       this.selectedScheme = scheme;
       fire(this, 'selected-scheme-changed', {value: scheme});
@@ -203,7 +195,7 @@
         });
       }
     }
-  };
+  }
 
   private computeTooltip(index: number) {
     return index <= 8 && this.showKeyboardShortcutTooltips
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 2f656b2..05011f8f 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -16,11 +16,11 @@
 import {createPreferences} from '../../../test/test-data-generators';
 import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
 import {createDefaultPreferences} from '../../../constants/constants';
-import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
-import {fixture, html, assert} from '@open-wc/testing';
-import {PaperTabElement} from '@polymer/paper-tabs/paper-tab';
+import {assert, fixture, html} from '@open-wc/testing';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {testResolver} from '../../../test/common-test-setup';
+import {MdTabs} from '@material/web/tabs/tabs';
+import {MdSecondaryTab} from '@material/web/tabs/secondary-tab';
 
 suite('gr-download-commands', () => {
   let element: GrDownloadCommands;
@@ -67,41 +67,17 @@
         element,
         /* HTML */ `
           <div class="schemes">
-            <paper-tabs
-              dir="null"
-              id="downloadTabs"
-              role="tablist"
-              tabindex="0"
-            >
-              <paper-tab
-                aria-disabled="false"
-                aria-selected="true"
-                class="iron-selected"
-                data-scheme="http"
-                role="tab"
-                tabindex="0"
-              >
+            <md-tabs id="downloadTabs">
+              <md-secondary-tab data-scheme="http" md-tab="" tabindex="0">
                 http
-              </paper-tab>
-              <paper-tab
-                aria-disabled="false"
-                aria-selected="false"
-                data-scheme="repo"
-                role="tab"
-                tabindex="-1"
-              >
+              </md-secondary-tab>
+              <md-secondary-tab data-scheme="repo" md-tab="" tabindex="-1">
                 repo
-              </paper-tab>
-              <paper-tab
-                aria-disabled="false"
-                aria-selected="false"
-                data-scheme="ssh"
-                role="tab"
-                tabindex="-1"
-              >
+              </md-secondary-tab>
+              <md-secondary-tab data-scheme="ssh" md-tab="" tabindex="-1">
                 ssh
-              </paper-tab>
-            </paper-tabs>
+              </md-secondary-tab>
+            </md-tabs>
           </div>
           <div class="commands"></div>
           <gr-shell-command class="_label_checkout"> </gr-shell-command>
@@ -122,31 +98,28 @@
     });
 
     test('element visibility', async () => {
-      assert.isFalse(isHidden(queryAndAssert(element, 'paper-tabs')));
+      assert.isFalse(isHidden(queryAndAssert(element, 'md-tabs')));
       assert.isFalse(isHidden(queryAndAssert(element, '.commands')));
       assert.isTrue(Boolean(query(element, '#downloadTabs')));
 
       element.schemes = [];
       await element.updateComplete;
-      assert.isTrue(isHidden(queryAndAssert(element, 'paper-tabs')));
+      assert.isUndefined(query(element, 'md-tabs'));
       assert.isTrue(Boolean(query(element, '.commands')));
-      assert.isTrue(isHidden(queryAndAssert(element, '.commands')));
-      // Should still be present but hidden
-      assert.isTrue(Boolean(query(element, '#downloadTabs')));
-      assert.isTrue(isHidden(queryAndAssert(element, '#downloadTabs')));
+      assert.isFalse(Boolean(query(element, '#downloadTabs')));
     });
 
     test('tab selection', async () => {
       assert.equal(
-        queryAndAssert<PaperTabsElement>(element, '#downloadTabs').selected,
-        '0'
+        queryAndAssert<MdTabs>(element, '#downloadTabs').activeTabIndex,
+        0
       );
-      queryAndAssert<PaperTabElement>(element, '[data-scheme="ssh"]').click();
+      queryAndAssert<MdSecondaryTab>(element, '[data-scheme="ssh"]').click();
       await element.updateComplete;
       assert.equal(element.selectedScheme, 'ssh');
       assert.equal(
-        queryAndAssert<PaperTabsElement>(element, '#downloadTabs').selected,
-        '2'
+        queryAndAssert<MdTabs>(element, '#downloadTabs').activeTabIndex,
+        2
       );
     });
 
@@ -158,13 +131,15 @@
 
       await element.updateComplete;
 
-      const repoTab = queryAndAssert<PaperTabElement>(
+      const repoTab = queryAndAssert<MdSecondaryTab>(
         element,
-        'paper-tab[data-scheme="repo"]'
+        'md-secondary-tab[data-scheme="repo"]'
       );
 
       repoTab.click();
 
+      await element.updateComplete;
+
       assert.isTrue(savePrefsStub.called);
       assert.equal(
         savePrefsStub.lastCall.args[0].download_scheme,
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index f3160fb..65c7241 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -3,17 +3,14 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-dropdown/iron-dropdown';
-import '@polymer/paper-item/paper-item';
-import '@polymer/paper-listbox/paper-listbox';
 import '../../../styles/shared-styles';
 import '../gr-button/gr-button';
 import '../gr-date-formatter/gr-date-formatter';
-import '../gr-select/gr-select';
 import '../gr-file-status/gr-file-status';
-import {css, html, LitElement, PropertyValues} from 'lit';
-import {customElement, property, query} from 'lit/decorators.js';
-import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
+import '../gr-vote-chip/gr-vote-chip';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
+import {ApprovalInfo, LabelInfo} from '../../../api/rest-api';
 import {CommentThread, Timestamp} from '../../../types/common';
 import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
 import {GrButton} from '../gr-button/gr-button';
@@ -22,8 +19,15 @@
 import {ValueChangedEvent} from '../../../types/events';
 import {incrementalRepeat} from '../../lit/incremental-repeat';
 import {when} from 'lit/directives/when.js';
-import {isMagicPath} from '../../../utils/path-list-util';
+import {computeTruncatedPath, isMagicPath} from '../../../utils/path-list-util';
 import {fireNoBubble} from '../../../utils/event-util';
+import {classMap} from 'lit/directives/class-map.js';
+import '@material/web/divider/divider';
+import '@material/web/menu/menu';
+import '@material/web/menu/menu-item';
+import {MdMenu} from '@material/web/menu/menu';
+import {isSafari, Key} from '../../../utils/dom-util';
+import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
 
 /**
  * Required values are text and value. mobileText and triggerText will
@@ -44,6 +48,9 @@
   disabled?: boolean;
   file?: NormalizedFileInfo;
   commentThreads?: CommentThread[];
+  deemphasizeReason?: string;
+  vote?: ApprovalInfo;
+  label?: LabelInfo;
 }
 
 declare global {
@@ -54,7 +61,7 @@
 @customElement('gr-dropdown-list')
 export class GrDropdownList extends LitElement {
   @query('#dropdown')
-  dropdown?: IronDropdownElement;
+  dropdown?: MdMenu;
 
   @query('#trigger')
   trigger?: GrButton;
@@ -85,6 +92,22 @@
   @property({type: Boolean, attribute: 'show-copy-for-trigger-text'})
   showCopyForTriggerText = false;
 
+  @state()
+  selectedIndex = 0;
+
+  @state()
+  private opened = false;
+
+  @state() private hadKeyboardEvent = false;
+
+  cursor = new GrCursorManager();
+
+  constructor() {
+    super();
+    this.cursor.cursorTargetAttribute = 'selected';
+    this.cursor.focusOnMove = true;
+  }
+
   static override get styles() {
     return [
       sharedStyles,
@@ -102,17 +125,49 @@
           cursor: pointer;
           padding: 0;
         }
-        .dropdown-content {
-          background-color: var(--dropdown-background-color);
-          box-shadow: var(--elevation-level-2);
+        md-menu {
+          white-space: nowrap;
+          --md-menu-container-color: var(--dropdown-background-color);
+          --md-menu-top-space: 0px;
+          --md-menu-bottom-space: 0px;
+          --md-focus-ring-duration: 0s;
+          max-height: calc(100vh - 48px);
+        }
+        md-divider {
+          margin: auto;
+          --md-divider-color: var(--border-color);
+        }
+        md-menu-item {
           max-height: 70vh;
           min-width: 266px;
+          --md-sys-color-on-surface: var(
+            --gr-dropdown-item-color,
+            var(--primary-text-color, black)
+          );
+          --md-sys-color-on-secondary-container: var(
+            --gr-dropdown-item-color,
+            var(--primary-text-color, black)
+          );
+          --md-sys-typescale-body-large-font: inherit;
+          --md-menu-item-hover-state-layer-color: var(
+            --selection-background-color
+          );
+          --md-menu-item-hover-state-layer-opacity: 1;
+          --md-menu-item-selected-container-color: var(
+            --selection-background-color
+          );
+          --md-focus-ring-color: var(--gr-dropdown-focus-ring-color);
+          --md-menu-item-one-line-container-height: auto;
         }
-        paper-item:hover {
-          background-color: var(--hover-background-color);
+        md-menu-item[active] .topContent {
+          font-weight: bold;
         }
-        paper-item:not(:last-of-type) {
-          border-bottom: 1px solid var(--border-color);
+        .dropdown {
+          position: relative;
+        }
+        .dropdown-menu {
+          position: relative;
+          z-index: 120;
         }
         .bottomContent {
           color: var(--deemphasized-text-color);
@@ -129,60 +184,41 @@
           font-family: var(--trigger-style-font-family);
           --gr-button-text-color: var(--trigger-style-text-color);
         }
+        gr-vote-chip {
+          margin-right: var(--spacing-s);
+        }
         gr-date-formatter {
           color: var(--deemphasized-text-color);
           margin-left: var(--spacing-xxl);
           white-space: nowrap;
         }
-        gr-select {
-          display: none;
-        }
-        /* Because the iron dropdown 'area' includes the trigger, and the entire
-          width of the dropdown, we want to treat tapping the area above the
-          dropdown content as if it is tapping whatever content is underneath
-          it. The next two styles allow this to happen. */
-        iron-dropdown {
-          max-width: none;
-          pointer-events: none;
-          z-index: 120;
-        }
-        paper-listbox {
-          pointer-events: auto;
-          --paper-listbox_-_padding: 0;
-        }
-        paper-item {
-          cursor: pointer;
-          flex-direction: column;
-          font-size: inherit;
-          /* This variable was introduced in Dec 2019. We keep both min-height
-            * rules around, because --paper-item-min-height is not yet
-            * upstreamed.
-            */
-          --paper-item-min-height: 0;
-          --paper-item_-_min-height: 0;
-          --paper-item_-_padding: 10px 16px;
-          --paper-item-focused-before_-_background-color: var(
-            --selection-background-color
-          );
-          --paper-item-focused_-_background-color: var(
-            --selection-background-color
-          );
+        .topContent.deemphasized {
+          color: var(--deemphasized-text-color);
+          font-style: italic;
         }
         gr-comments-summary {
           padding-left: var(--spacing-s);
         }
+        .copyClipboard {
+          display: inline-flex;
+          vertical-align: top;
+        }
+        .mobileText {
+          display: none;
+        }
+        .desktopText {
+          display: inline-block;
+        }
+        gr-file-status {
+          margin-left: var(--spacing-xxl);
+        }
         @media only screen and (max-width: 50em) {
-          gr-select {
-            display: var(--gr-select-style-display, inline-block);
-            width: var(--gr-select-style-width);
+          .mobileText {
+            display: inline-block;
           }
-          gr-button,
-          iron-dropdown {
+          .desktopText {
             display: none;
           }
-          select {
-            width: var(--native-select-style-width);
-          }
         }
       `,
     ];
@@ -191,76 +227,183 @@
   protected override willUpdate(changedProperties: PropertyValues): void {
     if (changedProperties.has('value') || changedProperties.has('items')) {
       this.updateText();
+      this.computeSelectedIndex();
     }
     if (changedProperties.has('value')) {
       fireNoBubble(this, 'value-change', {value: this.value});
     }
   }
 
+  private computeSelectedIndex() {
+    if (this.value === undefined || this.items === undefined) {
+      this.selectedIndex = 0;
+      return;
+    }
+    const index = this.items.findIndex(
+      item => String(item.value) === this.value
+    );
+    this.selectedIndex = index >= 0 ? index : 0;
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('items')) {
+      this.resetCursorStops();
+    }
+
+    if (changedProperties.has('opened')) {
+      if (this.opened) {
+        this.resetCursorStops();
+        this.cursor.setCursorAtIndex(this.selectedIndex);
+        if (this.cursor.target !== null) {
+          this.cursor.target.focus();
+          if (isSafari() && !this.hadKeyboardEvent) {
+            const mdFocusRing = this.cursor.target?.shadowRoot
+              ?.querySelector('md-item')
+              ?.querySelector('md-focus-ring');
+            if (mdFocusRing) mdFocusRing.visible = false;
+          }
+        }
+        this.setUpGlobalEventListeners();
+      } else {
+        this.cleanUpGlobalEventListeners();
+      }
+    }
+  }
+
+  private setUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.addEventListener('resize', this.onWindowResize, passiveOptions);
+    window.addEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private cleanUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.removeEventListener('resize', this.onWindowResize, passiveOptions);
+    window.removeEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private readonly onWindowResize = () => {
+    this.dropdown?.reposition();
+  };
+
   override render() {
-    return html`
+    return html`<div class="dropdown">
       <gr-button
         id="trigger"
-        ?disabled=${this.disabled}
+        ?disabled=${!!this.disabled}
         down-arrow
         link
         class="dropdown-trigger"
         slot="dropdown-trigger"
         @click=${this.showDropdownTapHandler}
+        @keydown=${(e: KeyboardEvent) => {
+          this.hadKeyboardEvent = true;
+          if (
+            (e.key === Key.DOWN || e.key === Key.UP) &&
+            !this.dropdown?.open
+          ) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.dropdown?.show();
+          }
+        }}
+        @mousedown=${() => {
+          this.hadKeyboardEvent = false;
+        }}
+        @pointerdown=${() => {
+          this.hadKeyboardEvent = false;
+        }}
+        @touchstart=${() => {
+          this.hadKeyboardEvent = false;
+        }}
       >
-        <span id="triggerText">${this.text}</span>
+        <span id="triggerText" class="desktopText">${this.text}</span>
+        <span id="triggerText" class="mobileText"
+          >${computeTruncatedPath(this.text)}</span
+        >
         <gr-copy-clipboard
+          class="copyClipboard"
           ?hidden=${!this.showCopyForTriggerText}
           hideInput
           .text=${this.text}
         ></gr-copy-clipboard>
       </gr-button>
-      <iron-dropdown
-        id="dropdown"
-        .verticalAlign=${'top'}
-        .horizontalAlign=${'left'}
-        .dynamicAlign=${true}
-        .noOverlap=${true}
-        .allowOutsideScroll=${true}
-        @click=${this.handleDropdownClick}
-      >
-        <paper-listbox
-          class="dropdown-content"
-          slot="dropdown-content"
-          .attrForSelected=${'data-value'}
-          .selected=${this.value}
-          @selected-changed=${this.selectedChanged}
+      <div class="dropdown-menu">
+        <md-menu
+          id="dropdown"
+          anchor="trigger"
+          default-focus="none"
+          tabindex="-1"
+          .menuCorner=${'start-start'}
+          ?quick=${true}
+          .skipRestoreFocus=${true}
+          positioning="fixed"
+          @click=${this.handleDropdownClick}
+          @opened=${(e: Event) => {
+            this.opened = true;
+            this.scrollToSelected(e);
+          }}
+          @closing=${this.handleMenuClosing}
+          @closed=${() => {
+            this.opened = false;
+            this.hadKeyboardEvent = false;
+            // This is an ugly hack but works.
+            this.cursor.target?.removeAttribute('selected');
+          }}
         >
           ${incrementalRepeat({
             values: this.items ?? [],
             initialCount: this.initialCount,
-            mapFn: item => this.renderPaperItem(item as DropdownItem),
+            mapFn: (item, index) =>
+              this.renderMdMenuItem(item as DropdownItem, index),
           })}
-        </paper-listbox>
-      </iron-dropdown>
-      <gr-select
-        .bindValue=${this.value}
-        @bind-value-changed=${this.selectedChanged}
-      >
-        <select>
-          ${this.items?.map(
-            item => html`
-              <option ?disabled=${item.disabled} value=${`${item.value}`}>
-                ${this.computeMobileText(item)}
-              </option>
-            `
-          )}
-        </select>
-      </gr-select>
-    `;
+        </md-menu>
+      </div>
+    </div> `;
   }
 
-  private renderPaperItem(item: DropdownItem) {
+  private renderMdMenuItem(item: DropdownItem, index: number) {
     return html`
-      <paper-item ?disabled=${item.disabled} data-value=${item.value}>
-        <div class="topContent">
+      <md-menu-item
+        ?selected=${this.value === String(item.value)}
+        ?active=${this.value === String(item.value)}
+        ?disabled=${!!item.disabled}
+        @click=${() => {
+          this.value = String(item.value);
+        }}
+        @keydown=${(e: KeyboardEvent) => {
+          if (e.key === Key.ENTER || e.key === Key.SPACE) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.handleEnter();
+          }
+          if (e.key === Key.UP) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.handleUp();
+          }
+          if (e.key === Key.DOWN) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.handleDown();
+          }
+        }}
+      >
+        <div
+          class=${classMap({
+            topContent: true,
+            deemphasized: !!item.deemphasizeReason,
+          })}
+        >
           <div>
-            <span>${item.text}</span>
+            <span class="desktopText">${item.text}</span>
+            <span class="mobileText">${this.computeMobileText(item)}</span>
+            ${when(
+              !!item.deemphasizeReason,
+              () => html`<span>| ${item.deemphasizeReason}</span>`
+            )}
             ${when(
               item.commentThreads,
               () => html`<gr-comments-summary
@@ -271,6 +414,16 @@
             )}
           </div>
           ${when(
+            item.vote,
+            () =>
+              html` <div>
+                <gr-vote-chip
+                  .vote=${item.vote}
+                  .label=${item.label}
+                ></gr-vote-chip>
+              </div>`
+          )}
+          ${when(
             item.date,
             () => html`
               <gr-date-formatter .dateStr=${item.date}></gr-date-formatter>
@@ -291,24 +444,46 @@
             </div>
           `
         )}
-      </paper-item>
+      </md-menu-item>
+      ${index < this.items!.length - 1
+        ? html`<md-divider role="separator" tabindex="-1"></md-divider>`
+        : nothing}
     `;
   }
 
-  private selectedChanged(e: ValueChangedEvent<string>) {
-    this.value = e.detail.value;
+  /**
+   * Handle the up key.
+   */
+  private handleUp() {
+    this.cursor.previous();
   }
 
   /**
-   * Handle a click on the iron-dropdown element.
+   * Handle the down key.
+   */
+  private handleDown() {
+    this.cursor.next();
+  }
+
+  /**
+   * Handle the enter key.
+   */
+  private handleEnter() {
+    if (this.cursor.target !== null) {
+      const el = this.cursor.target.shadowRoot?.querySelector(':not([hidden])');
+      if (el) {
+        (el as HTMLElement).click();
+      }
+    }
+  }
+
+  /**
+   * Handle a click on the md-menu element.
    */
   private handleDropdownClick() {
-    // async is needed so that that the click event is fired before the
-    // dropdown closes (This was a bug for touch devices).
-    setTimeout(() => {
-      assertIsDefined(this.dropdown);
-      this.dropdown.close();
-    }, 1);
+    assertIsDefined(this.dropdown);
+
+    this.dropdown.close();
   }
 
   private updateText() {
@@ -327,7 +502,9 @@
   /**
    * Handle a click on the button to open the dropdown.
    */
-  private showDropdownTapHandler() {
+  private showDropdownTapHandler(e: Event) {
+    e.preventDefault();
+    e.stopPropagation();
     this.open();
   }
 
@@ -336,13 +513,43 @@
    */
   open() {
     assertIsDefined(this.dropdown);
-    this.dropdown.open();
+    this.dropdown.open = !this.dropdown.open;
+    this.dropdown.focus();
   }
 
   // Private but used in tests.
   computeMobileText(item: DropdownItem) {
     return item.mobileText ? item.mobileText : item.text;
   }
+
+  private scrollToSelected(e: Event) {
+    const target = e.target as HTMLElement;
+    const selected = target.querySelector<MdMenu>('md-menu-item[selected]');
+    selected?.scrollIntoView({block: 'nearest'});
+  }
+
+  /**
+   * Recompute the stops for the dropdown item cursor.
+   */
+  private resetCursorStops() {
+    assertIsDefined(this.dropdown);
+    if (this.items && this.items.length > 0 && this.dropdown.open) {
+      this.cursor.stops = Array.from(
+        this.shadowRoot?.querySelectorAll('md-menu-item') ?? []
+      );
+    }
+  }
+
+  private handleMenuClosing() {
+    // Blur focused item before aria-hidden="true" is applied
+    // Fixes console warning about aria-hidden can't be set because
+    // md-menu is focused already.
+    const active = (this.shadowRoot?.activeElement ??
+      document.activeElement) as HTMLElement;
+    if (active && this.dropdown?.contains(active)) {
+      active.blur();
+    }
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts
index fe856dd..dae104a 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts
@@ -13,10 +13,9 @@
   queryAndAssert,
   waitEventLoop,
 } from '../../../test/test-utils';
-import {PaperListboxElement} from '@polymer/paper-listbox';
 import {Timestamp} from '../../../types/common';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-dropdown-list tests', () => {
   let element: GrDropdownList;
@@ -56,85 +55,75 @@
     assert.shadowDom.equal(
       element,
       /* HTML */ `
-        <gr-button
-          aria-disabled="false"
-          class="dropdown-trigger"
-          down-arrow=""
-          id="trigger"
-          link=""
-          role="button"
-          slot="dropdown-trigger"
-          tabindex="0"
-        >
-          <span id="triggerText"> Button Text 2 </span>
-          <gr-copy-clipboard hidden="" hideinput=""> </gr-copy-clipboard>
-        </gr-button>
-        <iron-dropdown
-          aria-disabled="false"
-          aria-hidden="true"
-          horizontal-align="left"
-          id="dropdown"
-          style="outline: none; display: none;"
-          vertical-align="top"
-        >
-          <paper-listbox
-            class="dropdown-content"
-            role="listbox"
-            slot="dropdown-content"
+        <div class="dropdown">
+          <gr-button
+            aria-disabled="false"
+            class="dropdown-trigger"
+            down-arrow=""
+            id="trigger"
+            link=""
+            role="button"
+            slot="dropdown-trigger"
             tabindex="0"
           >
-            <paper-item
-              aria-disabled="false"
-              aria-selected="false"
-              data-value="1"
-              role="option"
+            <span id="triggerText" class="desktopText"> Button Text 2 </span>
+            <span id="triggerText" class="mobileText"> Button Text 2 </span>
+            <gr-copy-clipboard class="copyClipboard" hidden="" hideinput="">
+            </gr-copy-clipboard>
+          </gr-button>
+          <div class="dropdown-menu">
+            <md-menu
+              anchor="trigger"
+              default-focus="none"
+              aria-hidden="true"
+              id="dropdown"
+              positioning="fixed"
+              quick=""
               tabindex="-1"
             >
-              <div class="topContent">
-                <div><span>Top Text 1</span></div>
-              </div>
-            </paper-item>
-            <paper-item
-              aria-disabled="false"
-              aria-selected="true"
-              class="iron-selected"
-              data-value="2"
-              role="option"
-              tabindex="0"
-            >
-              <div class="topContent">
-                <div><span>Top Text 2</span></div>
-              </div>
-              <div class="bottomContent">
-                <div>Bottom Text 2</div>
-              </div>
-            </paper-item>
-            <paper-item
-              aria-disabled="true"
-              aria-selected="false"
-              data-value="3"
-              disabled=""
-              role="option"
-              style="pointer-events: none;"
-              tabindex="-1"
-            >
-              <div class="topContent">
-                <div><span>Top Text 3</span></div>
-                <gr-date-formatter> </gr-date-formatter>
-              </div>
-              <div class="bottomContent">
-                <div>Bottom Text 3</div>
-              </div>
-            </paper-item>
-          </paper-listbox>
-        </iron-dropdown>
-        <gr-select>
-          <select>
-            <option value="1">Top Text 1</option>
-            <option value="2">Mobile Text 2</option>
-            <option disabled="" value="3">Mobile Text 3</option>
-          </select>
-        </gr-select>
+              <md-menu-item md-menu-item="" tabindex="0">
+                <div class="topContent">
+                  <div>
+                  <span class="desktopText">
+                    Top Text 1
+                  </span>
+                  <span class="mobileText">
+                    Top Text 1
+                  </div>
+                </div>
+              </md-menu-item>
+              <md-divider role="separator" tabindex="-1"> </md-divider>
+              <md-menu-item active="" md-menu-item="" selected="" tabindex="-1">
+                <div class="topContent">
+                  <div>
+                  <span class="desktopText">
+                    Top Text 2
+                  </span>
+                    <span class="mobileText"> Mobile Text 2 </span>
+                  </div>
+                </div>
+                <div class="bottomContent">
+                  <div>Bottom Text 2</div>
+                </div>
+              </md-menu-item>
+              <md-divider role="separator" tabindex="-1"> </md-divider>
+              <md-menu-item disabled="" md-menu-item="" tabindex="-1">
+                <div class="topContent">
+                  <div>
+                  <span class="desktopText">
+                    Top Text 3
+                  </span>
+                    <span class="mobileText"> Mobile Text 3 </span>
+                  </div>
+                  <gr-date-formatter> </gr-date-formatter>
+                </div>
+                <div class="bottomContent">
+                  <div>Bottom Text 3</div>
+                </div>
+              </md-menu-item>
+            </md-menu>
+          </div>
+        </div>
       `
     );
   });
@@ -162,13 +151,13 @@
   test('tap on trigger opens menu', () => {
     sinon.stub(element, 'open').callsFake(() => {
       assertIsDefined(element.dropdown);
-      element.dropdown.open();
+      element.dropdown.show();
     });
     assertIsDefined(element.dropdown);
-    assert.isFalse(element.dropdown.opened);
+    assert.isFalse(element.dropdown.open);
     assertIsDefined(element.trigger);
     element.trigger.click();
-    assert.isTrue(element.dropdown.opened);
+    assert.isTrue(element.dropdown.open);
   });
 
   test('computeMobileText', () => {
@@ -208,89 +197,51 @@
     await element.updateComplete;
     await waitEventLoop();
 
-    assert.equal(
-      queryAndAssert<PaperListboxElement>(element, 'paper-listbox').selected,
-      element.value
-    );
-    assert.equal(element.text, 'Button Text 2');
+    const menu = queryAndAssert<HTMLElement>(element, 'md-menu');
+    const items = queryAll<HTMLElement>(menu, 'md-menu-item');
 
-    const items = queryAll<HTMLInputElement>(element, 'paper-item');
-    const mobileItems = queryAll<HTMLOptionElement>(element, 'option');
     assert.equal(items.length, 3);
-    assert.equal(mobileItems.length, 3);
 
-    // First Item
-    // The first item should be disabled, has no bottom text, and no date.
-    assert.isFalse(!!items[0].disabled);
-    assert.isFalse(mobileItems[0].disabled);
-    assert.isFalse(items[0].classList.contains('iron-selected'));
-    assert.isFalse(mobileItems[0].selected);
-
+    // Item 0 (First)
+    assert.isFalse(items[0].hasAttribute('disabled'));
+    assert.isFalse(items[0].hasAttribute('selected'));
     assert.isNotOk(items[0].querySelector('gr-date-formatter'));
     assert.isNotOk(items[0].querySelector('.bottomContent'));
-    assert.equal(items[0].dataset.value, element.items[0].value as any);
-    assert.equal(mobileItems[0].value, element.items[0].value);
     assert.equal(
-      queryAndAssert<HTMLDivElement>(items[0], '.topContent div span')
+      queryAndAssert<HTMLSpanElement>(items[0], '.topContent div span')
         .innerText,
       element.items[0].text
     );
 
-    // Since no mobile specific text, it should fall back to text.
-    assert.equal(mobileItems[0].text, element.items[0].text);
-
-    // Second Item
-    // The second item should have top text, bottom text, and no date.
-    assert.isFalse(!!items[1].disabled);
-    assert.isFalse(mobileItems[1].disabled);
-    assert.isTrue(items[1].classList.contains('iron-selected'));
-    assert.isTrue(mobileItems[1].selected);
-
+    // Item 1 (Second)
+    assert.isFalse(items[1].hasAttribute('disabled'));
+    assert.isTrue(items[1].hasAttribute('selected'));
     assert.isNotOk(items[1].querySelector('gr-date-formatter'));
     assert.isOk(items[1].querySelector('.bottomContent'));
-    assert.equal(items[1].dataset.value, element.items[1].value as any);
-    assert.equal(mobileItems[1].value, element.items[1].value);
     assert.equal(
-      queryAndAssert<HTMLDivElement>(items[1], '.topContent div span')
+      queryAndAssert<HTMLSpanElement>(items[1], '.topContent div span')
         .textContent,
       element.items[1].text
     );
-
-    // Since there is mobile specific text, it should that.
-    assert.equal(mobileItems[1].text, element.items[1].mobileText);
-
-    // Since this item is selected, and it has triggerText defined, that
-    // should be used.
     assert.equal(element.text, element.items[1].triggerText);
 
-    // Third item
-    // The third item should be disabled, and have a date, and bottom content.
-    assert.isTrue(!!items[2].disabled);
-    assert.isTrue(mobileItems[2].disabled);
-    assert.isFalse(items[2].classList.contains('iron-selected'));
-    assert.isFalse(mobileItems[2].selected);
-
+    // Item 2 (Third)
+    assert.isTrue(items[2].hasAttribute('disabled'));
+    assert.isFalse(items[2].hasAttribute('selected'));
     assert.isOk(items[2].querySelector('gr-date-formatter'));
     assert.isOk(items[2].querySelector('.bottomContent'));
-    assert.equal(items[2].dataset.value, element.items[2].value as any);
-    assert.equal(mobileItems[2].value, element.items[2].value);
     assert.equal(
-      queryAndAssert<HTMLDivElement>(items[2], '.topContent div span')
+      queryAndAssert<HTMLSpanElement>(items[2], '.topContent div span')
         .innerText,
       element.items[2].text
     );
 
-    // Since there is mobile specific text, it should that.
-    assert.equal(mobileItems[2].text, element.items[2].mobileText);
-
-    // Select a new item.
+    // Select first item
     items[0].click();
     await element.updateComplete;
-    assert.equal(element.value, '1');
-    assert.isTrue(items[0].classList.contains('iron-selected'));
-    assert.isTrue(mobileItems[0].selected);
 
-    // Since no triggerText, the fallback is used.
+    assert.equal(element.value, '1');
+    assert.isTrue(items[0].hasAttribute('selected'));
     assert.equal(element.text, element.items[0].text);
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
index 0ae647d..910d481 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -3,25 +3,26 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-dropdown/iron-dropdown';
 import '../gr-button/gr-button';
 import {GrButton} from '../gr-button/gr-button';
 import '../gr-cursor-manager/gr-cursor-manager';
 import '../gr-tooltip-content/gr-tooltip-content';
 import '../../../styles/shared-styles';
 import {getBaseUrl} from '../../../utils/url-util';
-import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
 import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
-import {property, customElement, query, state} from 'lit/decorators.js';
-import {Key} from '../../../utils/dom-util';
+import {customElement, property, query, state} from 'lit/decorators.js';
+import {isSafari, Key} from '../../../utils/dom-util';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {fire} from '../../../utils/event-util';
 import {ValueChangedEvent} from '../../../types/events';
 import {assertIsDefined} from '../../../utils/common-util';
-import {ShortcutController} from '../../lit/shortcut-controller';
 import {DropdownLink} from '../../../types/common';
+import '@material/web/divider/divider';
+import '@material/web/menu/menu';
+import '@material/web/menu/menu-item';
+import {MdMenu} from '@material/web/menu/menu';
 
 const REL_NOOPENER = 'noopener';
 const REL_EXTERNAL = 'external';
@@ -43,7 +44,7 @@
 @customElement('gr-dropdown')
 export class GrDropdown extends LitElement {
   @query('#dropdown')
-  dropdown?: IronDropdownElement;
+  dropdown?: MdMenu;
 
   @query('#trigger')
   trigger?: GrButton;
@@ -55,16 +56,25 @@
         :host {
           display: inline-block;
         }
+        .container {
+          position: relative;
+        }
         .dropdown-trigger {
           text-decoration: none;
           width: 100%;
         }
         .dropdown-content {
-          background-color: var(--dropdown-background-color);
-          box-shadow: var(--elevation-level-2);
           min-width: 112px;
           max-width: 280px;
         }
+        md-menu {
+          white-space: nowrap;
+          --md-menu-container-color: var(--dropdown-background-color);
+          --md-menu-top-space: 0px;
+          --md-menu-bottom-space: 0px;
+          --md-focus-ring-duration: 0s;
+          max-height: calc(100vh - 48px);
+        }
         gr-button {
           vertical-align: top;
         }
@@ -79,44 +89,6 @@
         ul {
           list-style: none;
         }
-        .topContent,
-        li {
-          border-bottom: 1px solid var(--border-color);
-        }
-        li:last-of-type {
-          border: none;
-        }
-        li .itemAction {
-          cursor: pointer;
-          display: block;
-          padding: var(--spacing-m) var(--spacing-l);
-        }
-        li .itemAction {
-          color: var(--gr-dropdown-item-color);
-          background-color: var(--gr-dropdown-item-background-color);
-          border: var(--gr-dropdown-item-border);
-          text-transform: var(--gr-dropdown-item-text-transform);
-        }
-        li .itemAction.disabled {
-          color: var(--deemphasized-text-color);
-          cursor: default;
-        }
-        li .itemAction:link,
-        li .itemAction:visited {
-          text-decoration: none;
-        }
-        li .itemAction:not(.disabled):hover {
-          background-color: var(--hover-background-color);
-        }
-        li:focus,
-        li.selected {
-          background-color: var(--selection-background-color);
-          outline: none;
-        }
-        li:focus .itemAction,
-        li.selected .itemAction {
-          background-color: transparent;
-        }
         .topContent {
           display: block;
           padding: var(--spacing-m) var(--spacing-l);
@@ -128,6 +100,34 @@
         .bold-text {
           font-weight: var(--font-weight-medium);
         }
+        md-divider {
+          margin: auto;
+          --md-divider-color: var(--border-color);
+        }
+        md-menu-item {
+          --md-sys-color-on-surface: var(
+            --gr-dropdown-item-color,
+            var(--primary-text-color, black)
+          );
+          --md-sys-color-on-secondary-container: var(
+            --gr-dropdown-item-color,
+            var(--primary-text-color, black)
+          );
+          --md-sys-typescale-body-large-font: inherit;
+          --md-menu-item-hover-state-layer-color: var(
+            --selection-background-color
+          );
+          --md-menu-item-hover-state-layer-opacity: 1;
+          --md-menu-item-selected-container-color: var(
+            --selection-background-color
+          );
+          --md-focus-ring-color: var(--gr-dropdown-focus-ring-color);
+          --md-menu-item-one-line-container-height: auto;
+        }
+        .itemAction:link,
+        .itemAction:visited {
+          text-decoration: none;
+        }
       `,
     ];
   }
@@ -163,7 +163,7 @@
   link = false;
 
   @property({type: Number, attribute: 'vertical-offset'})
-  verticalOffset = 40;
+  verticalOffset = 0;
 
   @state()
   private opened = false;
@@ -175,28 +175,15 @@
   @property({type: Array})
   disabledIds: string[] = [];
 
+  @state() private hadKeyboardEvent = false;
+
   // Used within the tests so needs to be non-private.
   cursor = new GrCursorManager();
 
-  private readonly shortcuts = new ShortcutController(this);
-
   constructor() {
     super();
-    this.cursor.cursorTargetClass = 'selected';
+    this.cursor.cursorTargetAttribute = 'selected';
     this.cursor.focusOnMove = true;
-    this.shortcuts.addLocal({key: Key.UP}, () => this.handleUp());
-    this.shortcuts.addLocal({key: Key.DOWN}, () => this.handleDown());
-    this.shortcuts.addLocal({key: Key.ENTER}, () => this.handleEnter());
-    this.shortcuts.addLocal({key: Key.SPACE}, () => this.handleEnter());
-  }
-
-  override connectedCallback() {
-    super.connectedCallback();
-  }
-
-  override disconnectedCallback() {
-    this.cursor.unsetCursor();
-    super.disconnectedCallback();
   }
 
   override willUpdate(changedProperties: PropertyValues) {
@@ -209,44 +196,114 @@
     if (changedProperties.has('items')) {
       this.resetCursorStops();
     }
-    if (changedProperties.has('opened') && this.opened) {
-      this.resetCursorStops();
-      this.cursor.setCursorAtIndex(0);
-      if (this.cursor.target !== null) this.cursor.target.focus();
+
+    if (changedProperties.has('opened')) {
+      if (this.opened) {
+        this.resetCursorStops();
+        this.cursor.setCursorAtIndex(0);
+        if (this.cursor.target !== null) {
+          this.cursor.target.focus();
+          if (isSafari() && !this.hadKeyboardEvent) {
+            const mdFocusRing = this.cursor.target?.shadowRoot
+              ?.querySelector('md-item')
+              ?.querySelector('md-focus-ring');
+            if (mdFocusRing) mdFocusRing.visible = false;
+          }
+        }
+        this.setUpGlobalEventListeners();
+      } else {
+        this.cleanUpGlobalEventListeners();
+      }
     }
   }
 
+  private setUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.addEventListener('resize', this.onWindowResize, passiveOptions);
+    window.addEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private cleanUpGlobalEventListeners() {
+    const passiveOptions: AddEventListenerOptions = {passive: true};
+
+    window.removeEventListener('resize', this.onWindowResize, passiveOptions);
+    window.removeEventListener('scroll', this.onWindowResize, passiveOptions);
+  }
+
+  private readonly onWindowResize = () => {
+    this.dropdown?.reposition();
+  };
+
   override render() {
-    return html` <gr-button
+    return html`<div class="container">
+      <gr-button
+        id="trigger"
         ?link=${this.link}
         class="dropdown-trigger"
-        id="trigger"
         ?down-arrow=${this.downArrow}
         @click=${this.dropdownTriggerTapHandler}
+        @keydown=${(e: KeyboardEvent) => {
+          this.hadKeyboardEvent = true;
+          if (
+            (e.key === Key.DOWN || e.key === Key.UP) &&
+            !this.dropdown?.open
+          ) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.dropdown?.show();
+          }
+        }}
+        @mousedown=${() => {
+          this.hadKeyboardEvent = false;
+        }}
+        @pointerdown=${() => {
+          this.hadKeyboardEvent = false;
+        }}
+        @touchstart=${() => {
+          this.hadKeyboardEvent = false;
+        }}
       >
         <slot></slot>
       </gr-button>
-      <iron-dropdown
+      <md-menu
+        default-focus="none"
         id="dropdown"
-        .verticalAlign=${'top'}
-        .verticalOffset=${this.verticalOffset}
-        allowOutsideScroll
-        .horizontalAlign=${this.horizontalAlign}
-        @click=${() => this.close()}
-        @opened-changed=${(e: ValueChangedEvent<boolean>) =>
-          (this.opened = e.detail.value)}
+        anchor="trigger"
+        tabindex="-1"
+        .menuCorner=${this.horizontalAlign === 'left'
+          ? 'start-start'
+          : this.horizontalAlign === 'center'
+          ? 'start-end'
+          : 'end-start'}
+        .yOffset=${this.verticalOffset}
+        ?quick=${true}
+        .skipRestoreFocus=${true}
+        @opened=${() => {
+          this.opened = true;
+        }}
+        @closing=${this.handleMenuClosing}
+        @closed=${() => {
+          this.opened = false;
+          this.hadKeyboardEvent = false;
+          // This is an ugly hack but works.
+          this.cursor.target?.removeAttribute('selected');
+        }}
       >
         ${this.renderDropdownContent()}
-      </iron-dropdown>`;
+      </md-menu>
+    </div>`;
   }
 
   private renderDropdownContent() {
-    return html` <div class="dropdown-content" slot="dropdown-content">
-      <ul>
+    return html`
+      <div class="dropdown-content">
         ${this.renderTopContent()}
-        ${(this.items ?? []).map(link => this.renderDropdownLink(link))}
-      </ul>
-    </div>`;
+        ${(this.items ?? []).map((link, index) =>
+          this.renderDropdownLink(link, index)
+        )}
+      </div>
+    `;
   }
 
   private renderTopContent() {
@@ -255,6 +312,7 @@
       <div class="topContent">
         ${(this.topContent ?? []).map(item => this.renderTopContentItem(item))}
       </div>
+      <md-divider role="separator" tabindex="-1"></md-divider>
     `;
   }
 
@@ -266,22 +324,37 @@
     `;
   }
 
-  private renderDropdownLink(link: DropdownLink) {
-    const disabledClass = this.computeDisabledClass(link.id);
-    return html`
-      <li tabindex="-1">
-        <gr-tooltip-content
-          ?has-tooltip=${!!link.tooltip}
-          title=${ifDefined(link.tooltip)}
-        >
-          <span
-            class="itemAction ${disabledClass}"
-            data-id=${ifDefined(link.id)}
-            @click=${this.handleItemTap}
-            ?hidden=${!!link.url}
-            tabindex="-1"
-            >${link.name}</span
-          >
+  private renderDropdownLink(link: DropdownLink, index: number) {
+    const itemContent = html`
+      <md-menu-item
+        data-index=${index}
+        ?selected=${index === 0}
+        ?active=${index === 0}
+        ?disabled=${!!link.id && this.disabledIds.includes(link.id)}
+        @keydown=${(e: KeyboardEvent) => {
+          if (e.key === Key.ENTER || e.key === Key.SPACE) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.handleEnter();
+          }
+          if (e.key === Key.UP) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.handleUp();
+          }
+          if (e.key === Key.DOWN) {
+            e.preventDefault();
+            e.stopPropagation();
+            this.handleDown();
+          }
+        }}
+      >
+        ${link.name}
+      </md-menu-item>
+    `;
+
+    const linkWrapper = link.url
+      ? html`
           <a
             class="itemAction"
             href=${this.computeLinkURL(link)}
@@ -290,10 +363,28 @@
             target=${ifDefined(link.target ?? undefined)}
             ?hidden=${!link.url}
             tabindex="-1"
-            >${link.name}</a
           >
-        </gr-tooltip-content>
-      </li>
+            ${itemContent}
+          </a>
+        `
+      : html`<span
+          class="itemAction"
+          data-id=${ifDefined(link.id)}
+          @click=${this.handleItemTap}
+          ?hidden=${!!link.url}
+          tabindex="-1"
+          >${itemContent}</span
+        >`;
+    return html`
+      <gr-tooltip-content
+        ?has-tooltip=${!!link.tooltip}
+        title=${ifDefined(link.tooltip)}
+      >
+        ${linkWrapper}
+      </gr-tooltip-content>
+      ${index < this.items!.length - 1
+        ? html`<md-divider role="separator" tabindex="-1"></md-divider>`
+        : nothing}
     `;
   }
 
@@ -301,75 +392,37 @@
    * Handle the up key.
    */
   private handleUp() {
-    assertIsDefined(this.dropdown);
-    if (this.dropdown.opened) {
-      this.cursor.previous();
-    } else {
-      this.open();
-    }
+    this.cursor.previous();
   }
 
   /**
    * Handle the down key.
    */
   private handleDown() {
-    assertIsDefined(this.dropdown);
-    if (this.dropdown.opened) {
-      this.cursor.next();
-    } else {
-      this.open();
-    }
+    this.cursor.next();
   }
 
   /**
    * Handle the enter key.
    */
   private handleEnter() {
-    assertIsDefined(this.dropdown);
-    if (this.dropdown.opened) {
-      // Since gr-tooltip-content click on shadow dom is not propagated down,
-      // we have to target `a` inside it.
-      if (this.cursor.target !== null) {
-        const el = this.cursor.target.querySelector(':not([hidden]) a');
-        if (el) {
-          (el as HTMLElement).click();
-        }
+    // Since gr-tooltip-content click on shadow dom is not propagated down,
+    // we have to target `a` inside it.
+    if (this.cursor.target !== null) {
+      const el = this.cursor.target.shadowRoot?.querySelector(':not([hidden])');
+      if (el) {
+        (el as HTMLElement).click();
       }
-    } else {
-      this.open();
     }
   }
 
   /**
    * Handle a click on the button to open the dropdown.
    */
-  private dropdownTriggerTapHandler(e: MouseEvent) {
+  dropdownTriggerTapHandler() {
     assertIsDefined(this.dropdown);
-    e.preventDefault();
-    e.stopPropagation();
-    if (this.dropdown.opened) {
-      this.close();
-    } else {
-      this.open();
-    }
-  }
 
-  /**
-   * Open the dropdown and initialize the cursor.
-   * Private but used in tests.
-   */
-  open() {
-    assertIsDefined(this.dropdown);
-    this.dropdown.open();
-  }
-
-  // Private but used in tests.
-  close() {
-    // async is needed so that that the click event is fired before the
-    // dropdown closes (This was a bug for touch devices).
-    setTimeout(() => {
-      this.dropdown?.close();
-    }, 1);
+    this.dropdown.open = !this.dropdown.open;
   }
 
   /**
@@ -388,9 +441,11 @@
    * Build a URL for the given host and path. The base URL will be only added,
    * if it is not already included in the path.
    *
+   * Private but used in tests.
+   *
    * @return The scheme-relative URL.
    */
-  _computeURLHelper(host: string, path: string) {
+  computeURLHelper(host: string, path: string) {
     const base = path.startsWith(getBaseUrl()) ? '' : getBaseUrl();
     return '//' + host + base + path;
   }
@@ -405,7 +460,7 @@
    */
   private computeRelativeURL(path: string) {
     const host = window.location.host;
-    return this._computeURLHelper(host, path);
+    return this.computeURLHelper(host, path);
   }
 
   /**
@@ -417,7 +472,7 @@
     if (typeof link.url === 'undefined') {
       return '';
     }
-    if (link.target || !link.url.startsWith('/')) {
+    if (!link.url.startsWith('/')) {
       return link.url;
     }
     return this.computeRelativeURL(link.url);
@@ -444,10 +499,12 @@
    * Handle a click on an item of the dropdown.
    */
   private handleItemTap(e: MouseEvent) {
-    if (e.target === null || !this.items) {
+    if (e.currentTarget === null || !this.items) {
       return;
     }
-    const id = (e.target as Element).getAttribute('data-id');
+
+    const target = e.currentTarget as HTMLElement;
+    const id = target.getAttribute('data-id');
     const item = this.items.find(item => item.id === id);
     if (id && !this.disabledIds.includes(id)) {
       if (item) {
@@ -455,26 +512,32 @@
       }
       this.dispatchEvent(new CustomEvent('tap-item-' + id));
     }
+
+    // For some reason this is needed, otherwise a console warning is thrown,
+    // with something about aria-hidden can't be set because md-menu is focused already.
+    target.blur();
   }
 
   /**
    * Recompute the stops for the dropdown item cursor.
    */
-  private resetCursorStops() {
+  resetCursorStops() {
     assertIsDefined(this.dropdown);
-    if (this.items && this.items.length > 0 && this.dropdown?.opened) {
+    if (this.items && this.items.length > 0 && this.dropdown.open) {
       this.cursor.stops = Array.from(
-        this.shadowRoot?.querySelectorAll('li') ?? []
+        this.shadowRoot?.querySelectorAll('md-menu-item') ?? []
       );
     }
   }
 
-  /**
-   * If a dropdown item is shown as a button, get the class for the button.
-   *
-   * @return The class for the item button.
-   */
-  private computeDisabledClass(id?: string) {
-    return id && this.disabledIds.includes(id) ? 'disabled' : '';
+  private handleMenuClosing() {
+    // Blur focused item before aria-hidden="true" is applied
+    // Fixes console warning about aria-hidden can't be set because
+    // md-menu is focused already.
+    const active = (this.shadowRoot?.activeElement ??
+      document.activeElement) as HTMLElement;
+    if (active && this.dropdown?.contains(active)) {
+      active.blur();
+    }
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
index 3a01748..3230e48 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
@@ -10,8 +10,9 @@
 import {pressKey, queryAll, queryAndAssert} from '../../../test/test-utils';
 import {GrTooltipContent} from '../gr-tooltip-content/gr-tooltip-content';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html, waitUntil} from '@open-wc/testing';
 import {DropdownLink} from '../../../types/common';
+import {MdMenuItem} from '@material/web/menu/menu-item';
 
 suite('gr-dropdown tests', () => {
   let element: GrDropdown;
@@ -20,28 +21,78 @@
     element = await fixture(html`<gr-dropdown></gr-dropdown>`);
   });
 
+  test('render', async () => {
+    element.items = [
+      {name: 'item one', id: 'foo', tooltip: 'hello'},
+      {name: 'item two', id: 'bar', url: 'http://bar'},
+    ];
+    element.disabledIds = [];
+    await element.updateComplete;
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ ` <div class="container">
+        <gr-button
+          aria-disabled="false"
+          class="dropdown-trigger"
+          id="trigger"
+          role="button"
+          tabindex="0"
+        >
+          <slot> </slot>
+        </gr-button>
+        <md-menu
+          anchor="trigger"
+          aria-hidden="true"
+          default-focus="none"
+          id="dropdown"
+          quick=""
+          tabindex="-1"
+        >
+          <div class="dropdown-content">
+            <gr-tooltip-content has-tooltip="" title="hello">
+              <span class="itemAction" data-id="foo" tabindex="-1">
+                <md-menu-item
+                  active=""
+                  data-index="0"
+                  md-menu-item=""
+                  selected=""
+                >
+                  item one
+                </md-menu-item>
+              </span>
+            </gr-tooltip-content>
+            <md-divider role="separator" tabindex="-1"> </md-divider>
+            <gr-tooltip-content>
+              <a class="itemAction" href="http://bar" tabindex="-1">
+                <md-menu-item data-index="1" md-menu-item="">
+                  item two
+                </md-menu-item>
+              </a>
+            </gr-tooltip-content>
+          </div>
+        </md-menu>
+      </div>`
+    );
+  });
+
   test('tap on trigger opens menu, then closes', () => {
-    sinon.stub(element, 'open').callsFake(() => {
+    sinon.stub(element, 'dropdownTriggerTapHandler').callsFake(() => {
       assertIsDefined(element.dropdown);
-      element.dropdown.open();
-    });
-    sinon.stub(element, 'close').callsFake(() => {
-      assertIsDefined(element.dropdown);
-      element.dropdown.close();
+      element.dropdown.open = !element.dropdown.open;
     });
     assertIsDefined(element.dropdown);
     assertIsDefined(element.trigger);
-    assert.isFalse(element.dropdown.opened);
+    assert.isFalse(element.dropdown.open);
     element.trigger.click();
-    assert.isTrue(element.dropdown.opened);
+    assert.isTrue(element.dropdown.open);
     element.trigger.click();
-    assert.isFalse(element.dropdown.opened);
+    assert.isFalse(element.dropdown.open);
   });
 
-  test('_computeURLHelper', () => {
+  test('computeURLHelper', () => {
     const path = '/test';
     const host = 'http://www.testsite.com';
-    const computedPath = element._computeURLHelper(host, path);
+    const computedPath = element.computeURLHelper(host, path);
     assert.equal(computedPath, '//http://www.testsite.com/test');
   });
 
@@ -60,7 +111,7 @@
     );
     assert.equal(
       element.computeLinkURL({url: '/test', target: '_blank'}),
-      '/test'
+      '//' + window.location.host + '/test'
     );
   });
 
@@ -103,7 +154,7 @@
     element.addEventListener('tap-item-foo', fooTapped);
     element.addEventListener('tap-item', tapped);
     await element.updateComplete;
-    queryAndAssert<HTMLSpanElement>(element, '.itemAction').click();
+    queryAndAssert<MdMenuItem>(element, '.itemAction').click();
     assert.isTrue(fooTapped.called);
     assert.isTrue(tapped.called);
     assert.deepEqual(tapped.lastCall.args[0].detail, item0);
@@ -118,7 +169,7 @@
     element.addEventListener('tap-item-foo', stub);
     element.addEventListener('tap-item', tapped);
     await element.updateComplete;
-    queryAndAssert<HTMLSpanElement>(element, '.itemAction').click();
+    queryAndAssert<MdMenuItem>(element, '.itemAction').click();
     assert.isFalse(stub.called);
     assert.isFalse(tapped.called);
   });
@@ -132,7 +183,7 @@
     await element.updateComplete;
     const tooltipContents = queryAll<GrTooltipContent>(
       element,
-      'iron-dropdown li gr-tooltip-content'
+      'md-menu gr-tooltip-content'
     );
     assert.equal(tooltipContents.length, 2);
     assert.isTrue(tooltipContents[0].hasTooltip);
@@ -140,87 +191,6 @@
     assert.isFalse(tooltipContents[1].hasTooltip);
   });
 
-  test('render', async () => {
-    element.items = [
-      {name: 'item one', id: 'foo', tooltip: 'hello'},
-      {name: 'item two', id: 'bar', url: 'http://bar'},
-    ];
-    element.disabledIds = [];
-    await element.updateComplete;
-    assert.shadowDom.equal(
-      element,
-      /* HTML */ `
-      <gr-button
-        aria-disabled="false"
-        class="dropdown-trigger"
-        id="trigger"
-        role="button"
-        tabindex="0"
-      >
-        <slot>
-        </slot>
-      </gr-button>
-      <iron-dropdown
-        allowoutsidescroll=""
-        aria-disabled="false"
-        aria-hidden="true"
-        horizontal-align="left"
-        id="dropdown"
-        style="outline: none; display: none;"
-        vertical-align="top"
-      >
-        <div
-          class="dropdown-content"
-          slot="dropdown-content"
-        >
-          <ul>
-            <li tabindex="-1">
-              <gr-tooltip-content
-                has-tooltip=""
-                title="hello"
-              >
-                <span
-                  class="itemAction"
-                  data-id="foo"
-                  tabindex="-1"
-                >
-                  item one
-                </span>
-                <a
-                  class="itemAction"
-                  hidden=""
-                  href=""
-                  tabindex="-1"
-                >
-                  item one
-                </a>
-              </gr-tooltip-content>
-            </li>
-            <li tabindex="-1">
-              <gr-tooltip-content>
-                <span
-                  class="itemAction"
-                  data-id="bar"
-                  hidden=""
-                  tabindex="-1"
-                >
-                  item two
-                </span>
-                <a
-                  class="itemAction"
-                  href="http://bar"
-                  tabindex="-1"
-                >
-                  item two
-                </a>
-              </gr-tooltip-content>
-            </li>
-        </div>
-          </ul>
-      </iron-dropdown>`
-    );
-  });
-
   suite('keyboard navigation', () => {
     setup(async () => {
       element.items = [
@@ -230,23 +200,33 @@
       await element.updateComplete;
     });
 
-    test('down', () => {
+    test('down', async () => {
       const stub = sinon.stub(element.cursor, 'next');
       assertIsDefined(element.dropdown);
-      assert.isFalse(element.dropdown.opened);
-      pressKey(element, 'ArrowDown');
-      assert.isTrue(element.dropdown.opened);
-      pressKey(element, 'ArrowDown');
+      assert.isFalse(element.dropdown.open);
+      pressKey(element!.shadowRoot!.querySelector('#trigger')!, 'ArrowDown');
+      await element.updateComplete;
+      assert.isTrue(element.dropdown.open);
+      pressKey(
+        element!.shadowRoot!.querySelectorAll('md-menu-item')[0],
+        'ArrowDown'
+      );
+      await element.updateComplete;
       assert.isTrue(stub.called);
     });
 
-    test('up', () => {
+    test('up', async () => {
       assertIsDefined(element.dropdown);
       const stub = sinon.stub(element.cursor, 'previous');
-      assert.isFalse(element.dropdown.opened);
-      pressKey(element, 'ArrowUp');
-      assert.isTrue(element.dropdown.opened);
-      pressKey(element, 'ArrowUp');
+      assert.isFalse(element.dropdown.open);
+      pressKey(element!.shadowRoot!.querySelector('#trigger')!, 'ArrowUp');
+      await element.updateComplete;
+      assert.isTrue(element.dropdown.open);
+      pressKey(
+        element!.shadowRoot!.querySelectorAll('md-menu-item')[0],
+        'ArrowUp'
+      );
+      await element.updateComplete;
       assert.isTrue(stub.called);
     });
 
@@ -254,17 +234,21 @@
       assertIsDefined(element.dropdown);
       // Because enter and space are handled by the same fn, we need only to
       // test one.
-      assert.isFalse(element.dropdown.opened);
-      pressKey(element, ' ');
+      assert.isFalse(element.dropdown.open);
+      pressKey(element!.shadowRoot!.querySelector('#trigger')!, ' ');
       await element.updateComplete;
-      assert.isTrue(element.dropdown.opened);
-
+      assert.isTrue(element.dropdown.open);
+      await element.updateComplete;
+      await waitUntil(() =>
+        element.shadowRoot?.querySelectorAll('md-menu-item')
+      );
       const el = queryAndAssert<HTMLAnchorElement>(
         element.cursor.target as HTMLElement,
-        ':not([hidden]) a'
+        ':not([hidden])'
       );
       const stub = sinon.stub(el, 'click');
-      pressKey(element, ' ');
+      pressKey(element!.shadowRoot!.querySelectorAll('md-menu-item')[0], ' ');
+      await element.updateComplete;
       assert.isTrue(stub.called);
     });
   });
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 ff702b6..d490423 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
@@ -3,7 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../gr-autogrow-textarea/gr-autogrow-textarea';
 import '../../../styles/shared-styles';
 import '../gr-button/gr-button';
 import '../gr-icon/gr-icon';
@@ -17,15 +17,13 @@
 import {getAppContext} from '../../../services/app-context';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {assertIsDefined} from '../../../utils/common-util';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import {Interaction} from '../../../constants/reporting';
-import {LitElement, html} from 'lit';
-import {customElement, property, state, query} from 'lit/decorators.js';
+import {html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {css} from 'lit';
 import {PropertyValues} from 'lit';
 import {
-  BindValueChangeEvent,
   EditableContentSaveEvent,
   ValueChangedEvent,
 } from '../../../types/events';
@@ -52,6 +50,7 @@
   FormattingError,
 } from '../../../utils/commit-message-formatter-util';
 import {modalStyles} from '../../../styles/gr-modal-styles';
+import {GrAutogrowTextarea} from '../gr-autogrow-textarea/gr-autogrow-textarea';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -76,8 +75,8 @@
  */
 @customElement('gr-editable-content')
 export class GrEditableContent extends LitElement {
-  @query('iron-autogrow-textarea')
-  private textarea?: IronAutogrowTextareaElement;
+  @query('gr-autogrow-textarea')
+  private textarea?: GrAutogrowTextarea;
 
   @query('#uploaderConfirmDialog')
   private readonly uploaderConfirmDialog?: HTMLDialogElement;
@@ -221,7 +220,7 @@
         :host {
           display: block;
         }
-        :host([disabled]) iron-autogrow-textarea {
+        :host([disabled]) gr-autogrow-textarea {
           opacity: 0.5;
         }
         .viewer {
@@ -236,15 +235,15 @@
           max-height: var(--collapsed-max-height, 300px);
           overflow: hidden;
         }
-        .editor iron-autogrow-textarea,
+        .editor gr-autogrow-textarea,
         .viewer {
           min-height: 100px;
         }
-        .editor iron-autogrow-textarea {
+        .editor gr-autogrow-textarea {
           background-color: var(--view-background-color);
           width: 100%;
           display: block;
-          --iron-autogrow-textarea_-_padding: var(--spacing-m);
+          --gr-autogrow-textarea-padding: var(--spacing-m);
         }
         .editButtons {
           display: flex;
@@ -338,14 +337,15 @@
     return html`
       <div class="editor">
         <div>
-          <iron-autogrow-textarea
+          <gr-autogrow-textarea
             autocomplete="on"
-            .bindValue=${this.newContent}
+            .value=${this.newContent}
             ?disabled=${this.disabled}
-            @bind-value-changed=${(e: BindValueChangeEvent) => {
-              this.newContent = e.detail.value ?? '';
+            @input=${(e: InputEvent) => {
+              const value = (e.target as GrAutogrowTextarea).value ?? '';
+              this.newContent = value;
             }}
-          ></iron-autogrow-textarea>
+          ></gr-autogrow-textarea>
         </div>
       </div>
     `;
@@ -402,7 +402,7 @@
               () => html` <div class="email-dropdown" id="editMessageEmailDropdown">Committer Email
             <gr-dropdown-list
                 .items=${this.getEmailDropdownItems()}
-                .value=${this.committerEmail}
+                .value=${this.committerEmail ?? ''}
                 @value-change=${this.setCommitterEmail}
             >
             </gr-dropdown-list>
@@ -488,7 +488,7 @@
   }
 
   focusTextarea() {
-    this.textarea?.textarea.focus();
+    this.textarea?.focus();
   }
 
   /**
@@ -693,7 +693,7 @@
   handleFormat(e: Event) {
     e.preventDefault();
 
-    const textarea = this.textarea?.textarea;
+    const textarea = this.textarea?.nativeElement;
     if (!textarea) return;
 
     // If we have lastFormattedContent, we're undoing
@@ -757,7 +757,7 @@
   }
 
   private getCurrentCursorLine(): number {
-    const textarea = this.textarea?.textarea;
+    const textarea = this.textarea?.nativeElement;
     if (!textarea) return -1;
 
     // Calculate which line the cursor is on
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 d45a34b..60cfc34 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
@@ -9,7 +9,7 @@
 import {GrEditableContent} from './gr-editable-content';
 import {query, queryAndAssert, waitUntil} from '../../../test/test-utils';
 import {GrButton} from '../gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {StorageService} from '../../../services/storage/gr-storage';
 import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
 import {testResolver} from '../../../test/common-test-setup';
@@ -22,8 +22,8 @@
   RevisionPatchSetNum,
 } from '../../../api/rest-api';
 import {changeViewModelToken} from '../../../models/views/change';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import {GrDialog} from '../gr-dialog/gr-dialog';
+import {GrAutogrowTextarea} from '../gr-autogrow-textarea/gr-autogrow-textarea';
 
 const emails = [
   {
@@ -317,8 +317,10 @@
 
   suite('format button', () => {
     let element: GrEditableContent;
+    let clock: sinon.SinonFakeTimers;
 
     setup(async () => {
+      clock = sinon.useFakeTimers();
       element = await fixture(
         html`<gr-editable-content></gr-editable-content>`
       );
@@ -426,10 +428,10 @@
       );
 
       element.newContent = 'line1\nline2    \nline3';
-      const textarea = queryAndAssert<IronAutogrowTextareaElement>(
+      const textarea = queryAndAssert<GrAutogrowTextarea>(
         element,
-        'iron-autogrow-textarea'
-      ).textarea;
+        'gr-autogrow-textarea'
+      ).nativeElement!;
 
       textarea.setSelectionRange(7, 7); // Position cursor after "line2"
       await element.updateComplete;
@@ -449,16 +451,20 @@
       );
 
       element.newContent = 'line1    \nline2    \nline3    ';
-      const textarea = queryAndAssert<IronAutogrowTextareaElement>(
+      const textarea = queryAndAssert<GrAutogrowTextarea>(
         element,
-        'iron-autogrow-textarea'
-      ).textarea;
+        'gr-autogrow-textarea'
+      ).nativeElement!;
 
       textarea.setSelectionRange(7, 7); // Position cursor after "line2"
       await element.updateComplete;
       element.updateFormatState(/* skipDebounce= */ false);
       await element.updateComplete;
+
+      // Advance past the debounce period (700ms)
+      clock.tick(700);
       await waitUntil(() => !formatButton?.disabled);
+      await element.updateComplete;
 
       // Format button should be enabled because other lines need formatting
       assert.isFalse(formatButton.disabled);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index da4d335..634a83d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -3,26 +3,26 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-dropdown/iron-dropdown';
-import '@polymer/paper-input/paper-input';
+
 import '../gr-button/gr-button';
 import '../gr-icon/gr-icon';
 import '../../shared/gr-autocomplete/gr-autocomplete';
-import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
 import {
   AutocompleteQuery,
   GrAutocomplete,
 } from '../gr-autocomplete/gr-autocomplete';
 import {Key} from '../../../utils/dom-util';
 import {queryAndAssert} from '../../../utils/common-util';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
-import {IronInputElement} from '@polymer/iron-input';
 import {ShortcutController} from '../../lit/shortcut-controller';
 import {ValueChangedEvent} from '../../../types/events';
 import {fire} from '../../../utils/event-util';
+import '@material/web/menu/menu';
+import {MdMenu} from '@material/web/menu/menu';
+import '@material/web/textfield/filled-text-field';
+import {MdFilledTextField} from '@material/web/textfield/filled-text-field';
 
 const AWAIT_MAX_ITERS = 10;
 const AWAIT_STEP = 5;
@@ -42,7 +42,7 @@
    */
 
   @query('#dropdown')
-  dropdown?: IronDropdownElement;
+  dropdown?: MdMenu;
 
   @property()
   labelText = '';
@@ -78,7 +78,7 @@
   query: AutocompleteQuery = () => Promise.resolve([]);
 
   @query('#input')
-  input?: PaperInputElement;
+  input?: MdFilledTextField;
 
   @query('#autocomplete')
   grAutocomplete?: GrAutocomplete;
@@ -110,6 +110,9 @@
         }
         #dropdown {
           box-shadow: var(--elevation-level-2);
+          --md-menu-container-color: var(--dialog-background-color);
+          --md-menu-top-space: 0px;
+          --md-menu-bottom-space: 0px;
         }
         .inputContainer {
           background-color: var(--dialog-background-color);
@@ -124,17 +127,30 @@
         .buttons gr-button {
           margin-left: var(--spacing-m);
         }
-        /* prettier formatter removes semi-colons after css mixins. */
-        /* prettier-ignore */
-        paper-input {
-          --paper-input-container: {
-            padding: 0;
-            min-width: 15em;
-          };
-          --paper-input-container-input: {
-            font-size: inherit;
-          };
-          --paper-input-container-focus-color: var(--link-color);
+        md-filled-text-field {
+          min-width: 15em;
+          --md-filled-text-field-container-color: rgba(0, 0, 0, 0);
+          --md-filled-text-field-focus-active-indicator-color: var(
+            --link-color
+          );
+          --md-filled-text-field-hover-state-layer-color: rgba(0, 0, 0, 0);
+          --md-filled-field-top-space: var(--spacing-m);
+          --md-filled-field-bottom-space: var(--spacing-m);
+          --md-filled-field-leading-space: 8px;
+          --md-filled-field-active-indicator-color: var(--link-color);
+          --md-filled-field-hover-active-indicator-color: var(--link-color);
+          --md-sys-color-primary: var(--primary-text-color);
+          --md-sys-color-on-surface: var(--primary-text-color);
+          --md-sys-color-on-surface-variant: var(--deemphasized-text-color);
+          --md-filled-text-field-label-text-color: var(
+            --deemphasized-text-color
+          );
+          --md-filled-text-field-focus-label-text-color: var(
+            --deemphasized-text-color
+          );
+          --md-filled-text-field-hover-label-text-color: var(
+            --deemphasized-text-color
+          );
         }
         gr-button gr-icon {
           color: inherit;
@@ -143,22 +159,27 @@
           --gr-button-padding: var(--spacing-s);
           --margin: calc(0px - var(--spacing-s));
         }
+        .dropdown-content {
+          width: max-content;
+        }
       `,
     ];
   }
 
   override render() {
     this.setAttribute('title', this.computeLabel());
-    return html`${this.renderActivateButton()}
-      <iron-dropdown
+    return html`<div style="position: relative;">
+      ${this.renderActivateButton()}
+      <md-menu
         id="dropdown"
-        .verticalAlign=${'auto'}
-        .horizontalAlign=${'auto'}
-        .allowOutsideScroll=${true}
-        .noCancelOnEscKey=${true}
-        .noCancelOnOutsideClick=${true}
+        anchor="button"
+        tabindex="-1"
+        .quick=${true}
+        @closing=${() => {
+          this.cancel();
+        }}
       >
-        <div class="dropdown-content" slot="dropdown-content">
+        <div class="dropdown-content">
           <div class="inputContainer" part="input-container">
             ${this.renderInputBox()}
             <div class="buttons">
@@ -169,12 +190,14 @@
             </div>
           </div>
         </div>
-      </iron-dropdown>`;
+      </md-menu>
+    </div>`;
   }
 
   private renderActivateButton() {
     if (this.showAsEditPencil) {
       return html`<gr-button
+        id="button"
         link=""
         class="pencil ${this.computeLabelClass()}"
         @click=${this.showDropdown}
@@ -186,6 +209,7 @@
       </gr-button>`;
     } else {
       return html`<label
+        id="button"
         class=${this.computeLabelClass()}
         title=${this.computeLabel()}
         aria-label=${this.computeLabel()}
@@ -200,6 +224,7 @@
     if (this.autocomplete) {
       return html`<gr-autocomplete
         .label=${this.labelText}
+        .placeholder=${this.placeholder}
         id="autocomplete"
         .text=${this.inputText}
         .query=${this.query}
@@ -210,12 +235,17 @@
       >
       </gr-autocomplete>`;
     } else {
-      return html`<paper-input
-        id="input"
-        .label=${this.labelText}
-        .maxlength=${this.maxLength}
-        .value=${this.inputText}
-      ></paper-input>`;
+      return html`
+        <md-filled-text-field
+          id="input"
+          .label=${this.labelText}
+          .placeholder=${this.placeholder}
+          .maxlength=${this.maxLength}
+          .value=${this.inputText}
+          aria-label=${this.labelText || this.placeholder || nothing}
+        >
+        </md-filled-text-field>
+      `;
     }
   }
 
@@ -267,7 +297,7 @@
   }
 
   private openDropdown() {
-    this.dropdown?.open();
+    this.dropdown?.show();
     this.inputText = this.value || '';
     this.editing = true;
 
@@ -322,8 +352,7 @@
     if (this.autocomplete) {
       return this.grAutocomplete!.nativeInput;
     } else {
-      return (this.input!.inputElement as IronInputElement)
-        .inputElement as HTMLInputElement;
+      return this.input!.shadowRoot!.querySelector('input')!;
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
index 6d01e00..3108166 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
@@ -8,17 +8,14 @@
 import './gr-editable-label';
 import {GrEditableLabel} from './gr-editable-label';
 import {queryAndAssert} from '../../../utils/common-util';
-import {PaperInputElement} from '@polymer/paper-input/paper-input';
 import {GrButton} from '../gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
-import {IronDropdownElement} from '@polymer/iron-dropdown';
-import {
-  AutocompleteSuggestion,
-  GrAutocomplete,
-} from '../gr-autocomplete/gr-autocomplete';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrAutocomplete} from '../gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../../utils/autocomplete-util';
 import {Key} from '../../../utils/dom-util';
 import {pressKey, waitEventLoop, waitUntil} from '../../../test/test-utils';
-import {IronInputElement} from '@polymer/iron-input';
+import {MdMenu} from '@material/web/menu/menu';
+import {MdFilledTextField} from '@material/web/textfield/filled-text-field';
 
 suite('gr-editable-label tests', () => {
   let element: GrEditableLabel;
@@ -38,73 +35,83 @@
       <gr-editable-label value=""></gr-editable-label>
     `);
 
-    const paperInput = queryAndAssert<PaperInputElement>(element, '#input');
-    input = (paperInput.inputElement as IronInputElement)
-      .inputElement as HTMLInputElement;
+    const mdFilledTextField = queryAndAssert<MdFilledTextField>(
+      element,
+      '#input'
+    );
+    input = mdFilledTextField.shadowRoot!.querySelector('input')!;
   });
 
   test('renders', () => {
     assert.shadowDom.equal(
       element,
-      `<label
-      aria-label="value text"
-      class="editable"
-      part="label"
-      title="value text"
-    >
-      value text
-    </label>
-    <iron-dropdown
-      aria-disabled="false"
-      aria-hidden="true"
-      horizontal-align="auto"
-      id="dropdown"
-      style="outline: none; display: none;"
-      vertical-align="auto"
-    >
-      <div class="dropdown-content" slot="dropdown-content">
-        <div class="inputContainer" part="input-container">
-          <paper-input
-            aria-disabled="false"
-            id="input"
-            tabindex="0"
-          ></paper-input>
-          <div class="buttons">
-          <gr-button
-              aria-disabled="false"
-              id="saveBtn"
-              primary
-              role="button"
-              tabindex="0"
+      `<div style="position: relative;">
+        <label
+          aria-label="value text"
+          class="editable"
+          id="button"
+          part="label"
+          title="value text"
+        >
+          value text
+        </label>
+        <md-menu
+          anchor="button"
+          aria-hidden="true"
+          id="dropdown"
+          tabindex="-1"
+        >
+          <div class="dropdown-content">
+            <div
+              class="inputContainer"
+              part="input-container"
             >
-              Save
-            </gr-button>
-            <gr-button
-              aria-disabled="false"
-              id="cancelBtn"
-              role="button"
-              tabindex="0"
-            >
-              Cancel
-            </gr-button>
+              <md-filled-text-field
+                autocomplete=""
+                data-aria-label="label text"
+                id="input"
+                inputmode=""
+                placeholder="label text"
+                type="text"
+              >
+              </md-filled-text-field>
+              <div class="buttons">
+                <gr-button
+                  aria-disabled="false"
+                  id="saveBtn"
+                  primary=""
+                  role="button"
+                  tabindex="0"
+                >
+                  Save
+                </gr-button>
+                <gr-button
+                  aria-disabled="false"
+                  id="cancelBtn"
+                  role="button"
+                  tabindex="0"
+                >
+                  Cancel
+                </gr-button>
+              </div>
+            </div>
           </div>
-        </div>
-      </div>
-    </iron-dropdown>`
+        </md-menu>
+      </div>`
     );
   });
 
   test('element render', async () => {
     // The dropdown is closed and the label is visible:
-    const dropdown = queryAndAssert<IronDropdownElement>(element, '#dropdown');
-    assert.isFalse(dropdown.opened);
+    const dropdown = queryAndAssert<MdMenu>(element, '#dropdown');
+    assert.isFalse(dropdown.open);
     assert.isTrue(label.classList.contains('editable'));
     assert.equal(label.textContent, 'value text');
 
     label.click();
     await element.updateComplete;
     // The dropdown is open (which covers up the label):
-    assert.isTrue(dropdown.opened);
+    assert.isTrue(dropdown.open);
     assert.equal(input.value, 'value text');
   });
 
@@ -224,17 +231,14 @@
 
     test('disallows edit when read-only', async () => {
       // The dropdown is closed.
-      const dropdown = queryAndAssert<IronDropdownElement>(
-        element,
-        '#dropdown'
-      );
-      assert.isFalse(dropdown.opened);
+      const dropdown = queryAndAssert<MdMenu>(element, '#dropdown');
+      assert.isFalse(dropdown.open);
       label.click();
 
       await element.updateComplete;
 
       // The dropdown is still closed.
-      assert.isFalse(dropdown.opened);
+      assert.isFalse(dropdown.open);
     });
 
     test('label is not marked as editable', () => {
@@ -261,6 +265,7 @@
       `);
 
       autocomplete = element.grAutocomplete!;
+      autocomplete.debounceWait = 10;
     });
 
     test('autocomplete suggestions shown esc closes suggestions', async () => {
@@ -272,7 +277,7 @@
       pressKey(autocomplete.input!, Key.ESC);
 
       await waitUntil(() => autocomplete.suggestionsDropdown!.isHidden);
-      assert.isTrue(element.dropdown?.opened);
+      assert.isTrue(element.dropdown?.open);
     });
 
     test('autocomplete suggestions closed esc closes dialogue', async () => {
@@ -288,7 +293,7 @@
       await element.updateComplete;
       // Dialogue is closed, save not triggered.
       assert.isTrue(autocomplete.suggestionsDropdown?.isHidden);
-      assert.isFalse(element.dropdown?.opened);
+      assert.isFalse(element.dropdown?.open);
       assert.isFalse(labelSaved);
     });
 
@@ -309,7 +314,7 @@
       // is shown, save has not been triggered.
       assert.strictEqual(element.inputText, 'value text 1');
       assert.isTrue(autocomplete.suggestionsDropdown?.isHidden);
-      assert.isTrue(element.dropdown?.opened);
+      assert.isTrue(element.dropdown?.open);
       assert.isFalse(labelSaved);
     });
 
@@ -331,7 +336,7 @@
       await element.updateComplete;
       // Dialogue is closed, save triggered.
       assert.isTrue(autocomplete.suggestionsDropdown?.isHidden);
-      assert.isFalse(element.dropdown?.opened);
+      assert.isFalse(element.dropdown?.open);
       assert.isTrue(labelSaved);
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts
index 943f4de..8a2e581 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {FileInfoStatus} from '../../../constants/constants';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {assertNever} from '../../../utils/common-util';
 import '../gr-tooltip-content/gr-tooltip-content';
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts
index 555b237..3577b8b 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status/gr-file-status_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-file-status';
 import {GrFileStatus} from './gr-file-status';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {FileInfoStatus} from '../../../api/rest-api';
 
 suite('gr-file-status tests', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
index c25af83..7c8bcf4 100644
--- a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -8,7 +8,7 @@
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 import {css, html, LitElement, PropertyValues} from 'lit';
-import {customElement, state, query, property} from 'lit/decorators.js';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {getDocUrl} from '../../../utils/url-util';
 import {subscribe} from '../../lit/subscription-controller';
@@ -16,17 +16,22 @@
 import {configModelToken} from '../../../models/config/config-model';
 import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 import {changeModelToken} from '../../../models/change/change-model';
-import {Comment, PatchSetNumber} from '../../../types/common';
+import {Comment, isDraft, PatchSetNumber} from '../../../types/common';
 import {OpenFixPreviewEventDetail} from '../../../types/events';
 import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
 import {SuggestionsProvider} from '../../../api/suggestions';
-import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
+import {id, PROVIDED_FIX_ID} from '../../../utils/comment-util';
+import {KnownExperimentId} from '../../../services/flags/flags';
 import {when} from 'lit/directives/when.js';
 import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
 import {getAppContext} from '../../../services/app-context';
 import {Interaction} from '../../../constants/reporting';
 import {ChangeStatus, FixSuggestionInfo} from '../../../api/rest-api';
+import {stringToReplacements} from '../../../utils/comment-util';
 import {ReportSource} from '../../../services/suggestions/suggestions-service';
+import {getFileExtension} from '../../../utils/file-util';
+import '@material/web/checkbox/checkbox';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 export const COLLAPSE_SUGGESTION_STORAGE_KEY = 'collapseSuggestionStorageKey';
 
@@ -50,6 +55,8 @@
 
   @state() private applyingFix = false;
 
+  @state() private isEditingSuggestion = false;
+
   @state() latestPatchNum?: PatchSetNumber;
 
   @state()
@@ -67,6 +74,10 @@
 
   @state() isChangeAbandoned = false;
 
+  @state() private thumbUpSelected = false;
+
+  @state() private thumbDownSelected = false;
+
   private readonly getConfigModel = resolve(this, configModelToken);
 
   private readonly getChangeModel = resolve(this, changeModelToken);
@@ -77,6 +88,8 @@
 
   private readonly reporting = getAppContext().reportingService;
 
+  private readonly flagsService = getAppContext().flagsService;
+
   @state() private previewLoaded = false;
 
   constructor() {
@@ -132,6 +145,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       css`
         :host {
           display: block;
@@ -143,7 +157,7 @@
           margin-left: var(--spacing-s);
         }
         /* just for a11y */
-        input.show-hide {
+        md-checkbox.show-hide {
           display: none;
         }
         label.show-hide {
@@ -165,6 +179,8 @@
         }
         .header .title {
           flex: 1;
+          display: flex;
+          align-items: center;
         }
         .headerMiddle {
           display: flex;
@@ -173,6 +189,17 @@
         .copyButton {
           margin-right: var(--spacing-l);
         }
+        .feedback-button[aria-label='Thumb up'] {
+          margin-left: var(--spacing-l);
+          margin-right: 0;
+        }
+        .feedback-button[aria-label='Thumb down'] {
+          margin-left: 0;
+        }
+        .selected {
+          color: var(--selected-foreground);
+          background-color: var(--selected-background);
+        }
       `,
     ];
   }
@@ -180,6 +207,9 @@
   override render() {
     const fix_suggestions = this.getFixSuggestions();
     if (!fix_suggestions) return;
+    const editableSuggestionEnabled = this.flagsService.isEnabled(
+      KnownExperimentId.ML_SUGGESTED_EDIT_EDITABLE_SUGGESTION
+    );
     return html`<div class="header">
         <div class="title">
           <span
@@ -198,11 +228,44 @@
                 name="suggestion"
                 .value=${fix_suggestions}
               ></gr-endpoint-param
-              ><gr-icon
-                icon="help"
-                title="read documentation"
-              ></gr-icon></gr-endpoint-decorator
-          ></a>
+              ><gr-icon icon="help" title="read documentation"></gr-icon
+            ></gr-endpoint-decorator>
+          </a>
+          ${when(
+            this.flagsService.isEnabled(
+              KnownExperimentId.ML_SUGGESTED_EDIT_FEEDBACK
+            ),
+            () => html`
+              <gr-button
+                secondary
+                flatten
+                class="action feedback-button ${this.thumbUpSelected
+                  ? 'selected'
+                  : ''}"
+                aria-label="Thumb up"
+                @click=${this.handleThumbUpClick}
+              >
+                <gr-icon
+                  icon="thumb_up"
+                  ?filled=${this.thumbUpSelected}
+                ></gr-icon>
+              </gr-button>
+              <gr-button
+                secondary
+                flatten
+                class="action feedback-button ${this.thumbDownSelected
+                  ? 'selected'
+                  : ''}"
+                aria-label="Thumb down"
+                @click=${this.handleThumbDownClick}
+              >
+                <gr-icon
+                  icon="thumb_down"
+                  ?filled=${this.thumbDownSelected}
+                ></gr-icon>
+              </gr-button>
+            `
+          )}
         </div>
         <div class="headerMiddle">
           <gr-button
@@ -214,19 +277,23 @@
             Show Edit
           </gr-button>
           ${when(
-            this.isOwner && !this.collapsed,
-            () =>
-              html`<gr-button
-                secondary
-                flatten
-                .loading=${this.applyingFix}
-                .disabled=${this.isApplyEditDisabled()}
-                class="action show-fix"
-                @click=${this.handleApplyFix}
-                .title=${this.computeApplyEditTooltip()}
-              >
-                Apply Edit
-              </gr-button>`
+            !this.collapsed &&
+              editableSuggestionEnabled &&
+              this.isEditingSuggestion,
+            () => this.renderEditingButtons()
+          )}
+          ${when(
+            !this.collapsed &&
+              editableSuggestionEnabled &&
+              !this.isEditingSuggestion &&
+              isDraft(this.comment),
+            () => this.renderChangeEditButton()
+          )}
+          ${when(
+            this.isOwner &&
+              !this.collapsed &&
+              !(editableSuggestionEnabled && this.isEditingSuggestion),
+            () => this.renderApplyEditButton()
           )}
           ${this.renderToggle()}
         </div>
@@ -234,21 +301,71 @@
       <gr-suggestion-diff-preview
         .fixSuggestionInfo=${this.getFixSuggestions()?.[0]}
         .patchSet=${this.comment?.patch_set}
-        .commentId=${this.comment?.id}
+        .editable=${editableSuggestionEnabled && this.isEditingSuggestion}
+        .commentId=${id(this.comment!)}
         @preview-loaded=${() => (this.previewLoaded = true)}
       ></gr-suggestion-diff-preview>`;
   }
 
+  private renderChangeEditButton() {
+    return html`<gr-button
+      secondary
+      flatten
+      class="action edit-fix"
+      @click=${this.handleEditFix}
+    >
+      Change Edit
+    </gr-button>`;
+  }
+
+  private renderEditingButtons() {
+    return html`<gr-button
+        secondary
+        flatten
+        class="action cancel-fix"
+        @click=${this.handleCancelFix}
+      >
+        Cancel </gr-button
+      ><gr-button
+        secondary
+        flatten
+        class="action reset-fix"
+        @click=${this.handleResetFix}
+      >
+        Reset </gr-button
+      ><gr-button
+        secondary
+        flatten
+        class="action save-fix"
+        @click=${this.handleSaveFix}
+      >
+        Save
+      </gr-button>`;
+  }
+
+  private renderApplyEditButton() {
+    return html`<gr-button
+      secondary
+      flatten
+      .loading=${this.applyingFix}
+      .disabled=${this.isApplyEditDisabled()}
+      class="action apply-fix"
+      @click=${this.handleApplyFix}
+      .title=${this.computeApplyEditTooltip()}
+    >
+      Apply Edit
+    </gr-button>`;
+  }
+
   private renderToggle() {
     const icon = this.collapsed ? 'expand_more' : 'expand_less';
     const ariaLabel = this.collapsed ? 'Expand' : 'Collapse';
     return html`
       <div class="show-hide" tabindex="0">
         <label class="show-hide" aria-label=${ariaLabel}>
-          <input
-            type="checkbox"
+          <md-checkbox
             class="show-hide"
-            .checked=${this.collapsed}
+            ?checked=${this.collapsed}
             @change=${() => {
               this.collapsed = !this.collapsed;
               if (this.collapsed) {
@@ -267,7 +384,7 @@
                 );
               }
             }}
-          />
+          ></md-checkbox>
           <gr-icon icon=${icon} id="icon"></gr-icon>
         </label>
       </div>
@@ -308,6 +425,42 @@
     fire(this, 'open-fix-preview', eventDetail);
   }
 
+  handleCancelFix() {
+    this.isEditingSuggestion = false;
+    this.suggestionDiffPreview?.reset();
+  }
+
+  handleEditFix() {
+    this.isEditingSuggestion = true;
+  }
+
+  handleResetFix() {
+    this.suggestionDiffPreview?.reset();
+  }
+
+  handleSaveFix() {
+    this.isEditingSuggestion = false;
+
+    const newContent = this.suggestionDiffPreview?.getEditedContent();
+    if (newContent === undefined) return;
+
+    const suggestions = this.getFixSuggestions();
+    if (!suggestions || !suggestions.length) return;
+
+    const newReplacements = stringToReplacements(newContent);
+    const newSuggestions = [
+      {
+        ...suggestions[0],
+        replacements: newReplacements,
+      },
+      ...suggestions.slice(1),
+    ];
+    if (this.comment) {
+      this.comment.fix_suggestions = newSuggestions;
+    }
+    fire(this, 'generated-suggestion-changed', {suggestions: newSuggestions});
+  }
+
   async handleApplyFix() {
     if (!this.getFixSuggestions()) return;
     this.applyingFix = true;
@@ -318,6 +471,39 @@
     }
   }
 
+  private handleThumbUpClick() {
+    this.thumbUpSelected = !this.thumbUpSelected;
+    if (this.thumbUpSelected) {
+      this.thumbDownSelected = false;
+    }
+    this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_THUMB_UP, {
+      commentId: id(this.comment!),
+      fileExtension: getFileExtension(this.comment?.path ?? ''),
+      uuid: this.getFixSuggestions()?.[0]?.fix_id,
+      replacement: this.getFixSuggestions()?.[0]
+        ?.replacements?.map(r => r.replacement)
+        .join('\n'),
+    });
+  }
+
+  private handleThumbDownClick() {
+    this.thumbDownSelected = !this.thumbDownSelected;
+    if (this.thumbDownSelected) {
+      this.thumbUpSelected = false;
+    }
+    this.reporting.reportInteraction(
+      Interaction.GENERATE_SUGGESTION_THUMB_DOWN,
+      {
+        commentId: id(this.comment!),
+        fileExtension: getFileExtension(this.comment?.path ?? ''),
+        uuid: this.getFixSuggestions()?.[0]?.fix_id,
+        replacement: this.getFixSuggestions()?.[0]
+          ?.replacements?.map(r => r.replacement)
+          .join('\n'),
+      }
+    );
+  }
+
   private isApplyEditDisabled() {
     if (this.comment?.patch_set === undefined) return true;
     if (this.isChangeMerged) return true;
@@ -345,4 +531,9 @@
   interface HTMLElementTagNameMap {
     'gr-fix-suggestions': GrFixSuggestions;
   }
+  interface HTMLElementEventMap {
+    'generated-suggestion-changed': CustomEvent<{
+      suggestions: FixSuggestionInfo[];
+    }>;
+  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index fa2a4ff..3259511 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -11,7 +11,6 @@
   sanitizeHtmlToFragment,
 } from '../../../utils/inner-html-util';
 import {unescapeHTML} from '../../../utils/syntax-util';
-import '@polymer/marked-element';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
@@ -24,6 +23,12 @@
   USER_SUGGESTION_INFO_STRING,
 } from '../../../utils/comment-util';
 import {sameOrigin} from '../../../utils/url-util';
+import '../gr-marked-element/gr-marked-element';
+
+// MIME types for images we allow showing. Do not include SVG, it can contain
+// arbitrary JavaScript.
+const IMAGE_MIME_PATTERN =
+  /^data:image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp);base64,/;
 
 /**
  * This element optionally renders markdown and also applies some regex
@@ -48,6 +53,9 @@
   @state()
   MARKDOWN_LIMIT = 100000;
 
+  @state()
+  private allowMarkdownBase64ImagesInComments = false;
+
   /**
    * Note: Do not use sharedStyles or other styles here that should not affect
    * the generated HTML of the markdown.
@@ -131,6 +139,56 @@
           link: '$1',
           enabled: true,
         };
+
+        // List of common TLDs to specifically match for schemeless URLs.
+        const TLD_REGEX = [
+          'com',
+          'org',
+          'net',
+          'edu',
+          'gov',
+          'co',
+          'jp',
+          'de',
+          'uk',
+          'fr',
+          'us',
+          'io',
+        ].join('|');
+
+        // Linkify schemeless URLs with proper domain structures.
+        this.repoCommentLinks['ALWAYS_LINK_SCHEMELESS'] = {
+          // (?<=\s|^|[('":[])   // Ensure the match is preceded by whitespace,
+          //                     // start of line, or one of ( ' " : [
+          // (                   // Start capture group 1
+          //   (?:               // Start non-capturing domain group
+          //     [\w-]+\.        //   Sequence of words/hyphens with dot, e.g. "a-b."
+          //   )+                // End domain group. Require at least one match
+          //   (?:${TLD_REGEX})  // Ensure the match ends with a common TLD
+          //   (?=.*?/)          // Positive lookahead to ensure a '/' exists in the path/query/fragment
+          //   (?:               // Start non-capturing path/query/fragment group
+          //     [/?#]           //   Start with one of / ? #
+          //     [^\s'"]*        //   Followed by some chars that are not whitespace,
+          //                     //   ' or " (to not grab trailing quotes)
+          //     ?<![)'"!?.,]+   // and not ending with one or more of ) ' " ! ? . ,
+          //   )                 // End path/query/fragment group
+          // )                   // End capture group 1
+          // (?=\s|$|[)'"!?.,])  // Ensure the match is followed by whitespace,
+          //                     // end of line, or one of ) ' " ! ? . ,
+          match: `(?<=\\s|^|[('":[])((?:[\\w-]+\\.)+(?:${TLD_REGEX})(?=.*?/)(?:[/?#][^\\s'"]*(?<![)'"!?.,]+)))(?=\\s|$|[)'"!?.,])`,
+          // Prepend http:// for the link href otherwise it will be treated as
+          // a relative URL.
+          link: 'http://$1',
+          enabled: true,
+        };
+      }
+    );
+
+    subscribe(
+      this,
+      () => this.getConfigModel().allowMarkdownBase64ImagesInComments$,
+      allow => {
+        this.allowMarkdownBase64ImagesInComments = allow;
       }
     );
   }
@@ -184,11 +242,14 @@
       return `<p>${linkedText}</p>`;
     };
 
-    // We are overriding some marked-element renderers for a few reasons:
+    const allowMarkdownBase64ImagesInComments =
+      this.allowMarkdownBase64ImagesInComments;
+
+    // We are overriding some gr-marked-element renderers for a few reasons:
     // 1. Disable inline images as a design/policy choice.
     // 2. Inline code blocks ("codespan") do not unescape HTML characters when
     //    rendering without <pre> and so we must do this manually.
-    //    <marked-element> is already escaping these internally. See test
+    //    <gr-marked-element> is already escaping these internally. See test
     //    covering this.
     // 3. Multiline code blocks ("code") is similarly handling escaped
     //    characters using <pre>. The convention is to only use <pre> for multi-
@@ -216,8 +277,19 @@
           >${text}</a
         >`;
       };
-      renderer['image'] = (href: string, _title: string, text: string) =>
-        `![${text}](${href})`;
+      renderer['image'] = (href: string, title: string, text: string) => {
+        // Check if this is a base64-encoded image
+        if (
+          allowMarkdownBase64ImagesInComments &&
+          IMAGE_MIME_PATTERN.test(href)
+        ) {
+          return `<img src="${href}" alt="${text}" ${
+            title ? `title="${title}"` : ''
+          } />`;
+        }
+        // For non-base64 images just return the markdown
+        return `![${text}](${href})`;
+      };
       renderer['codespan'] = (text: string) =>
         `<code>${unescapeHTML(text)}</code>`;
       renderer['code'] = (text: string, infostring: string) => {
@@ -233,7 +305,7 @@
           return `<pre><code>${text}</code></pre>`;
         }
       };
-      // <marked-element> internals will be in charge of calling our custom
+      // <gr-marked-element> internals will be in charge of calling our custom
       // renderer so we write these functions separately so that 'this' is
       // preserved via closure.
       renderer['paragraph'] = boundRewriteAsterisks;
@@ -242,10 +314,10 @@
 
     // The child with slot is optional but allows us control over the styling.
     // The `callback` property lets us do a final sanitization of the output
-    // HTML string before it is rendered by `<marked-element>` in case any
+    // HTML string before it is rendered by `<gr-marked-element>` in case any
     // rewrites have been abused to attempt an XSS attack.
     return html`
-      <marked-element
+      <gr-marked-element
         .markdown=${this.escapeAllButBlockQuotes(this.content)}
         .breaks=${true}
         .renderer=${customRenderer}
@@ -253,7 +325,7 @@
           sanitizeHtml(contents)}
       >
         <div class="markdown-html" slot="markdown-html"></div>
-      </marked-element>
+      </gr-marked-element>
     `;
   }
 
@@ -278,10 +350,21 @@
   }
 
   override updated() {
+    this.removeEventListener(
+      'marked-render-complete',
+      this.markedRenderComplete
+    );
+    // When masked-element was using Polymer, it was rendered synchronously,
+    // compared to the lit version of the element. updated() ran before the markdown
+    // was inserted into the slot.
+    this.addEventListener('marked-render-complete', this.markedRenderComplete);
+  }
+
+  readonly markedRenderComplete = () => {
     // Look for @mentions and replace them with an account-label chip.
     this.convertEmailsToAccountChips();
     this.convertCodeToSuggestions();
-  }
+  };
 
   private convertEmailsToAccountChips() {
     for (const emailLink of this.renderRoot.querySelectorAll(
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 723267e..ad76bfc 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -16,7 +16,11 @@
 import './gr-formatted-text';
 import {GrFormattedText} from './gr-formatted-text';
 import {createComment, createConfig} from '../../../test/test-data-generators';
-import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
+import {
+  query,
+  queryAndAssert,
+  waitUntilObserved,
+} from '../../../test/test-utils';
 import {CommentLinks, EmailAddress} from '../../../api/rest-api';
 import {testResolver} from '../../../test/common-test-setup';
 import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
@@ -236,12 +240,37 @@
     });
 
     test('does default linking', async () => {
-      const checkLinking = async (url: string) => {
+      const checkLinking = async (url: string, expectLinkified = true) => {
         element.content = url;
         await element.updateComplete;
-        const a = queryAndAssert<HTMLElement>(element, 'a');
-        assert.equal(a.getAttribute('href'), url);
-        assert.equal(a.innerText, url);
+        const a = query<HTMLElement>(element, 'a');
+
+        if (expectLinkified) {
+          assert.isDefined<HTMLElement | undefined>(a);
+          // URLs without scheme are upgraded to https:// by the
+          // ALWAYS_LINK_SCHEMELESS rule. URLs with http:// or https://
+          // are preserved by the ALWAYS_LINK_HTTP rule.
+          const isSchemeless =
+            !url.startsWith('http://') &&
+            !url.startsWith('https://') &&
+            !url.startsWith('mailto:') &&
+            !url.startsWith('/');
+          const expectedHref = isSchemeless ? `http://${url}` : url;
+
+          assert.equal(a.getAttribute('href'), expectedHref);
+          assert.equal(a.innerText, url);
+        } else {
+          assert.isUndefined(a);
+        }
+      };
+
+      const getLinkifiedUrl = async (url: string) => {
+        element.content = url;
+        await element.updateComplete;
+        const a = query<HTMLElement>(element, 'a');
+
+        assert.isDefined<HTMLElement | undefined>(a);
+        return a.innerText;
       };
 
       await checkLinking('http://www.google.com');
@@ -249,11 +278,45 @@
       await checkLinking('https://www.google.com/');
       await checkLinking('https://www.google.com/asdf~');
       await checkLinking('https://www.google.com/asdf-');
-      await checkLinking('https://www.google.com/asdf-');
+      // We DO NOT want to linkify URLs ending with `)`, even though the URL
+      // spec does allow it. Links ending with `)` are extremely rare, and
+      // users putting links into parenthesis happens extremely often. :-)
+      assert.equal(
+        await getLinkifiedUrl('https://www.google.com/asdf)'),
+        'https://www.google.com/asdf'
+      );
+      assert.equal(
+        await getLinkifiedUrl('hello gerrit.com/asdf,'),
+        'gerrit.com/asdf'
+      );
+      assert.equal(
+        await getLinkifiedUrl('hello gerrit.com/asdf.),'),
+        'gerrit.com/asdf'
+      );
+      assert.equal(
+        await getLinkifiedUrl('hello gerrit.com/asdf),'),
+        'gerrit.com/asdf'
+      );
+      assert.equal(
+        await getLinkifiedUrl('hello gerrit.com/foo,bar'),
+        'gerrit.com/foo,bar'
+      );
       // matches & part as well, even we first linkify and then htmlEscape
       await checkLinking(
         'https://google.com/traces/list?project=gerrit&tid=123'
       );
+
+      await checkLinking('www.google.com/path');
+      await checkLinking('www.google-foo.com/path');
+      await checkLinking('google.co.uk/path?q=1#frag');
+
+      // Do not linkify URLs without `/`.
+      await checkLinking('google.com', false);
+      await checkLinking('com.google.gerrit.server.Event', false);
+
+      // Do not linkify URLs without a recognized TLD.
+      await checkLinking('google.foogle/path', false);
+      await checkLinking('google.com.blah/path', false);
     });
   });
 
@@ -274,7 +337,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>text</p>
                 <p>
@@ -309,7 +372,7 @@
                   </a>
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -376,7 +439,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <h1>h1-heading</h1>
                 <h2>h2-heading</h2>
@@ -405,7 +468,7 @@
                   </a>
                 </h1>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -421,7 +484,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>
                   <code>inline code</code>
@@ -433,7 +496,7 @@
                   <code>inline code with config link: LinkRewriteMe</code>
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -449,7 +512,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <pre>
               <code>multiline code</code>
@@ -461,7 +524,7 @@
               <code>multiline code with config link: LinkRewriteMe</code>
             </pre>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -475,11 +538,11 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>![img](google.com/img.png)</p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -493,13 +556,13 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>
                   <gr-account-chip></gr-account-chip>
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -521,7 +584,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>
                   <code>@</code>
@@ -534,7 +597,7 @@
                   </a>
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -555,7 +618,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>
                   <a
@@ -591,7 +654,7 @@
                   >
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -607,7 +670,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <blockquote>
                   <p>block quote</p>
@@ -637,7 +700,7 @@
                   </p>
                 </blockquote>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -656,7 +719,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>plain text ${escapedDiv}</p>
                 <p>
@@ -679,7 +742,7 @@
                   >
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -693,7 +756,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <blockquote>
                   <blockquote>
@@ -703,7 +766,7 @@
                   </blockquote>
                 </blockquote>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
@@ -724,7 +787,7 @@
         element,
         /* HTML */ `
           <gr-endpoint-decorator name="formatted-text-endpoint">
-            <marked-element>
+            <gr-marked-element>
               <div slot="markdown-html" class="markdown-html">
                 <p>
                   I think
@@ -736,20 +799,36 @@
                   >
                 </p>
               </div>
-            </marked-element>
+            </gr-marked-element>
           </gr-endpoint-decorator>
         `
       );
     });
 
     test('does default linking', async () => {
-      const checkLinking = async (url: string) => {
+      const checkLinking = async (url: string, expectLinkified = true) => {
         element.content = url;
         await element.updateComplete;
-        const a = queryAndAssert<HTMLElement>(element, 'a');
+        const a = query<HTMLElement>(element, 'a');
         const p = queryAndAssert<HTMLElement>(element, 'p');
-        assert.equal(a.getAttribute('href'), url);
         assert.equal(p.innerText, url);
+
+        if (expectLinkified) {
+          assert.isDefined<HTMLElement | undefined>(a);
+          // URLs without scheme are upgraded to https:// by the
+          // ALWAYS_LINK_SCHEMELESS rule. URLs with http:// or https://
+          // are preserved by the ALWAYS_LINK_HTTP rule.
+          const isSchemeless =
+            !url.startsWith('http://') &&
+            !url.startsWith('https://') &&
+            !url.startsWith('mailto:') &&
+            !url.startsWith('/');
+          const expectedHref = isSchemeless ? `http://${url}` : url;
+
+          assert.equal(a.getAttribute('href'), expectedHref);
+        } else {
+          assert.isUndefined(a);
+        }
       };
 
       await checkLinking('http://www.google.com');
@@ -759,6 +838,18 @@
       await checkLinking(
         'https://google.com/traces/list?project=gerrit&tid=123'
       );
+
+      await checkLinking('www.google.com/path');
+      await checkLinking('www.google-foo.com/path');
+      await checkLinking('google.co.uk/path?q=1#frag');
+
+      // Do not linkify URLs without `/`.
+      await checkLinking('google.com', false);
+      await checkLinking('com.google.gerrit.server.Event', false);
+
+      // Do not linkify URLs without a recognized TLD.
+      await checkLinking('google.foogle/path', false);
+      await checkLinking('google.com.blah/path', false);
     });
 
     suite('user suggest fix', () => {
@@ -775,11 +866,11 @@
           /* HTML */
           `
             <gr-endpoint-decorator name="formatted-text-endpoint">
-              <marked-element>
+              <gr-marked-element>
                 <div class="markdown-html" slot="markdown-html">
                   <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix>
                 </div>
-              </marked-element>
+              </gr-marked-element>
             </gr-endpoint-decorator>
           `
         );
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
index e11287b..b185ca2 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -19,8 +19,8 @@
 import {
   AccountInfo,
   ChangeInfo,
-  ServerInfo,
   ReviewInput,
+  ServerInfo,
 } from '../../../types/common';
 import {
   canHaveAttention,
@@ -43,8 +43,8 @@
 import {configModelToken} from '../../../models/config/config-model';
 import {createSearchUrl} from '../../../models/views/search';
 import {
-  DashboardType,
   createDashboardUrl,
+  DashboardType,
 } from '../../../models/views/dashboard';
 import {fire, fireReload} from '../../../utils/event-util';
 import {userModelToken} from '../../../models/user/user-model';
@@ -407,7 +407,7 @@
     if (this.account._account_id)
       return createDashboardUrl({
         type: DashboardType.USER,
-        user: `${this.account._account_id}`,
+        user: this.account._account_id,
       });
     if (this.account.email)
       return createDashboardUrl({
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
index 647282f..6f3fe36 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -5,7 +5,7 @@
  */
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-hovercard-account-contents';
 import {GrHovercardAccountContents} from './gr-hovercard-account-contents';
@@ -225,6 +225,7 @@
     stubRestApi('removeChangeReviewer').returns(
       Promise.resolve({...new Response(), ok: true})
     );
+    await element.updateComplete;
     const reloadListener = sinon.spy();
     element.addEventListener('reload', reloadListener);
     const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
@@ -250,6 +251,7 @@
     stubRestApi('removeChangeReviewer').returns(
       Promise.resolve({...new Response(), ok: true})
     );
+    await element.updateComplete;
     const reloadListener = sinon.spy();
     element.addEventListener('reload', reloadListener);
 
@@ -278,6 +280,7 @@
     stubRestApi('removeChangeReviewer').returns(
       Promise.resolve({...new Response(), ok: true})
     );
+    await element.updateComplete;
     const reloadListener = sinon.spy();
     element.addEventListener('reload', reloadListener);
 
@@ -303,6 +306,7 @@
     stubRestApi('removeChangeReviewer').returns(
       Promise.resolve({...new Response(), ok: true})
     );
+    await element.updateComplete;
     const reloadListener = sinon.spy();
     element.addEventListener('reload', reloadListener);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
index 40e4c75..1219a1c 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-hovercard-account';
 import {GrHovercardAccount} from './gr-hovercard-account';
diff --git a/polygerrit-ui/app/elements/shared/gr-html-renderer/gr-html-renderer.ts b/polygerrit-ui/app/elements/shared/gr-html-renderer/gr-html-renderer.ts
new file mode 100644
index 0000000..5ec3cea
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-html-renderer/gr-html-renderer.ts
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+import {sanitizeHtmlToFragment} from 'safevalues';
+
+/**
+ * A component for rendering HTML durectly.
+ * In Gerrit we reset css using sharedStyles meaning the components are not styled
+ * like the way the response should be.
+ * This component SHOULD NOT import sharedStyles.
+ */
+@customElement('gr-html-renderer')
+export class GrHtmlRenderer extends LitElement {
+  @property({type: String})
+  htmlContent?: string;
+
+  override render() {
+    return html`${sanitizeHtmlToFragment(this.htmlContent ?? '')}`;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-html-renderer': GrHtmlRenderer;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-html-renderer/gr-html-renderer_test.ts b/polygerrit-ui/app/elements/shared/gr-html-renderer/gr-html-renderer_test.ts
new file mode 100644
index 0000000..58d549e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-html-renderer/gr-html-renderer_test.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-html-renderer';
+import {GrHtmlRenderer} from './gr-html-renderer';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {assert} from '@open-wc/testing';
+
+suite('gr-html-renderer tests', () => {
+  let element: GrHtmlRenderer;
+
+  setup(async () => {
+    element = await fixture<GrHtmlRenderer>(
+      html`<gr-html-renderer></gr-html-renderer>`
+    );
+  });
+
+  test('renders html content', async () => {
+    const content = '<b>bold</b>';
+    element.htmlContent = content;
+    await element.updateComplete;
+    assert.shadowDom.equal(element, content);
+  });
+
+  test('renders empty string for undefined content', async () => {
+    element.htmlContent = undefined;
+    await element.updateComplete;
+    assert.shadowDom.equal(element, '');
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
deleted file mode 100644
index 89c8770..0000000
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '@polymer/iron-icon/iron-icon';
-import '@polymer/iron-iconset-svg/iron-iconset-svg';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24">
-  <svg>
-    <defs>
-      <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=swap_horiz-->
-      <g id="swapHoriz"><path d="M0 0h24v24H0z" fill="none"/><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/></g>
-      <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aplay_arrow-->
-      <g id="playArrow"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></g>
-      <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Apause-->
-      <g id="pause"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></g>
-    </defs>
-  </svg>
-</iron-iconset-svg>`;
-
-document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
index b2e412c..f3418a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
@@ -8,7 +8,7 @@
 import '../../change/gr-change-actions/gr-change-actions';
 import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
 import {GrChangeActions} from '../../change/gr-change-actions/gr-change-actions';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {PluginApi} from '../../../api/plugin';
 import {
   ActionPriority,
@@ -131,7 +131,7 @@
       await element.updateComplete;
       let button = query<GrButton>(element, `[data-action-key="${key}"]`);
       assert.isOk(button);
-      assert.isFalse(button!.hasAttribute('hidden'));
+      assert.isFalse(button.hasAttribute('hidden'));
       changeActions.setActionHidden(ActionType.REVISION, key, true);
       await element.updateComplete;
       button = query<GrButton>(element, `[data-action-key="${key}"]`);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 28e3718..8417a94 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -13,8 +13,8 @@
 } from '../../../types/common';
 import {GrAdminApi} from '../../plugins/gr-admin-api/gr-admin-api';
 import {
-  JsApiService,
   EventCallback,
+  JsApiService,
   ShowChangeDetail,
   ShowDiffDetail,
   ShowRevisionActionsDetail,
@@ -66,6 +66,21 @@
     return !cancelSubmit;
   }
 
+  async handleBeforeChangeAction(
+    key: string,
+    change?: ParsedChangeInfo
+  ): Promise<boolean> {
+    let okay = true;
+    for (const cb of this._getEventCallbacks(EventType.BEFORE_CHANGE_ACTION)) {
+      try {
+        okay = (await cb(key, change)) && okay;
+      } catch (err: unknown) {
+        this.reportError(err, EventType.BEFORE_CHANGE_ACTION);
+      }
+    }
+    return okay;
+  }
+
   handlePublishEdit(change: ChangeInfo, revision?: RevisionInfo | null) {
     for (const cb of this._getEventCallbacks(EventType.PUBLISH_EDIT)) {
       try {
@@ -83,6 +98,17 @@
     }
   }
 
+  async handleViewChange(view: string) {
+    await this.waitForPluginsToLoad();
+    for (const cb of this._getEventCallbacks(EventType.VIEW_CHANGE)) {
+      try {
+        cb(view);
+      } catch (err: unknown) {
+        this.reportError(err, EventType.VIEW_CHANGE);
+      }
+    }
+  }
+
   async handleShowChange(detail: ShowChangeDetail) {
     if (!detail.change) return;
     await this.waitForPluginsToLoad();
@@ -128,6 +154,22 @@
     }
   }
 
+  async handleBeforeReplySent(
+    change: ChangeInfo | ParsedChangeInfo,
+    reviewInput: ReviewInput
+  ): Promise<boolean> {
+    await this.waitForPluginsToLoad();
+    let okay = true;
+    for (const cb of this._getEventCallbacks(EventType.BEFORE_REPLY_SENT)) {
+      try {
+        okay = (await cb(change, reviewInput)) && okay;
+      } catch (err: unknown) {
+        this.reportError(err, EventType.BEFORE_REPLY_SENT);
+      }
+    }
+    return okay;
+  }
+
   async handleReplySent() {
     await this.waitForPluginsToLoad();
     for (const cb of this._getEventCallbacks(EventType.REPLY_SENT)) {
@@ -153,6 +195,23 @@
     }
   }
 
+  async handleBeforeCommitMessage(
+    change: ChangeInfo | ParsedChangeInfo,
+    msg: string
+  ): Promise<boolean> {
+    let okay = true;
+    for (const cb of this._getEventCallbacks(
+      EventType.BEFORE_COMMIT_MSG_EDIT
+    )) {
+      try {
+        okay = (await cb(change, msg)) && okay;
+      } catch (err: unknown) {
+        this.reportError(err, EventType.BEFORE_COMMIT_MSG_EDIT);
+      }
+    }
+    return okay;
+  }
+
   handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string) {
     for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
       try {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts
index ae6f3f6..460be3c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts
@@ -9,15 +9,15 @@
 import {EventType} from '../../../api/plugin';
 import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils';
 import {
-  stubRestApi,
   stubBaseUrl,
+  stubRestApi,
   waitEventLoop,
   waitUntilCalled,
 } from '../../../test/test-utils';
 import {assert} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 import {PluginLoader, pluginLoaderToken} from './gr-plugin-loader';
-import {useFakeTimers, stub, SinonFakeTimers, SinonStub} from 'sinon';
+import {SinonFakeTimers, SinonStub, stub, useFakeTimers} from 'sinon';
 import {GrJsApiInterface} from './gr-js-api-interface-element';
 import {Plugin} from './gr-public-js-api';
 import {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index ac62b21..9f40f7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -5,8 +5,8 @@
  */
 import {
   ActionInfo,
-  ChangeInfo,
   BasePatchSetNum,
+  ChangeInfo,
   PatchSetNum,
   ReviewInput,
   RevisionInfo,
@@ -46,10 +46,22 @@
     revertSubmissionMsg: string,
     origMsg: string
   ): string;
+  /**
+   * This method is called before handling a change action.
+   * It allows plugins to conditionally block actions.
+   * @param key The change action key.
+   * @param change The relevant change.
+   * @return A promise that resolves to true if the action should proceed.
+   */
+  handleBeforeChangeAction(
+    key: string,
+    change?: ParsedChangeInfo
+  ): Promise<boolean>;
   handlePublishEdit(change: ChangeInfo, revision?: RevisionInfo | null): void;
   handleShowChange(detail: ShowChangeDetail): Promise<void>;
   handleShowRevisionActions(detail: ShowRevisionActionsDetail): void;
   handleLabelChange(detail: {change?: ParsedChangeInfo}): void;
+  handleViewChange(view?: string): void;
   modifyEmojis(emojis?: EmojiSuggestion[]): EmojiSuggestion[] | undefined;
   modifyRevertMsg(
     change: ChangeInfo,
@@ -58,9 +70,31 @@
   ): string;
   addElement(key: TargetElement, el: HTMLElement): void;
   getAdminMenuLinks(): MenuLink[];
+  /**
+   * This method is called before handling a commit message edit.
+   * It allows plugins to conditionally block edits.
+   * @param change The relevant change.
+   * @param msg The new commit message text.
+   * @return A promise that resolves to true if the action should proceed.
+   */
+  handleBeforeCommitMessage(
+    change: ChangeInfo | ParsedChangeInfo,
+    msg: string
+  ): Promise<boolean>;
   handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string): void;
   canSubmitChange(change: ChangeInfo, revision?: RevisionInfo | null): boolean;
   getReviewPostRevert(change?: ChangeInfo): ReviewInput;
   handleShowDiff(detail: ShowDiffDetail): void;
+  /**
+   * This method is called before a reply to a change is sent.
+   * It allows plugins to modify the review input or to block sending.
+   * @param change The change the reply is on.
+   * @param reviewInput The review input that is about to be sent.
+   * @return A promise that resolves to true if the reply should be sent.
+   */
+  handleBeforeReplySent(
+    change: ChangeInfo | ParsedChangeInfo,
+    reviewInput: ReviewInput
+  ): Promise<boolean>;
   handleReplySent(): Promise<void>;
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
index c81f586..765b0ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
@@ -3,7 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {RevisionInfo, ChangeInfo, RequestPayload} from '../../../types/common';
+import {ChangeInfo, RequestPayload, RevisionInfo} from '../../../types/common';
 import {PluginApi} from '../../../api/plugin';
 import {UIActionInfo} from './gr-change-actions-js-api';
 import {windowLocationReload} from '../../../utils/dom-util';
@@ -51,7 +51,7 @@
   }
 
   textfield(): HTMLElement {
-    return document.createElement('paper-input');
+    return document.createElement('md-outlined-text-field');
   }
 
   br() {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
index 76a6573..3eb4bf3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.ts
@@ -9,7 +9,7 @@
 import {addListenerForTest, waitEventLoop} from '../../../test/test-utils';
 import {assert} from '@open-wc/testing';
 import {PluginApi} from '../../../api/plugin';
-import {SinonStub, stub, spy} from 'sinon';
+import {SinonStub, spy, stub} from 'sinon';
 import {PopupPluginApi} from '../../../api/popup';
 import {GrButton} from '../gr-button/gr-button';
 import {createChange, createRevision} from '../../../test/test-data-generators';
@@ -59,7 +59,7 @@
   });
 
   test('textfield', () => {
-    assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
+    assert.equal(instance.textfield().tagName, 'MD-OUTLINED-TEXT-FIELD');
   });
 
   test('br', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 7082ec5..70952da 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -4,9 +4,9 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {
-  PLUGIN_LOADING_TIMEOUT_MS,
   getPluginNameFromUrl,
   isThemeFile,
+  PLUGIN_LOADING_TIMEOUT_MS,
   THEME_JS,
 } from './gr-api-utils';
 import {Plugin} from './gr-public-js-api';
@@ -25,11 +25,11 @@
 import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {iconStyles} from '../../../styles/gr-icon-styles';
 import {GrJsApiInterface} from './gr-js-api-interface-element';
 import {define} from '../../../models/dependency';
 import {modalStyles} from '../../../styles/gr-modal-styles';
 import {Finalizable} from '../../../types/types';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 enum PluginState {
   /** State that indicates the plugin is pending to be loaded. */
@@ -84,7 +84,7 @@
   public readonly styles = {
     font: fontStyles,
     form: grFormStyles,
-    icon: iconStyles,
+    material: materialStyles,
     menuPage: menuPageStyles,
     spinner: spinnerStyles,
     subPage: subpageStyles,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index b076373..35c93e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -36,6 +36,7 @@
 import {GrPluginStyleApi} from './gr-plugin-style-api';
 import {StylePluginApi} from '../../../api/styles';
 import {GrSuggestionsApi} from '../../plugins/gr-suggestions-api/gr-suggestions-api';
+import {GrChangeUpdatesApi} from '../../plugins/gr-change-updates-api/gr-change-updates-api';
 
 const PLUGIN_NAME_NOT_SET = 'NULL';
 
@@ -204,6 +205,10 @@
     return new GrChecksApi(this.report, this.pluginsModel, this);
   }
 
+  changeUpdates(): GrChangeUpdatesApi {
+    return new GrChangeUpdatesApi(this.pluginsModel, this);
+  }
+
   suggestions(): GrSuggestionsApi {
     return new GrSuggestionsApi(this.report, this.pluginsModel, this);
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index c7a6b88..28f6d6e 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -12,15 +12,14 @@
 import '../gr-button/gr-button';
 import '../gr-tooltip-content/gr-tooltip-content';
 import {
-  AccountInfo,
-  LabelInfo,
-  ApprovalInfo,
   AccountId,
+  AccountInfo,
+  ApprovalInfo,
   isDetailedLabelInfo,
+  LabelInfo,
 } from '../../../types/common';
-import {LitElement, css, html} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {GrButton} from '../gr-button/gr-button';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
 import {
   canReviewerVote,
   getApprovalInfo,
@@ -33,7 +32,6 @@
 import {fontStyles} from '../../../styles/gr-font-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {votingStyles} from '../../../styles/gr-voting-styles';
-import {ifDefined} from 'lit/directives/if-defined.js';
 import {fireReload} from '../../../utils/event-util';
 import {sortReviewers} from '../../../utils/attention-set-util';
 
@@ -73,13 +71,12 @@
   @property({type: Boolean})
   showAllReviewers = true;
 
+  @state() private deleteButtonDisabled = false;
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly reporting = getAppContext().reportingService;
 
-  // TODO(TS): not used, remove later
-  _xhrPromise?: Promise<void>;
-
   static override get styles() {
     return [
       sharedStyles,
@@ -103,7 +100,7 @@
         gr-button {
           vertical-align: top;
         }
-        gr-button::part(paper-button) {
+        gr-button::part(md-text-button) {
           height: var(--line-height-normal);
           width: var(--line-height-normal);
           padding: 0;
@@ -194,18 +191,22 @@
     reviewer: AccountInfo,
     approvalInfo: ApprovalInfo | undefined
   ) {
+    const accountId = reviewer._account_id;
+    const canDeleteVote = this.canDeleteVote(
+      reviewer,
+      this.mutable,
+      this.change,
+      approvalInfo
+    );
+    if (!accountId || !canDeleteVote) return;
+
     return html`<gr-tooltip-content has-tooltip title="Remove vote">
       <gr-button
         link
         aria-label="Remove vote"
-        @click=${this.onDeleteVote}
-        data-account-id=${ifDefined(reviewer._account_id as number | undefined)}
-        class="deleteBtn ${this.computeDeleteClass(
-          reviewer,
-          this.mutable,
-          this.change,
-          approvalInfo
-        )}"
+        @click=${() => this.onDeleteVote(accountId)}
+        ?disabled=${this.deleteButtonDisabled}
+        class="deleteBtn"
       >
         <gr-icon icon="delete" filled></gr-icon>
       </gr-button>
@@ -258,12 +259,9 @@
    * A user is able to delete a vote iff the mutable property is true and the
    * reviewer that left the vote exists in the list of removable_labels
    * received from the backend.
-   *
-   * @param reviewer An object describing the reviewer that left the
-   *     vote.
    */
-  private computeDeleteClass(
-    reviewer: ApprovalInfo,
+  private canDeleteVote(
+    reviewer: AccountInfo,
     mutable: boolean,
     change?: ParsedChangeInfo,
     approvalInfo?: ApprovalInfo
@@ -275,65 +273,31 @@
       !approvalInfo.value ||
       !change.removable_labels
     ) {
-      return 'hidden';
+      return false;
     }
     const removableAccounts =
       change.removable_labels[this.label]?.[valueString(approvalInfo.value)];
     if (!removableAccounts) {
-      return 'hidden';
+      return false;
     }
-    if (removableAccounts.find(r => r._account_id === reviewer?._account_id)) {
-      return '';
-    }
-    return 'hidden';
+    return removableAccounts.find(r => r._account_id === reviewer?._account_id);
   }
 
-  /**
-   * Closure annotation for Polymer.prototype.splice is off.
-   * For now, suppressing annotations.
-   */
-  private onDeleteVote(e: MouseEvent) {
-    if (!this.change) return;
+  private async onDeleteVote(accountId: AccountId) {
+    if (!this.change || !accountId) return;
 
-    e.preventDefault();
-    let target = e.composedPath()[0] as GrButton;
-    while (!target.classList.contains('deleteBtn')) {
-      if (!target.parentElement) {
-        return;
-      }
-      target = target.parentElement as GrButton;
+    this.deleteButtonDisabled = true;
+    try {
+      const response = await this.restApiService.deleteVote(
+        this.change._number,
+        accountId,
+        this.label
+      );
+      if (response.ok && this.change) fireReload(this);
+    } catch (err) {
+      this.reporting.error('Delete vote', err as Error);
+    } finally {
+      this.deleteButtonDisabled = false;
     }
-
-    target.disabled = true;
-    const accountID = Number(
-      `${target.getAttribute('data-account-id')}`
-    ) as AccountId;
-    this._xhrPromise = this.restApiService
-      .deleteVote(this.change._number, accountID, this.label)
-      .then(response => {
-        target.disabled = false;
-        if (!response.ok) {
-          return;
-        }
-        if (this.change) {
-          fireReload(this);
-        }
-      })
-      .catch(err => {
-        this.reporting.error('Delete vote', err);
-        target.disabled = false;
-        return;
-      });
-  }
-
-  _computeValueTooltip(labelInfo: LabelInfo | undefined, score: string) {
-    if (
-      !labelInfo ||
-      !isDetailedLabelInfo(labelInfo) ||
-      !labelInfo.values?.[score]
-    ) {
-      return '';
-    }
-    return labelInfo.values[score];
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index 79a5036..14561ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -3,12 +3,11 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-label-info';
 import {
-  isHidden,
   mockPromise,
+  query,
   queryAndAssert,
   stubRestApi,
 } from '../../../test/test-utils';
@@ -20,7 +19,7 @@
   createParsedChange,
 } from '../../../test/test-data-generators';
 import {ApprovalInfo, LabelInfo} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-label-info tests', () => {
   let element: GrLabelInfo;
@@ -58,19 +57,6 @@
           <gr-account-chip>
             <gr-vote-chip circle-shape="" slot="vote-chip"> </gr-vote-chip>
           </gr-account-chip>
-          <gr-tooltip-content has-tooltip="" title="Remove vote">
-            <gr-button
-              aria-disabled="false"
-              aria-label="Remove vote"
-              class="deleteBtn hidden"
-              data-account-id="5"
-              link=""
-              role="button"
-              tabindex="0"
-            >
-              <gr-icon icon="delete" filled></gr-icon>
-            </gr-button>
-          </gr-tooltip-content>
         </div>
       </div>`
     );
@@ -84,7 +70,6 @@
     };
 
     setup(async () => {
-      sinon.stub(element, '_computeValueTooltip').returns('');
       element.account = account;
       element.change = {
         ...createParsedChange(),
@@ -103,12 +88,12 @@
     test('_computeCanDeleteVote', async () => {
       element.mutable = false;
       await element.updateComplete;
-      const removeButton = queryAndAssert<GrButton>(element, 'gr-button');
-      assert.isTrue(isHidden(removeButton));
+      assert.isUndefined(query<GrButton>(element, 'gr-button'));
+
       element.change!.removable_labels = {'Code-Review': {'+1': [account]}};
       element.mutable = true;
       await element.updateComplete;
-      assert.isFalse(isHidden(removeButton));
+      assert.isDefined(query<GrButton>(element, 'gr-button'));
     });
 
     test('deletes votes', async () => {
@@ -121,12 +106,16 @@
         recommended: account,
       };
       element.mutable = true;
-      const removeButton = queryAndAssert<GrButton>(element, 'gr-button');
+      await element.updateComplete;
 
+      const removeButton = queryAndAssert<GrButton>(element, 'gr-button');
       removeButton.click();
+      await element.updateComplete;
+
       assert.isTrue(removeButton.disabled);
       mock.resolve();
       await deleteResponse;
+      await element.updateComplete;
 
       assert.isFalse(removeButton.disabled);
       assert.isTrue(
@@ -139,22 +128,6 @@
     });
   });
 
-  test('_computeValueTooltip', () => {
-    // Existing label.
-    let labelInfo: LabelInfo = {values: {0: 'Baz'}};
-    let score = '0';
-    assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
-
-    // Non-existent score.
-    score = '2';
-    assert.equal(element._computeValueTooltip(labelInfo, score), '');
-
-    // No values on label.
-    labelInfo = {values: {}};
-    score = '0';
-    assert.equal(element._computeValueTooltip(labelInfo, score), '');
-  });
-
   suite('computeVoters', () => {
     const account2 = createAccountWithIdNameAndEmail(7);
     test('show reviewer who voted', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
index a40b0cf..b12622a 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.ts
@@ -5,11 +5,11 @@
  */
 import '../gr-autocomplete/gr-autocomplete';
 import '../../../styles/shared-styles';
-import {LitElement, css, html, PropertyValues} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query} from 'lit/decorators.js';
 import {
-  GrAutocomplete,
   AutocompleteQuery,
+  GrAutocomplete,
 } from '../gr-autocomplete/gr-autocomplete';
 import {assertIsDefined} from '../../../utils/common-util';
 import {fire} from '../../../utils/event-util';
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts
index 1a2d23c..86a7943a 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.ts
@@ -8,7 +8,7 @@
 import './gr-labeled-autocomplete';
 import {GrLabeledAutocomplete} from './gr-labeled-autocomplete';
 import {assertIsDefined} from '../../../utils/common-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-labeled-autocomplete tests', () => {
   let element: GrLabeledAutocomplete;
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts
index 583cf80..a85fd6e 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts
@@ -67,7 +67,7 @@
    * @return a promise that resolves when the script's onload
    * executes.
    */
-  _loadScript(src: string | null) {
+  _loadScript(src: string | null): Promise<unknown> {
     return new Promise((resolve, reject) => {
       const script = document.createElement('script');
 
@@ -75,10 +75,22 @@
         reject(new Error('Unable to load blank script url.'));
         return;
       }
+
+      const timeoutId = setTimeout(
+        () => reject(new Error(`Timeout loading script: ${src}`)),
+        2000
+      );
+
       script.setAttribute('crossorigin', 'anonymous');
       script.setAttribute('src', src);
-      script.onload = resolve;
-      script.onerror = reject;
+      script.onload = e => {
+        clearTimeout(timeoutId);
+        resolve(e);
+      };
+      script.onerror = e => {
+        clearTimeout(timeoutId);
+        reject(e);
+      };
       document.head.appendChild(script);
     });
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts
index c646525..9963aa1 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts
@@ -8,7 +8,7 @@
 import {GrTooltipContent} from '../gr-tooltip-content/gr-tooltip-content';
 import './gr-limited-text';
 import {GrLimitedText} from './gr-limited-text';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-limited-text tests', () => {
   let element: GrLimitedText;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
index 7717683..f048046 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
@@ -8,7 +8,7 @@
 import '../gr-limited-text/gr-limited-text';
 import {fire} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 
 declare global {
@@ -65,9 +65,9 @@
         gr-icon {
           font-size: 1.2rem;
         }
-        gr-button::part(paper-button),
-        gr-button.remove:hover::part(paper-button),
-        gr-button.remove:focus::part(paper-button) {
+        gr-button::part(md-text-button),
+        gr-button.remove:hover::part(md-text-button),
+        gr-button.remove:focus::part(md-text-button) {
           border-top-width: 0;
           border-right-width: 0;
           border-bottom-width: 0;
@@ -88,7 +88,7 @@
     return html`<div class="container">
       <a href=${this.href}>
         <gr-limited-text
-          .limit=${this.limit}
+          .limit=${this.limit ?? 25}
           .text=${this.text}
         ></gr-limited-text>
       </a>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.ts
index 3f9a32b..d6437ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.ts
@@ -8,7 +8,7 @@
 import './gr-linked-chip';
 import {GrLinkedChip} from './gr-linked-chip';
 import {queryAndAssert, waitEventLoop} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrButton} from '../gr-button/gr-button';
 
 suite('gr-linked-chip tests', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
index 53149d0..67b4332 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
@@ -3,19 +3,19 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-input/iron-input';
 import '../gr-button/gr-button';
 import '../gr-icon/gr-icon';
 import {encodeURL, getBaseUrl} from '../../../utils/url-util';
 import {fire} from '../../../utils/event-util';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
-import {BindValueChangeEvent} from '../../../types/events';
 import {resolve} from '../../../models/dependency';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {formStyles} from '../../../styles/form-styles';
+import '@material/web/textfield/outlined-text-field';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
 
@@ -33,6 +33,9 @@
   @property({type: Boolean})
   createNew?: boolean;
 
+  @property({type: String})
+  createNewText = 'Create New';
+
   @property({type: Array})
   items?: unknown[];
 
@@ -63,15 +66,10 @@
 
   static override get styles() {
     return [
+      materialStyles,
       formStyles,
       sharedStyles,
       css`
-        #filter {
-          max-width: 25em;
-        }
-        #filter:focus {
-          outline: none;
-        }
         #topContainer {
           align-items: center;
           display: flex;
@@ -79,8 +77,13 @@
           justify-content: space-between;
           margin: 0 var(--spacing-l);
         }
-        #createNewContainer:not(.show) {
-          display: none;
+        .filterContainer {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+        }
+        .filterContainer label {
+          margin-bottom: 3px;
         }
         a {
           color: var(--primary-text-color);
@@ -101,6 +104,9 @@
           font-size: 1.85rem;
           margin-left: 16px;
         }
+        md-outlined-text-field {
+          max-width: 25em;
+        }
       `,
     ];
   }
@@ -110,22 +116,27 @@
       <div id="topContainer">
         <div class="filterContainer">
           <label>Filter:</label>
-          <iron-input
-            .bindValue=${this.filter}
-            @bind-value-changed=${this.handleFilterBindValueChanged}
+          <md-outlined-text-field
+            .value=${this.filter ?? ''}
+            @input=${(e: InputEvent) => {
+              const target = e.target as HTMLInputElement;
+              this.filter = target.value;
+            }}
           >
-            <input type="text" id="filter" />
-          </iron-input>
+          </md-outlined-text-field>
         </div>
-        <div id="createNewContainer" class=${this.createNew ? 'show' : ''}>
+        <div id="createNewContainer">
+          <slot name="createNewContainerTop"></slot>
           <gr-button
             id="createNew"
+            ?hidden=${!this.createNew}
             primary
             link
             @click=${() => this.createNewItem()}
           >
-            Create New
+            ${this.createNewText}
           </gr-button>
+          <slot name="createNewContainerBottom"></slot>
         </div>
       </div>
       <slot></slot>
@@ -219,8 +230,4 @@
   computePage() {
     return this.offset / this.itemsPerPage + 1;
   }
-
-  private handleFilterBindValueChanged(e: BindValueChangeEvent) {
-    this.filter = e.detail.value;
-  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
index c0d9755..5dab2ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
@@ -9,7 +9,7 @@
 import {GrListView} from './gr-list-view';
 import {queryAndAssert, stubBaseUrl} from '../../../test/test-utils';
 import {GrButton} from '../gr-button/gr-button';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {testResolver} from '../../../test/common-test-setup';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 
@@ -27,13 +27,14 @@
         <div id="topContainer">
           <div class="filterContainer">
             <label> Filter: </label>
-            <iron-input>
-              <input id="filter" type="text" />
-            </iron-input>
+            <md-outlined-text-field autocomplete="" inputmode="" type="text">
+            </md-outlined-text-field>
           </div>
           <div id="createNewContainer">
+            <slot name="createNewContainerTop"> </slot>
             <gr-button
               aria-disabled="false"
+              hidden=""
               id="createNew"
               link=""
               primary=""
@@ -42,16 +43,17 @@
             >
               Create New
             </gr-button>
+            <slot name="createNewContainerBottom"> </slot>
           </div>
         </div>
         <slot> </slot>
         <nav>
           Page 1
           <a hidden="" href="" id="prevArrow">
-            <gr-icon icon="chevron_left"></gr-icon>
+            <gr-icon icon="chevron_left"> </gr-icon>
           </a>
           <a hidden="" href=",25" id="nextArrow">
-            <gr-icon icon="chevron_right"></gr-icon>
+            <gr-icon icon="chevron_right"> </gr-icon>
           </a>
         </nav>
       `
@@ -157,19 +159,11 @@
   });
 
   test('createNew link appears correctly', async () => {
-    assert.isFalse(
-      queryAndAssert<HTMLDivElement>(
-        element,
-        '#createNewContainer'
-      ).classList.contains('show')
-    );
+    assert.isTrue(queryAndAssert<HTMLDivElement>(element, '#createNew').hidden);
     element.createNew = true;
     await element.updateComplete;
-    assert.isTrue(
-      queryAndAssert<HTMLDivElement>(
-        element,
-        '#createNewContainer'
-      ).classList.contains('show')
+    assert.isFalse(
+      queryAndAssert<HTMLDivElement>(element, '#createNew').hidden
     );
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-marked-element/gr-marked-element.ts b/polygerrit-ui/app/elements/shared/gr-marked-element/gr-marked-element.ts
new file mode 100644
index 0000000..5aeb2cb
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-marked-element/gr-marked-element.ts
@@ -0,0 +1,141 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {
+  customElement,
+  property,
+  queryAssignedElements,
+} from 'lit/decorators.js';
+// @ts-ignore
+import * as marked from 'marked/lib/marked';
+
+if (!window.marked) {
+  window.marked = marked;
+}
+
+declare global {
+  interface Window {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    marked: any;
+  }
+
+  interface HTMLElementTagNameMap {
+    'gr-marked-element': GrMarkedElement;
+  }
+}
+
+/**
+ * This is based on [marked-element](https://github.com/PolymerElements/marked-element) by Polymer
+ * but converted to use Lit. It uses the [marked](https://github.com/markedjs/marked) library.
+ */
+@customElement('gr-marked-element')
+export class GrMarkedElement extends LitElement {
+  @property({type: String}) markdown: string | null = null;
+
+  @property({type: Boolean}) breaks = false;
+
+  @property({type: Boolean}) pedantic = false;
+
+  @property({type: Function}) renderer: Function | null = null;
+
+  @property({type: Boolean}) sanitize = false;
+
+  @property({type: Function}) sanitizer:
+    | ((html: string) => string)
+    | undefined = undefined;
+
+  @property({type: Boolean}) smartypants = false;
+
+  @property({type: Function}) callback: Function | null = null;
+
+  @queryAssignedElements({
+    flatten: true,
+    slot: 'markdown-html',
+  })
+  private outputElement!: Array<HTMLElement>;
+
+  static override styles = css`
+    :host {
+      display: block;
+    }
+  `;
+
+  override render() {
+    return html`
+      <slot name="markdown-html">
+        <div id="content" slot="markdown-html"></div>
+      </slot>
+    `;
+  }
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.renderMarkdown();
+  }
+
+  protected override updated(changedProps: PropertyValues) {
+    const propsToWatch = [
+      'markdown',
+      'breaks',
+      'pedantic',
+      'renderer',
+      'sanitize',
+      'sanitizer',
+      'smartypants',
+      'callback',
+    ];
+
+    if (propsToWatch.some(prop => changedProps.has(prop))) {
+      this.renderMarkdown();
+    }
+  }
+
+  override firstUpdated() {
+    this.renderMarkdown();
+  }
+
+  private renderMarkdown() {
+    if (!this.isConnected || !this.outputElement.length) {
+      return;
+    }
+
+    if (!this.markdown) {
+      this.outputElement[0].innerHTML = '';
+      return;
+    }
+
+    const renderer = new window.marked.Renderer();
+    if (this.renderer) this.renderer(renderer);
+
+    const options = {
+      renderer,
+      highlight: this.highlight.bind(this),
+      breaks: this.breaks,
+      sanitize: this.sanitize,
+      sanitizer: this.sanitizer,
+      pedantic: this.pedantic,
+      smartypants: this.smartypants,
+    };
+
+    const output = window.marked(this.markdown, options, this.callback);
+
+    this.outputElement[0].innerHTML = output;
+    this.dispatchEvent(
+      new CustomEvent('marked-render-complete', {bubbles: true, composed: true})
+    );
+  }
+
+  private highlight(code: string, lang: string): string {
+    const event = new CustomEvent('syntax-highlight', {
+      detail: {code, lang},
+      bubbles: true,
+      composed: true,
+    });
+    this.dispatchEvent(event);
+    return event.detail.code || code;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-marked-element/gr-marked-element_test.ts b/polygerrit-ui/app/elements/shared/gr-marked-element/gr-marked-element_test.ts
new file mode 100644
index 0000000..68f5401
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-marked-element/gr-marked-element_test.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-marked-element';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrMarkedElement} from './gr-marked-element';
+
+suite('gr-marked-element tests', () => {
+  let element: GrMarkedElement;
+
+  setup(async () => {
+    element = await fixture<GrMarkedElement>(
+      html`<gr-marked-element></gr-marked-element>`
+    );
+    await element.updateComplete;
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <slot name="markdown-html">
+          <div id="content" slot="markdown-html"></div>
+        </slot>
+      `
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
index 08ae5a3..ec79b5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
index f627577..717150e 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
@@ -7,7 +7,7 @@
 import '../../../test/common-test-setup';
 import './gr-page-nav';
 import {GrPageNav} from './gr-page-nav';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {queryAndAssert} from '../../../test/test-utils';
 
 suite('gr-page-nav tests', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts
index 175f4f7..1c6551d 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts
@@ -8,16 +8,16 @@
 import {singleDecodeURL} from '../../../utils/url-util';
 import {AutocompleteQuery} from '../gr-autocomplete/gr-autocomplete';
 import {
-  BranchName,
-  RepoName,
-  ProjectInfoWithName,
   BranchInfo,
+  BranchName,
+  ProjectInfoWithName,
+  RepoName,
 } from '../../../types/common';
 import {GrLabeledAutocomplete} from '../gr-labeled-autocomplete/gr-labeled-autocomplete';
 import {getAppContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, html, css} from 'lit';
-import {customElement, property, state, query} from 'lit/decorators.js';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {assertIsDefined} from '../../../utils/common-util';
 import {fire} from '../../../utils/event-util';
 import {BindValueChangeEvent} from '../../../types/events';
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
index 24b377c..65c9709 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
@@ -9,7 +9,7 @@
 import {GrRepoBranchPicker} from './gr-repo-branch-picker';
 import {stubRestApi} from '../../../test/test-utils';
 import {GitRef, ProjectInfoWithName, RepoName} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-repo-branch-picker tests', () => {
   let element: GrRepoBranchPicker;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
index 40c1acb..030b4a1 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
@@ -6,14 +6,14 @@
 import * as sinon from 'sinon';
 import '../../../../test/common-test-setup';
 import {
-  SiteBasedCache,
   FetchPromisesCache,
+  getFetchOptions,
   GrRestApiHelper,
   JSON_PREFIX,
-  readJSONResponsePayload,
   parsePrefixedJSON,
-  getFetchOptions,
+  readJSONResponsePayload,
   REQUEST_ORIGIN_HEADER,
+  SiteBasedCache,
 } from './gr-rest-api-helper';
 import {
   addListenerForTest,
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
index 20f708e..763e292 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
@@ -6,7 +6,7 @@
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-select';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrSelect} from './gr-select';
 
 suite('gr-select tests', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-selector/gr-selector.ts b/polygerrit-ui/app/elements/shared/gr-selector/gr-selector.ts
new file mode 100644
index 0000000..b424f010
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-selector/gr-selector.ts
@@ -0,0 +1,206 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fire} from '../../../utils/event-util';
+
+/**
+ * This is a replacement for iron-selector.
+ * Based on https://github.com/chromium/chromium/blob/f7322d4ecf3ee3804ce7e80e1e9d4b98f23b9295/ui/webui/resources/cr_elements/cr_selectable_mixin.ts.
+ * Modified for gerrit.
+ */
+@customElement('gr-selector')
+export class GrSelector extends LitElement {
+  @property({type: String})
+  attrForSelected: string | null = null;
+
+  @property({type: String})
+  selected?: string | number;
+
+  @property({type: String})
+  selectedAttribute: string | null = null;
+
+  @property({type: String})
+  selectable?: string;
+
+  @state()
+  items: Element[] = [];
+
+  private selectedItem_: Element | null = null;
+
+  override render() {
+    return html`<slot></slot>`;
+  }
+
+  override firstUpdated(changedProperties: PropertyValues<this>) {
+    super.firstUpdated(changedProperties);
+    this.addEventListener('click', e => this.onClick(e));
+    this.observeItems();
+  }
+
+  // Override this method in client code to modify the observation logic,
+  // or to turn it off completely. By default it listens for any changes on
+  // the first <slot> node in this shadowRoot.
+  observeItems() {
+    this.getSlot().addEventListener('slotchange', () => this.itemsChanged());
+  }
+
+  override connectedCallback() {
+    super.connectedCallback();
+    this.updateItems();
+  }
+
+  override willUpdate(changedProperties: PropertyValues<this>) {
+    super.willUpdate(changedProperties);
+
+    if (changedProperties.has('attrForSelected')) {
+      if (this.selectedItem_) {
+        assertIsDefined(this.attrForSelected);
+        const value = this.selectedItem_.getAttribute(this.attrForSelected);
+        assertIsDefined(value);
+        this.selected = value;
+      }
+    }
+  }
+
+  override updated(changedProperties: PropertyValues<this>) {
+    super.updated(changedProperties);
+    if (changedProperties.has('selected')) {
+      this.updateSelectedItem();
+    }
+  }
+
+  /**
+   * Selects the given value.
+   */
+  select(value: string | number) {
+    this.selected = value;
+  }
+
+  getSlot(): HTMLSlotElement {
+    const slot = this.shadowRoot!.querySelector('slot');
+    assertIsDefined(slot);
+    return slot;
+  }
+
+  // Override this method in client code to modify this logic, for example to
+  // grab children that don't reside in a <slot>.
+  queryItems(): Element[] {
+    const selectable = this.selectable === undefined ? '*' : this.selectable;
+    return Array.from(this.querySelectorAll(`:scope > ${selectable}`));
+  }
+
+  // If overriding queryItems(), override this method to return the list item
+  // element matching the CSS selector string |selector|.
+  queryMatchingItem(selector: string): HTMLElement | null {
+    const selectable = this.selectable || '*';
+    return this.querySelector<HTMLElement>(
+      `:scope > :is(${selectable})${selector}`
+    );
+  }
+
+  private updateItems() {
+    this.items = this.queryItems();
+    this.items.forEach((item, index) =>
+      item.setAttribute('data-selection-index', index.toString())
+    );
+  }
+
+  get selectedItem(): Element | null {
+    return this.selectedItem_;
+  }
+
+  private updateSelectedItem() {
+    if (!this.items) {
+      return;
+    }
+
+    const item =
+      this.selected === null || this.selected === undefined
+        ? null
+        : this.items[this.valueToIndex_(this.selected)];
+    if (!!item && this.selectedItem_ !== item) {
+      this.setItemSelected_(this.selectedItem_, false);
+      this.setItemSelected_(item, true);
+    } else if (!item) {
+      this.setItemSelected_(this.selectedItem_, false);
+    }
+  }
+
+  private setItemSelected_(item: Element | null, isSelected: boolean) {
+    if (!item) {
+      return;
+    }
+
+    item.classList.toggle('selected', isSelected);
+    if (this.selectedAttribute) {
+      item.toggleAttribute(this.selectedAttribute, isSelected);
+    }
+    this.selectedItem_ = isSelected ? item : null;
+    fire(this, isSelected ? 'select' : 'deselect', {item});
+  }
+
+  private valueToIndex_(value: string | number): number {
+    if (!this.attrForSelected) {
+      return Number(value);
+    }
+
+    const match = this.queryMatchingItem(
+      `[${this.attrForSelected}="${value}"]`
+    );
+    return match ? Number(match.dataset['selectionIndex']) : -1;
+  }
+
+  private indexToValue_(index: number): string | number {
+    if (!this.attrForSelected) {
+      return index;
+    }
+
+    const item = this.items[index];
+    if (!item) {
+      return index;
+    }
+
+    return item.getAttribute(this.attrForSelected) || index;
+  }
+
+  itemsChanged() {
+    this.updateItems();
+    this.updateSelectedItem();
+
+    // Let other interested parties know about the change.
+    fire(this, 'items-changed', {});
+  }
+
+  private onClick(e: MouseEvent) {
+    let element = e.target as HTMLElement;
+    while (element && element !== this) {
+      const idx = this.items.indexOf(element);
+      if (idx >= 0) {
+        const value = this.indexToValue_(idx);
+        assertIsDefined(value);
+        fire(this, 'activate', {item: element, selected: value});
+        this.select(value);
+        return;
+      }
+      element = element.parentNode as HTMLElement;
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-selector': GrSelector;
+  }
+  interface HTMLElementEventMap {
+    activate: CustomEvent<{}>;
+    'items-changed': CustomEvent<{}>;
+    select: CustomEvent<{}>;
+    deselect: CustomEvent<{}>;
+  }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-selector/gr-selector_test.ts b/polygerrit-ui/app/elements/shared/gr-selector/gr-selector_test.ts
new file mode 100644
index 0000000..6b48213
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-selector/gr-selector_test.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-selector';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrSelector} from './gr-selector';
+
+suite('gr-selector tests', () => {
+  let element: GrSelector;
+
+  setup(async () => {
+    element = await fixture<GrSelector>(html`<gr-selector></gr-selector>`);
+    await element.updateComplete;
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(element, /* HTML */ ' <slot> </slot> ');
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
index ef0f531..0c9be74 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
@@ -7,7 +7,7 @@
 import {GrCopyClipboard} from '../gr-copy-clipboard/gr-copy-clipboard';
 import {queryAndAssert} from '../../../utils/common-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 
 declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
index b2fed78..1e0ff11 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
@@ -9,7 +9,7 @@
 import {GrShellCommand} from './gr-shell-command';
 import {GrCopyClipboard} from '../gr-copy-clipboard/gr-copy-clipboard';
 import {queryAndAssert} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-shell-command tests', () => {
   let element: GrShellCommand;
@@ -38,7 +38,7 @@
 
   test('focusOnCopy', async () => {
     const focusStub = sinon.stub(
-      queryAndAssert<GrCopyClipboard>(element, 'gr-copy-clipboard')!,
+      queryAndAssert<GrCopyClipboard>(element, 'gr-copy-clipboard'),
       'focusOnCopy'
     );
     await element.focusOnCopy();
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
index c451bc2..837546e 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
@@ -5,14 +5,9 @@
  */
 import '../../../embed/diff/gr-diff/gr-diff';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {customElement, property, query, state} from 'lit/decorators.js';
 import {getAppContext} from '../../../services/app-context';
-import {
-  EDIT,
-  BasePatchSetNum,
-  PatchSetNumber,
-  RepoName,
-} from '../../../types/common';
+import {EDIT, PatchSetNumber, RepoName} from '../../../types/common';
 import {
   DiffLayer,
   DiffPreferencesInfo,
@@ -23,7 +18,11 @@
 import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
 import {resolve} from '../../../models/dependency';
 import {highlightServiceToken} from '../../../services/highlight/highlight-service';
-import {FixSuggestionInfo, NumericChangeId} from '../../../api/rest-api';
+import {
+  FixSuggestionInfo,
+  NumericChangeId,
+  RevisionPatchSetNum,
+} from '../../../api/rest-api';
 import {changeModelToken} from '../../../models/change/change-model';
 import {subscribe} from '../../lit/subscription-controller';
 import {DiffPreview} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
@@ -35,6 +34,8 @@
 import {getFileExtension} from '../../../utils/file-util';
 import {throwingErrorCallback} from '../gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {ReportSource} from '../../../services/suggestions/suggestions-service';
+import {replacementsToString} from '../../../utils/comment-util';
+import {GrTextarea} from '../../../embed/gr-textarea';
 
 export interface PreviewLoadedDetail {
   previewLoadedFor?: FixSuggestionInfo;
@@ -49,6 +50,9 @@
  */
 @customElement('gr-suggestion-diff-preview')
 export class GrSuggestionDiffPreview extends LitElement {
+  @query('#edit-textarea')
+  textarea?: GrTextarea;
+
   // Optional. Used as backup when preview is not loaded.
   @property({type: String})
   codeText?: string;
@@ -67,12 +71,15 @@
   uuid?: string;
 
   @property({type: Number})
-  patchSet?: BasePatchSetNum;
+  patchSet?: RevisionPatchSetNum;
 
   // Optional. Used in logging.
   @property({type: String})
   commentId?: string;
 
+  @property({type: Boolean, reflect: true})
+  editable = false;
+
   @state()
   layers: DiffLayer[] = [];
 
@@ -170,6 +177,40 @@
         .diff-container {
           border: 1px solid var(--border-color);
           border-top: none;
+          max-height: 70vh;
+          overflow-y: auto;
+        }
+        #edit-textarea {
+          width: 100%;
+          height: 200px;
+        }
+        /*
+         * On some operating systems (e.g. macOS), scrollbars are hidden by
+         * default and only appear when scrolling. The following rules force
+         * the scrollbar to be visible when the content overflows.
+         *
+         * This is only enabled for devices with a mouse, because it would be
+         * obtrusive on touch devices.
+         */
+        @media (pointer: fine) {
+          .diff-container::-webkit-scrollbar,
+          .diff-container::-webkit-scrollbar-corner {
+            background: transparent;
+            height: 12px;
+            width: 12px;
+          }
+          .diff-container::-webkit-scrollbar-button {
+            height: 0;
+            width: 0;
+          }
+          .diff-container::-webkit-scrollbar-thumb {
+            background: content-box currentColor;
+            border: 2px solid transparent;
+            border-radius: 8px;
+            color: var(--deemphasized-text-color);
+            min-height: 48px;
+            min-width: 48px;
+          }
         }
         code {
           max-width: var(--gr-formatted-text-prose-max-width, none);
@@ -210,9 +251,20 @@
         () => this.renderDiff(),
         () => html`<code>${this.codeText}</code>`
       )}
+      ${when(this.editable, () => this.renderTextarea())}
     `;
   }
 
+  private renderTextarea() {
+    const suggestion = replacementsToString(
+      this.fixSuggestionInfo?.replacements ?? []
+    );
+    return html`<gr-textarea
+      id="edit-textarea"
+      .value=${suggestion}
+    ></gr-textarea>`;
+  }
+
   private renderDiff() {
     if (!this.preview) return;
     const diff = this.preview.preview;
@@ -258,6 +310,19 @@
 
     return res;
   }
+
+  public reset() {
+    if (this.textarea && this.fixSuggestionInfo) {
+      this.textarea.value = replacementsToString(
+        this.fixSuggestionInfo.replacements
+      );
+    }
+  }
+
+  public getEditedContent(): string | undefined {
+    return this.textarea?.value;
+  }
+
   /**
    * Applies a fix (codeblock in comment message) previewed in
    * `suggestion-diff-preview`, navigating to the new change URL with the EDIT
@@ -304,13 +369,15 @@
         errorText,
       });
     }
-    if (res?.ok) {
+    // basePatchNum is from comment patchset and comment cannot be created
+    // in EDIT. RevisionPatchset without EDIT is PatchSetNumber
+    if (res?.ok && basePatchNum !== undefined && basePatchNum !== EDIT) {
       this.getNavigation().setUrl(
         createChangeUrl({
           changeNum,
           repo: this.repo!,
           patchNum: EDIT,
-          basePatchNum,
+          basePatchNum: basePatchNum as PatchSetNumber,
           forceReload: !this.hasEdit,
         })
       );
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
index 593d0a6..160996a5 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
@@ -5,7 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import './gr-suggestion-diff-preview';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {
   CommentModel,
   commentModelToken,
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts
index a5af48c..ac87d07 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea.ts
@@ -5,7 +5,6 @@
  */
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import '../gr-cursor-manager/gr-cursor-manager';
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import '../../../styles/shared-styles';
 import '../../../embed/gr-textarea';
 import {getAppContext} from '../../../services/app-context';
@@ -16,7 +15,7 @@
 } from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import {Key} from '../../../utils/dom-util';
 import {fire} from '../../../utils/event-util';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {PropertyValues} from 'lit';
@@ -37,6 +36,30 @@
 
 const MAX_ITEMS_DROPDOWN = 25;
 
+const FALLBACK_SUGGESTIONS: EmojiSuggestion[] = [
+  {value: '😊', match: 'smile :)'},
+  {value: '👍', match: 'thumbs up'},
+  {value: '😄', match: 'laugh :D'},
+  {value: '❤️', match: 'heart <3'},
+  {value: '😂', match: "tears :')"},
+  {value: '🎉', match: 'party'},
+  {value: '😎', match: 'cool |;)'},
+  {value: '😞', match: 'sad :('},
+  {value: '😐', match: 'neutral :|'},
+  {value: '😮', match: 'shock :O'},
+  {value: '🙏', match: 'pray'},
+  {value: '😕', match: 'confused'},
+  {value: '👌', match: 'ok'},
+  {value: '🔥', match: 'fire'},
+  {value: '💯', match: '100'},
+  {value: '✔', match: 'check'},
+  {value: '😋', match: 'tongue'},
+  {value: '😭', match: "crying :'("},
+  {value: '🤓', match: 'glasses'},
+  {value: '😢', match: 'tear'},
+  {value: '😜', match: 'winking tongue ;)'},
+];
+
 export interface EmojiSuggestion extends Item {
   match: string;
 }
@@ -95,6 +118,10 @@
 
   @state() suggestions: (Item | EmojiSuggestion)[] = [];
 
+  @state() private isDragging = false;
+
+  @state() private allowMarkdownBase64ImagesInComments = false;
+
   @state() emojis?: EmojiSuggestion[];
 
   // Accessed in tests.
@@ -126,6 +153,10 @@
 
   constructor() {
     super();
+    this.addEventListener('dragenter', this.handleDragEnter);
+    this.addEventListener('dragover', this.handleDragOver);
+    this.addEventListener('dragleave', this.handleDragLeave);
+    this.addEventListener('drop', this.handleDrop);
     subscribe(
       this,
       () => this.getChangeModel().changeNum$,
@@ -138,6 +169,13 @@
         this.serverConfig = config;
       }
     );
+    subscribe(
+      this,
+      () => this.getConfigModel().allowMarkdownBase64ImagesInComments$,
+      allow => {
+        this.allowMarkdownBase64ImagesInComments = allow;
+      }
+    );
     this.shortcuts.addLocal({key: Key.UP}, e => this.handleUpKey(e), {
       preventDefault: false,
     });
@@ -213,7 +251,6 @@
           --text-default: var(--primary-text-color);
           --text-disabled: var(--deemphasized-text-color);
           --text-secondary: var(--deemphasized-text-color);
-          --iron-autogrow-textarea_-_padding: var(--spacing-s);
         }
         #hiddenText #emojiSuggestions {
           visibility: visible;
@@ -230,6 +267,13 @@
           width: 100%;
           white-space: pre-wrap;
         }
+        :host {
+          display: block;
+        }
+        :host([data-dragging]) {
+          border: 2px dashed var(--border-color);
+          background-color: var(--background-color-secondary);
+        }
       `,
     ];
   }
@@ -252,7 +296,7 @@
       putCursorAtEndOnFocus
       class=${classMap({noBorder: this.hideBorder})}
       .placeholder=${this.placeholder}
-      ?disabled=${this.disabled}
+      ?disabled=${!!this.disabled}
       .value=${this.text}
       .hint=${this.autocompleteHint}
       @input=${(e: InputEvent) => {
@@ -294,6 +338,10 @@
         .getLibrary(EMOJIS_LIBRARY_CONFIG)
         .then(emojis => {
           if (emojis) this.emojis = emojis as EmojiSuggestion[];
+        })
+        .catch(err => {
+          console.error('Failed to load emojis library', err);
+          this.emojis = FALLBACK_SUGGESTIONS;
         });
     }
   }
@@ -303,6 +351,13 @@
       this.fireChangedEvents();
       this.handleTextChanged();
     }
+    if (changedProperties.has('isDragging')) {
+      if (this.isDragging) {
+        this.setAttribute('data-dragging', '');
+      } else {
+        this.removeAttribute('data-dragging');
+      }
+    }
   }
 
   // private but used in test
@@ -686,6 +741,58 @@
   isTextareaFocused() {
     return !!this.textarea?.isFocused;
   }
+
+  private handleDragEnter(e: DragEvent) {
+    if (!this.allowMarkdownBase64ImagesInComments) return;
+    e.preventDefault();
+    this.isDragging = true;
+  }
+
+  private handleDragOver(e: DragEvent) {
+    if (!this.allowMarkdownBase64ImagesInComments) return;
+    e.preventDefault();
+    e.dataTransfer!.dropEffect = 'copy';
+  }
+
+  private handleDragLeave(e: DragEvent) {
+    if (!this.allowMarkdownBase64ImagesInComments) return;
+    e.preventDefault();
+    this.isDragging = false;
+  }
+
+  private async handleDrop(e: DragEvent) {
+    if (!this.allowMarkdownBase64ImagesInComments) return;
+    e.preventDefault();
+    this.isDragging = false;
+
+    const files = e.dataTransfer?.files;
+    if (!files?.length) return;
+
+    const file = files[0];
+    if (!file.type.startsWith('image/')) {
+      console.warn('Only image files are supported');
+      return;
+    }
+
+    try {
+      const base64 = await this.fileToBase64(file);
+      const imageMarkdown = `![${file.name}](${base64})`;
+
+      // Add the image markdown to the text
+      this.text = this.text ? `${this.text}\n${imageMarkdown}` : imageMarkdown;
+    } catch (err) {
+      console.error('Error processing dropped image:', err);
+    }
+  }
+
+  private fileToBase64(file: File): Promise<string> {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = () => resolve(reader.result as string);
+      reader.onerror = reject;
+      reader.readAsDataURL(file);
+    });
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea_test.ts
index f7cd8936..28e22a6 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-textarea/gr-suggestion-textarea_test.ts
@@ -17,7 +17,7 @@
   stubRestApi,
   waitUntil,
 } from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {createAccountWithEmail} from '../../../test/test-data-generators';
 import {Key} from '../../../utils/dom-util';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 7daa20d..c83164e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -54,7 +54,6 @@
 
   private readonly hideHandler: (e: Event) => void;
 
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
   constructor() {
     super();
     this.windowScrollHandler = () => this._handleWindowScroll();
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
index 661515a..ba9be69 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
@@ -7,7 +7,7 @@
 import '../../../test/common-test-setup';
 import './gr-tooltip-content';
 import {GrTooltipContent} from './gr-tooltip-content';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrTooltip} from '../gr-tooltip/gr-tooltip';
 import {query} from '../../../test/test-utils';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
index 40f9d81..ff5105f 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {styleMap} from 'lit/directives/style-map.js';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
index 265ffb7..4bc9977 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
@@ -7,7 +7,7 @@
 import './gr-tooltip';
 import {GrTooltip} from './gr-tooltip';
 import {queryAndAssert} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-tooltip tests', () => {
   let element: GrTooltip;
@@ -44,21 +44,21 @@
 
   test('the correct arrow is displayed', async () => {
     assert.equal(
-      getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
+      getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')).display,
       'none'
     );
     assert.notEqual(
-      getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
+      getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')).display,
       'none'
     );
     element.positionBelow = true;
     await element.updateComplete;
     assert.notEqual(
-      getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
+      getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')).display,
       'none'
     );
     assert.equal(
-      getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
+      getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')).display,
       'none'
     );
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
index 0e267ac..bebaf1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
@@ -8,7 +8,7 @@
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
-import {customElement, state, query} from 'lit/decorators.js';
+import {customElement, query, state} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {getDocUrl} from '../../../utils/url-util';
 import {subscribe} from '../../lit/subscription-controller';
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_screenshot_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_screenshot_test.ts
new file mode 100644
index 0000000..3f2e666
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_screenshot_test.ts
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-user-suggestion-fix';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {GrUserSuggestionsFix} from './gr-user-suggestion-fix';
+import {
+  createComment,
+  createFixSuggestionInfo,
+} from '../../../test/test-data-generators';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {commentModelToken} from '../gr-comment-model/gr-comment-model';
+import {CommentModel} from '../gr-comment-model/gr-comment-model';
+import {getAppContext} from '../../../services/app-context';
+import {stubFlags, visualDiffDarkTheme} from '../../../test/test-utils';
+
+suite('gr-user-suggestion-fix screenshot tests', () => {
+  let element: GrUserSuggestionsFix;
+
+  setup(async () => {
+    stubFlags('isEnabled').returns(true);
+    const commentModel = new CommentModel(getAppContext().restApiService);
+    commentModel.updateState({
+      comment: createComment(),
+      commentedText: 'const result = data.map(item => item.value);',
+    });
+    element = (
+      await fixture<GrUserSuggestionsFix>(
+        wrapInProvider(
+          html`<gr-user-suggestion-fix
+            >const result = data.map(item =>
+            item.value).filter(Boolean);</gr-user-suggestion-fix
+          >`,
+          commentModelToken,
+          commentModel
+        )
+      )
+    ).querySelector<GrUserSuggestionsFix>('gr-user-suggestion-fix')!;
+  });
+
+  test('screenshot', async () => {
+    await element.updateComplete;
+    // mock preview because it's calculated on backend
+    element.suggestionDiffPreview!.previewLoadedFor = createFixSuggestionInfo();
+    element.suggestionDiffPreview!.preview = {
+      filepath: 'test.ts',
+      preview: {
+        meta_a: {
+          name: 'test.ts',
+          content_type: 'application/typescript',
+          lines: 6,
+        },
+        meta_b: {
+          name: 'test.ts',
+          content_type: 'application/typescript',
+          lines: 6,
+        },
+        intraline_status: 'OK',
+        change_type: 'MODIFIED',
+        content: [
+          {
+            ab: ['export class Test {'],
+          },
+          {
+            a: ['  private oldMethod() {', '    console.log("old");', '  }'],
+            b: ['  private newMethod() {', '    console.log("new");', '  }'],
+            edit_a: [
+              [24, 2],
+              [23, 2],
+              [27, 2],
+            ],
+            edit_b: [],
+          },
+          {
+            ab: ['}'],
+          },
+        ],
+      },
+    };
+    await visualDiff(element, 'gr-user-suggestion-fix');
+    await visualDiffDarkTheme(element, 'gr-user-suggestion-fix');
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
index 3200f47..5f8011f 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -5,7 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import './gr-user-suggestion-fix';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrUserSuggestionsFix} from './gr-user-suggestion-fix';
 import {
   CommentModel,
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 02bde24..6c98969 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,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../gr-tooltip-content/gr-tooltip-content';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement, TemplateResult} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {
   ApprovalInfo,
@@ -127,7 +127,7 @@
 
     return html`<gr-tooltip-content
       class="container ${this.more ? 'more' : ''}"
-      title=${this.computeTooltip()}
+      title=${this.computeTooltip(renderValue)}
       has-tooltip
     >
       <div class="vote-chip ${this.computeClass()}">${renderValue}</div>
@@ -173,12 +173,14 @@
     }
   }
 
-  private computeTooltip() {
+  private computeTooltip(renderValue: string | TemplateResult<1>) {
     if (!this.label || !isDetailedLabelInfo(this.label)) {
       return '';
     }
     const voteDescription =
-      this.label.values?.[valueString(this.vote?.value)] ?? '';
+      typeof renderValue === 'string'
+        ? this.label.values?.[renderValue] ?? ''
+        : '';
 
     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 62a1461..52870f3 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
@@ -5,7 +5,7 @@
  */
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import {getAppContext} from '../../../services/app-context';
 import './gr-vote-chip';
diff --git a/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts b/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts
index da1a283..8216b61 100644
--- a/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts
+++ b/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../gr-tooltip-content/gr-tooltip-content';
-import {LitElement, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {WebLinkInfo} from '../../../api/rest-api';
 import {ifDefined} from 'lit/directives/if-defined.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink_test.ts b/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink_test.ts
index fb93118..e1f02ac 100644
--- a/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-weblink/gr-weblink_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {html} from 'lit';
 import './gr-weblink';
 import {GrWeblink} from './gr-weblink';
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
index be7553e..00c45ae 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls-section_test.ts
@@ -15,7 +15,7 @@
 import {DiffModel, diffModelToken} from '../gr-diff-model/gr-diff-model';
 import './gr-context-controls-section';
 import {GrContextControlsSection} from './gr-context-controls-section';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-context-controls-section test', () => {
   let element: GrContextControlsSection;
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index 1ec5262e..eb65e1c 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -3,29 +3,14 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-button/paper-button';
-import '@polymer/paper-card/paper-card';
-import '@polymer/paper-checkbox/paper-checkbox';
-import '@polymer/paper-dropdown-menu/paper-dropdown-menu';
-import '@polymer/paper-fab/paper-fab';
-import '@polymer/paper-icon-button/paper-icon-button';
-import '@polymer/paper-item/paper-item';
-import '@polymer/paper-listbox/paper-listbox';
-import '@polymer/paper-tooltip/paper-tooltip';
-import {of, EMPTY, Subject} from 'rxjs';
-import {switchMap, delay} from 'rxjs/operators';
+import '../../../elements/shared/gr-tooltip-content/gr-tooltip-content';
+import {EMPTY, of, Subject} from 'rxjs';
+import {delay, switchMap} from 'rxjs/operators';
 import '../../../elements/shared/gr-button/gr-button';
 import {pluralize} from '../../../utils/string-util';
 import {fire} from '../../../utils/event-util';
 import {assertIsDefined} from '../../../utils/common-util';
-import {
-  css,
-  html,
-  LitElement,
-  nothing,
-  PropertyValues,
-  TemplateResult,
-} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {property, state} from 'lit/decorators.js';
 import {subscribe} from '../../../elements/lit/subscription-controller';
 import {
@@ -131,6 +116,11 @@
   return leftEnd < lineCountLeft && !lastGroupIsSkipped;
 }
 
+interface ContextButtonTooltip {
+  title: string;
+  positionBelow: boolean;
+}
+
 /**
  * Renders context control buttons such as "+23 lines" or "+Block". It is only
  * meant to be used to be rendered into a diff table cell of its parent
@@ -207,7 +197,7 @@
           font: var(--context-control-button-font, inherit);
         }
 
-        paper-button {
+        button {
           text-transform: none;
           align-items: center;
           background-color: var(--background-color);
@@ -220,13 +210,10 @@
           border-radius: var(--border-radius);
           padding: var(--spacing-s) var(--spacing-l);
         }
-
-        paper-button:hover {
-          /* same as defined in gr-button */
+        button:hover {
           background: rgba(0, 0, 0, 0.12);
         }
-        paper-button:focus-visible {
-          /* paper-button sets this to 0, thus preventing focus-based styling. */
+        button:focus-visible {
           outline-width: 1px;
         }
 
@@ -371,7 +358,7 @@
   private createContextButton(
     type: ContextButtonType,
     linesToExpand: number,
-    tooltip?: TemplateResult
+    tooltip?: ContextButtonTooltip
   ) {
     if (!this.group) return;
     let text = '';
@@ -449,7 +436,7 @@
       });
     };
 
-    const button = html` <paper-button
+    let button = html` <button
       class=${classes}
       aria-label=${ariaLabel}
       @click=${expandHandler}
@@ -457,8 +444,17 @@
       @mouseleave=${() => mouseHandler('leave')}
     >
       <span class="showContext">${text}</span>
-      ${tooltip}
-    </paper-button>`;
+    </button>`;
+    if (tooltip) {
+      button = html`<gr-tooltip-content
+        class="breadcrumbTooltip"
+        has-tooltip
+        title=${tooltip.title}
+        ?position-below=${tooltip.positionBelow}
+      >
+        ${button}
+      </gr-tooltip-content>`;
+    }
     return button;
   }
 
@@ -570,10 +566,11 @@
       : `${linesToExpand} common lines`;
 
     const position =
-      buttonType === ContextButtonType.BLOCK_ABOVE ? 'top' : 'bottom';
-    return html`<paper-tooltip offset="10" position=${position}
-      ><div class="breadcrumbTooltip">${tooltipText}</div></paper-tooltip
-    >`;
+      buttonType === ContextButtonType.BLOCK_ABOVE ? false : true;
+    return {
+      title: tooltipText,
+      positionBelow: position,
+    };
   }
 
   /**
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
index 74726bf..f09e077 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
@@ -8,12 +8,13 @@
 import './gr-context-controls';
 import {GrContextControls} from './gr-context-controls';
 import {SyntaxBlock} from '../../../api/diff';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {waitEventLoop} from '../../../test/test-utils';
 import {
   createContextGroup,
   createContextGroupWithDelta,
 } from '../../../test/test-data-generators';
+import {GrTooltipContent} from '../../../elements/shared/gr-tooltip-content/gr-tooltip-content';
 
 suite('gr-context-control tests', () => {
   let element: GrContextControls;
@@ -32,9 +33,7 @@
 
     await waitEventLoop();
 
-    const buttons = element.shadowRoot!.querySelectorAll(
-      'paper-button.showContext'
-    );
+    const buttons = element.shadowRoot!.querySelectorAll('button.showContext');
     assert.equal(buttons.length, 1);
     assert.equal(buttons[0].textContent!.trim(), '+10 common lines');
   });
@@ -44,9 +43,7 @@
 
     await waitEventLoop();
 
-    const buttons = element.shadowRoot!.querySelectorAll(
-      'paper-button.showContext'
-    );
+    const buttons = element.shadowRoot!.querySelectorAll('button.showContext');
 
     assert.equal(buttons.length, 2);
     assert.equal(buttons[0].textContent!.trim(), '+20 common lines');
@@ -61,9 +58,7 @@
 
     await waitEventLoop();
 
-    const buttons = element.shadowRoot!.querySelectorAll(
-      'paper-button.showContext'
-    );
+    const buttons = element.shadowRoot!.querySelectorAll('button.showContext');
 
     assert.equal(buttons.length, 3);
     assert.equal(buttons[0].textContent!.trim(), '+20 common lines');
@@ -80,9 +75,7 @@
 
     await waitEventLoop();
 
-    const buttons = element.shadowRoot!.querySelectorAll(
-      'paper-button.showContext'
-    );
+    const buttons = element.shadowRoot!.querySelectorAll('button.showContext');
 
     assert.equal(buttons.length, 2);
     assert.equal(buttons[0].textContent!.trim(), '+20 common lines');
@@ -104,13 +97,13 @@
     await waitEventLoop();
 
     const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.fullExpansion paper-button'
+      '.fullExpansion button'
     );
     const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.partialExpansion paper-button'
+      '.partialExpansion button'
     );
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
+      '.blockExpansion button'
     );
     assert.equal(fullExpansionButtons.length, 1);
     assert.equal(partialExpansionButtons.length, 1);
@@ -132,13 +125,13 @@
     await waitEventLoop();
 
     const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.fullExpansion paper-button'
+      '.fullExpansion button'
     );
     const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.partialExpansion paper-button'
+      '.partialExpansion button'
     );
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
+      '.blockExpansion button'
     );
     assert.equal(fullExpansionButtons.length, 1);
     assert.equal(partialExpansionButtons.length, 2);
@@ -168,13 +161,13 @@
     await waitEventLoop();
 
     const fullExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.fullExpansion paper-button'
+      '.fullExpansion button'
     );
     const partialExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.partialExpansion paper-button'
+      '.partialExpansion button'
     );
     const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
+      '.blockExpansion button'
     );
     assert.equal(fullExpansionButtons.length, 1);
     assert.equal(partialExpansionButtons.length, 1);
@@ -206,21 +199,12 @@
 
     await waitEventLoop();
 
-    const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
-    );
-    assert.equal(
-      blockExpansionButtons[0]
-        .querySelector('.breadcrumbTooltip')!
-        .textContent?.trim(),
-      'aSpecificFunction'
-    );
-    assert.equal(
-      blockExpansionButtons[1]
-        .querySelector('.breadcrumbTooltip')!
-        .textContent?.trim(),
-      'anotherFunction'
-    );
+    const blockExpansionButtons =
+      element.shadowRoot!.querySelectorAll<GrTooltipContent>(
+        '.blockExpansion .breadcrumbTooltip'
+      );
+    assert.equal(blockExpansionButtons[0].title?.trim(), 'aSpecificFunction');
+    assert.equal(blockExpansionButtons[1].title?.trim(), 'anotherFunction');
   });
 
   test('+Block tooltip shows nested syntax blocks as breadcrumbs', async () => {
@@ -257,13 +241,12 @@
 
     await waitEventLoop();
 
-    const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
-    );
+    const blockExpansionButtons =
+      element.shadowRoot!.querySelectorAll<GrTooltipContent>(
+        '.blockExpansion .breadcrumbTooltip'
+      );
     assert.equal(
-      blockExpansionButtons[0]
-        .querySelector('.breadcrumbTooltip')!
-        .textContent?.trim(),
+      blockExpansionButtons[0].title?.trim(),
       'aSpecificNamespace > MyClass > aMethod'
     );
   });
@@ -301,13 +284,12 @@
     element.group = createContextGroup({offset: 10, count: 20});
     await waitEventLoop();
 
-    const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
-    );
+    const blockExpansionButtons =
+      element.shadowRoot!.querySelectorAll<GrTooltipContent>(
+        '.blockExpansion .breadcrumbTooltip'
+      );
     assert.equal(
-      blockExpansionButtons[0]
-        .querySelector('.breadcrumbTooltip')!
-        .textContent?.trim(),
+      blockExpansionButtons[0].title?.trim(),
       'aSpecificNamespace > (anonymous) > aMethod'
     );
   });
@@ -318,32 +300,22 @@
     element.group = createContextGroup({offset: 10, count: 20});
     await waitEventLoop();
 
-    const blockExpansionButtons = element.shadowRoot!.querySelectorAll(
-      '.blockExpansion paper-button'
+    const tooltip = element.shadowRoot!.querySelectorAll<GrTooltipContent>(
+      '.blockExpansion .breadcrumbTooltip'
     );
-    const tooltipAbove =
-      blockExpansionButtons[0].querySelector('paper-tooltip')!;
-    const tooltipBelow =
-      blockExpansionButtons[1].querySelector('paper-tooltip')!;
-    assert.equal(
-      tooltipAbove.querySelector('.breadcrumbTooltip')!.textContent?.trim(),
-      '20 common lines'
-    );
-    assert.equal(
-      tooltipBelow.querySelector('.breadcrumbTooltip')!.textContent?.trim(),
-      '20 common lines'
-    );
-    assert.equal(tooltipAbove.getAttribute('position'), 'top');
-    assert.equal(tooltipBelow.getAttribute('position'), 'bottom');
+    const tooltipAbove = tooltip[0];
+    const tooltipBelow = tooltip[1];
+    assert.equal(tooltipAbove.title?.trim(), '20 common lines');
+    assert.equal(tooltipBelow.title?.trim(), '20 common lines');
+    assert.isNotOk(tooltipAbove.getAttribute('position-below'));
+    assert.equal(tooltipBelow.getAttribute('position-below'), '');
   });
 
   test('context control with delta group', async () => {
     element.group = createContextGroupWithDelta();
     await waitEventLoop();
 
-    const buttons = element.shadowRoot!.querySelectorAll(
-      'paper-button.showContext'
-    );
+    const buttons = element.shadowRoot!.querySelectorAll('button.showContext');
     assert.equal(buttons.length, 1);
     assert.equal(buttons[0].textContent!.trim(), '+ Unrelated changes');
     assert.include([...buttons[0].classList.values()], 'unrelatedChanges');
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
index 84e5ffe..287f4da 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-image.ts
@@ -3,7 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {ImageInfo} from '../../../types/common';
+import {Base64ImageFile} from '../../../types/common';
 import {Side} from '../../../api/diff';
 import '../gr-diff-image-viewer/gr-image-viewer';
 import {html, LitElement, nothing} from 'lit';
@@ -14,9 +14,9 @@
 const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|x-icon|jpeg|jpg|png|tiff|webp)$/;
 
 class GrDiffImageNew extends LitElement {
-  @property() baseImage?: ImageInfo;
+  @property() baseImage?: Base64ImageFile;
 
-  @property() revisionImage?: ImageInfo;
+  @property() revisionImage?: Base64ImageFile;
 
   @property() automaticBlink = false;
 
@@ -53,9 +53,9 @@
 }
 
 class GrDiffImageOld extends LitElement {
-  @property() baseImage?: ImageInfo;
+  @property() baseImage?: Base64ImageFile;
 
-  @property() revisionImage?: ImageInfo;
+  @property() revisionImage?: Base64ImageFile;
 
   @property() columnCount = 0;
 
@@ -198,8 +198,8 @@
   }
 }
 
-function imageSrc(image?: ImageInfo): string {
-  return image && IMAGE_MIME_PATTERN.test(image.type)
+function imageSrc(image?: Base64ImageFile): string {
+  return image?.type && IMAGE_MIME_PATTERN.test(image.type)
     ? `data:${image.type};base64,${image.body}`
     : '';
 }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index 1caaebd..0380d0c 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -8,13 +8,13 @@
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {createRef, Ref, ref} from 'lit/directives/ref.js';
 import {
-  DiffResponsiveMode,
-  Side,
-  LineNumber,
   DiffLayer,
-  GrDiffLineType,
-  LOST,
+  DiffResponsiveMode,
   FILE,
+  GrDiffLineType,
+  LineNumber,
+  LOST,
+  Side,
 } from '../../../api/diff';
 import {BlameInfo} from '../../../types/common';
 import {assertIsDefined} from '../../../utils/common-util';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
index 346d3fe..7e87d32 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-diff-row';
 import {GrDiffRow} from './gr-diff-row';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
 import {DiffViewMode, GrDiffLineType} from '../../../api/diff';
 import {diffModelToken} from '../gr-diff-model/gr-diff-model';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
index a189e05..b9a239a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -8,11 +8,11 @@
 import {
   DiffInfo,
   DiffLayer,
+  DiffPreferencesInfo,
   DiffViewMode,
+  LineNumber,
   RenderPreferences,
   Side,
-  LineNumber,
-  DiffPreferencesInfo,
 } from '../../../api/diff';
 import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
 import {getResponsiveMode} from '../gr-diff/gr-diff-utils';
@@ -132,7 +132,6 @@
     if (this.group.isTotal()) extras.push('total');
     if (this.group.dueToRebase) extras.push('dueToRebase');
     if (this.group.moveDetails) extras.push('dueToMove');
-    if (this.group.moveDetails?.changed) extras.push('changed');
     if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly');
 
     const pairs = this.getLinePairs();
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
index d5e471b..99b92ca 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-diff-section';
 import {GrDiffSection} from './gr-diff-section';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
 import {DiffViewMode, GrDiffLineType} from '../../../api/diff';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
index 2156a5b..c4bbb4d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
@@ -3,7 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, html, TemplateResult} from 'lit';
+import {html, LitElement, TemplateResult} from 'lit';
 import {property} from 'lit/decorators.js';
 import {styleMap} from 'lit/directives/style-map.js';
 
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
index a458a61..d26dfe6 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-diff-text';
 import {GrDiffText} from './gr-diff-text';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 const LINE_BREAK = '<span class="br"></span>';
 
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
index e1728ff..4555454 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
@@ -7,8 +7,8 @@
 import {GrDiffLine, Side, TokenHighlightListener} from '../../../api/diff';
 import {assertIsDefined} from '../../../utils/common-util';
 import {
-  GrAnnotationImpl,
   getStringLength,
+  GrAnnotationImpl,
 } from '../gr-diff-highlight/gr-annotation';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {getLineElByChild, getSideByLineEl} from '../gr-diff/gr-diff-utils';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
index 6f5674c..6cbf422 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
@@ -7,7 +7,7 @@
 import '../../../test/common-test-setup';
 import '../gr-diff/gr-diff';
 import './gr-diff-cursor';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {
   mockPromise,
   queryAll,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
index 6ee5f18..11684d3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
@@ -6,10 +6,10 @@
 import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {
-  TEST_ONLY,
   annotateElement,
   annotateWithElement,
   getStringLength,
+  TEST_ONLY,
 } from './gr-annotation';
 import {
   getSanitizeDOMValue,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
index 75d6a57..0eea837 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -393,6 +393,15 @@
       return;
     }
 
+    const range = {
+      start_line: start.line,
+      start_character: start.column,
+      end_line: end.line,
+      end_character: end.column,
+    };
+    const side = start.side;
+    this.selectedRange = {range, side};
+    this.diffBuilder?.diffModel.fireRangeSelectedEvent(side, range, isMouseUp);
     let actionBox = this.diffTable.querySelector('gr-selection-action-box');
     if (!actionBox) {
       actionBox = document.createElement('gr-selection-action-box');
@@ -403,15 +412,6 @@
     if (hoverCardText) {
       actionBox.setAttribute('hoverCardText', hoverCardText);
     }
-    this.selectedRange = {
-      range: {
-        start_line: start.line,
-        start_character: start.column,
-        end_line: end.line,
-        end_character: end.column,
-      },
-      side: start.side,
-    };
     if (start.line === end.line) {
       this.positionActionBox(actionBox, start.line, domRange);
     } else if (start.node instanceof Text) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
index e186b72..f44a251 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
@@ -7,14 +7,14 @@
 import '../../../test/common-test-setup';
 import './gr-diff-highlight';
 import {getTextOffset} from './gr-range-normalizer';
-import {fixture, fixtureCleanup, html, assert} from '@open-wc/testing';
-import {GrDiffHighlight, DiffBuilderInterface} from './gr-diff-highlight';
+import {assert, fixture, fixtureCleanup, html} from '@open-wc/testing';
+import {DiffBuilderInterface, GrDiffHighlight} from './gr-diff-highlight';
 import {Side} from '../../../api/diff';
 import {SinonStubbedMember} from 'sinon';
 import {queryAndAssert} from '../../../utils/common-util';
 import {
-  GrDiffThreadElement,
   getDataFromCommentThreadEl,
+  GrDiffThreadElement,
 } from '../gr-diff/gr-diff-utils';
 import {
   stubElement,
@@ -26,7 +26,7 @@
 
 // Splitting long lines in html into shorter rows breaks tests:
 // zero-length text nodes and new lines are not expected in some places
-/* eslint-disable max-len, lit/prefer-static-styles */
+
 /* prettier-ignore */
 const diffTable = html`
   <table id="diffTable">
@@ -125,7 +125,6 @@
     </tbody>
   </table>
 `;
-/* eslint-enable max-len */
 
 suite('gr-diff-highlight', () => {
   suite('comment events', () => {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
index e0febdc..8c63209 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -3,31 +3,30 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/paper-button/paper-button';
-import '@polymer/paper-card/paper-card';
-import '@polymer/paper-checkbox/paper-checkbox';
-import '@polymer/paper-dropdown-menu/paper-dropdown-menu';
-import '@polymer/paper-fab/paper-fab';
-import '@polymer/paper-icon-button/paper-icon-button';
+import '@material/web/button/text-button';
+import '@material/web/checkbox/checkbox';
+// Google internal screenshot tests are failing without this import.
+// We have no idea why, but for the time being we will just keep the import.
 import '@polymer/paper-item/paper-item';
-import '@polymer/paper-listbox/paper-listbox';
 import './gr-overview-image';
 import './gr-zoomed-image';
-import '../../../elements/shared/gr-icons/gr-icons';
-
+import '@material/web/labs/card/filled-card';
+import '@material/web/fab/fab';
+import '@material/web/icon/icon';
+import '@material/web/iconbutton/icon-button';
+import '@material/web/select/filled-select';
+import '@material/web/select/select-option';
 import {GrLibLoader} from '../../../elements/shared/gr-lib-loader/gr-lib-loader';
 import {RESEMBLEJS_LIBRARY_CONFIG} from '../../../elements/shared/gr-lib-loader/resemblejs_config';
-
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {classMap} from 'lit/directives/class-map.js';
 import {StyleInfo, styleMap} from 'lit/directives/style-map.js';
-
 import {Dimensions, fitToFrame, FrameConstrainer, Point, Rect} from './util';
-import {ValueChangedEvent} from '../../../types/events';
 import {fire} from '../../../utils/event-util';
 import {ImageDiffAction} from '../../../api/diff';
+import {materialStyles} from '../../../styles/gr-material-styles';
 
 const DRAG_DEAD_ZONE_PIXELS = 5;
 
@@ -152,6 +151,7 @@
 
   static override get styles() {
     return [
+      materialStyles,
       css`
         :host {
           display: grid;
@@ -195,16 +195,20 @@
         gr-zoomed-image.revision {
           border-color: var(--revision-image-border-color, rgb(170, 242, 170));
         }
+        md-icon[filled] {
+          font-variation-settings: 'FILL' 1;
+        }
         #automatic-blink-button {
           position: absolute;
           right: var(--spacing-xl);
           bottom: var(--spacing-xl);
           opacity: 0;
           transition: opacity 200ms ease;
-          --paper-fab-background: var(--primary-button-background-color);
-          --paper-fab-keyboard-focus-background: var(
+          --md-sys-color-primary-container: var(
             --primary-button-background-color
           );
+          --md-sys-color-on-primary-container: #ffffff;
+          --md-fab-container-shape: 50px;
         }
         #automatic-blink-button.show,
         #automatic-blink-button:focus-visible {
@@ -242,6 +246,9 @@
           padding: var(--spacing-xxs) var(--spacing-s);
         }
         .controls {
+          font-family: var(--header-font-family);
+          box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+            0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
           grid-column-start: 2;
           flex-grow: 0;
           display: flex;
@@ -249,20 +256,24 @@
           align-self: flex-start;
           margin: var(--spacing-m);
           padding-bottom: var(--spacing-xl);
+          --md-sys-color-surface-container-highest: var(
+            --card-surface-container-highest
+          );
         }
-        paper-button {
+        md-text-button {
           padding: var(--spacing-m);
           font: var(--image-diff-button-font);
           text-transform: var(--image-diff-button-text-transform, uppercase);
           outline: 1px solid transparent;
           border: 1px solid var(--primary-button-background-color);
+          --md-text-button-container-shape: 0;
         }
-        paper-button.unelevated {
-          color: var(--primary-button-text-color);
+        md-text-button.unelevated {
           background-color: var(--primary-button-background-color);
+          --md-sys-color-primary: var(--primary-button-text-color);
         }
-        paper-button.outlined {
-          color: var(--primary-button-background-color);
+        md-text-button.outlined {
+          --md-sys-color-primary: var(--primary-button-background-color);
         }
         #version-switcher {
           display: flex;
@@ -271,26 +282,32 @@
           /* Start a stacking context to contain FAB below. */
           z-index: 0;
         }
-        #version-switcher paper-button {
+        #version-switcher md-text-button {
           flex-grow: 1;
           margin: 0;
           /*
-        The floating action button below overlaps part of the version buttons.
-        This min-width ensures the button text still appears somewhat balanced.
-        */
+            The floating action button below overlaps part of the version buttons.
+            This min-width ensures the button text still appears somewhat balanced.
+          */
           min-width: 7rem;
         }
-        #version-switcher paper-fab {
+        #version-switcher md-fab {
           /* Round button overlaps Base and Revision buttons. */
           z-index: 1;
           margin: 0 -12px;
           /* Styled as an outlined button. */
           color: var(--primary-button-background-color);
           border: 1px solid var(--primary-button-background-color);
-          --paper-fab-background: var(--primary-background-color);
-          --paper-fab-keyboard-focus-background: var(
-            --primary-background-color
+          border-radius: 50%;
+          box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+            0 1px 5px 0 rgba(0, 0, 0, 0.12);
+          --md-sys-color-primary-container: var(--primary-background-color);
+          --md-sys-color-on-primary-container: var(
+            --primary-button-background-color
           );
+          --md-fab-container-shape: 50px;
+          --md-fab-container-height: 40px;
+          --md-fab-container-width: 40px;
         }
         #version-explanation {
           color: var(--deemphasized-text-color);
@@ -306,13 +323,23 @@
           margin-top: var(--spacing-m);
         }
         #zoom-control {
-          margin: 0 var(--spacing-xl);
+          margin: var(--spacing-m) var(--spacing-xl) 0;
         }
-        paper-item {
+        md-filled-select::part(field) {
           cursor: pointer;
         }
-        paper-item:hover {
-          background-color: var(--hover-background-color);
+        md-filled-select {
+          --md-sys-color-surface-container: var(--select-surface-container);
+          --md-sys-color-surface-container-highest: var(
+            --select-surface-container-highest
+          );
+          --md-sys-color-on-surface: var(--select-on-surface);
+          --md-sys-color-on-surface-variant: var(--select-on-surface-variant);
+          --md-sys-color-primary: var(--select-primary);
+          --md-sys-color-secondary-container: var(--select-secondary-container);
+          --md-sys-color-on-secondary-container: var(
+            --select-on-secondary-container
+          );
         }
         #follow-mouse {
           margin: var(--spacing-m) var(--spacing-xl);
@@ -345,6 +372,7 @@
           border-color: var(--outline-color-focus);
         }
         .color-picker-button .color {
+          display: flex;
           border: 1px solid var(--border-color);
           border-radius: 50%;
           width: 100%;
@@ -359,6 +387,11 @@
           top: 0;
           left: 0;
         }
+        .highlight-changes,
+        .follow-mouse {
+          display: flex;
+          align-items: center;
+        }
       `,
     ];
   }
@@ -373,11 +406,12 @@
           selected,
         })}
       >
-        <paper-icon-button
+        <md-icon-button
           class="color"
+          touch-target="none"
           style=${styleMap({backgroundColor: color})}
           @click=${colorPicked}
-        ></paper-icon-button>
+        ></md-icon-button>
       </div>
     `;
   }
@@ -390,11 +424,11 @@
           selected: this.checkerboardSelected,
         })}
       >
-        <paper-icon-button
+        <md-icon-button
           class="color checkerboard"
+          touch-target="none"
           @click=${this.pickCheckerboard}
-        >
-        </paper-icon-button>
+        ></md-icon-button>
       </div>
     `;
   }
@@ -454,24 +488,31 @@
     };
     const versionToggle = html`
       <div id="version-switcher">
-        <paper-button class=${classMap(leftClasses)} @click=${this.selectBase}>
+        <md-text-button
+          class=${classMap(leftClasses)}
+          touch-target="none"
+          @click=${this.selectBase}
+        >
           Base
-        </paper-button>
-        <paper-fab
+        </md-text-button>
+        <md-fab
           mini
-          icon="gr-icons:swapHoriz"
           title=${this.baseSelected
             ? 'switch to Revision version'
             : 'switch to Base version'}
+          variant="primary"
+          touch-target="none"
           @click=${this.manualBlink}
         >
-        </paper-fab>
-        <paper-button
+          <md-icon slot="icon" filled>swap_horiz</md-icon>
+        </md-fab>
+        <md-text-button
           class=${classMap(rightClasses)}
+          touch-target="none"
           @click=${this.selectRevision}
         >
           Revision
-        </paper-button>
+        </md-text-button>
       </div>
     `;
 
@@ -481,13 +522,15 @@
 
     const highlightSwitcher = this.diffHighlightSrc
       ? html`
-          <paper-checkbox
-            id="highlight-changes"
-            ?checked=${this.showHighlight}
-            @change=${this.showHighlightChanged}
-          >
+          <label class="highlight-changes">
+            <md-checkbox
+              id="highlight-changes"
+              ?checked=${this.showHighlight}
+              @change=${this.showHighlightChanged}
+            >
+            </md-checkbox>
             Highlight differences
-          </paper-checkbox>
+          </label>
         `
       : '';
 
@@ -509,32 +552,36 @@
     `;
 
     const zoomControl = html`
-      <paper-dropdown-menu id="zoom-control" label="Zoom">
-        <paper-listbox
-          slot="dropdown-content"
-          selected="fit"
-          .attrForSelected=${'value'}
-          @selected-changed=${this.zoomControlChanged}
-        >
-          ${this.zoomLevels.map(
-            zoomLevel => html`
-              <paper-item value=${zoomLevel}>
+      <md-filled-select
+        id="zoom-control"
+        label="Zoom"
+        @change=${this.zoomControlChanged}
+      >
+        ${this.zoomLevels.map(
+          zoomLevel => html`
+            <md-select-option
+              ?selected=${zoomLevel === 'fit'}
+              value=${zoomLevel}
+            >
+              <div slot="headline">
                 ${zoomLevel === 'fit' ? 'Fit' : `${zoomLevel * 100}%`}
-              </paper-item>
-            `
-          )}
-        </paper-listbox>
-      </paper-dropdown-menu>
+              </div>
+            </md-select-option>
+          `
+        )}
+      </md-filled-select>
     `;
 
     const followMouse = html`
-      <paper-checkbox
-        id="follow-mouse"
-        ?checked=${this.followMouse}
-        @change=${this.followMouseChanged}
-      >
+      <label class="follow-mouse">
+        <md-checkbox
+          id="follow-mouse"
+          ?checked=${this.followMouse}
+          @change=${this.followMouseChanged}
+        >
+        </md-checkbox>
         Magnifier follows mouse
-      </paper-checkbox>
+      </label>
     `;
 
     const backgroundPicker = html`
@@ -571,41 +618,21 @@
     `;
 
     const automaticBlink = html`
-      <paper-fab
+      <md-fab
         id="automatic-blink-button"
         class=${classMap({show: this.automaticBlinkShown})}
         title="Automatic blink"
-        icon="gr-icons:${this.automaticBlink ? 'pause' : 'playArrow'}"
+        variant="primary"
+        touch-target="none"
         @click=${this.toggleAutomaticBlink}
       >
-      </paper-fab>
-    `;
-
-    // To pass CSS mixins for @apply to Polymer components, they need to appear
-    // in <style> inside the template.
-    /* eslint-disable lit/prefer-static-styles */
-    const customStyle = html`
-      <style>
-        /* prettier formatter removes semi-colons after css mixins. */
-        /* prettier-ignore */
-        paper-item {
-          --paper-item-min-height: 48;
-          --paper-item: {
-            min-height: 48px;
-            padding: 0 var(--spacing-xl);
-          };
-          --paper-item-focused-before: {
-            background-color: var(--selection-background-color);
-          };
-          --paper-item-focused: {
-            background-color: var(--selection-background-color);
-          };
-        }
-      </style>
+        <md-icon slot="icon" filled
+          >${this.automaticBlink ? 'pause' : 'play_arrow'}</md-icon
+        >
+      </md-fab>
     `;
 
     return html`
-      ${customStyle}
       <div
         class="imageArea"
         @mousemove=${this.mousemoveImageArea}
@@ -637,10 +664,10 @@
         ${this.imageSize.width} x ${this.imageSize.height}
       </div>
 
-      <paper-card class="controls">
+      <md-filled-card class="controls">
         ${versionSwitcher} ${highlightSwitcher} ${overviewImage} ${zoomControl}
         ${!this.scaledSelected ? followMouse : ''} ${backgroundPicker}
-      </paper-card>
+      </md-filled-card>
     `;
   }
 
@@ -655,9 +682,8 @@
   // We don't want property changes in updateSizes() to trigger infinite update
   // loops, so we perform this in update() instead of updated().
   override update(changedProperties: PropertyValues) {
-    // eslint-disable-next-line lit/no-property-change-update
     if (!this.baseUrl) this.baseSelected = false;
-    // eslint-disable-next-line lit/no-property-change-update
+
     if (!this.revisionUrl) this.baseSelected = true;
     this.updateSizes();
     super.update(changedProperties);
@@ -767,8 +793,8 @@
     });
   }
 
-  zoomControlChanged(event: ValueChangedEvent<string>) {
-    const scaleString = event.detail.value;
+  zoomControlChanged(event: Event) {
+    const scaleString = (event.target as HTMLSelectElement).value;
     if (!scaleString) return;
     if (scaleString === 'fit') {
       this.scaledSelected = true;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer_test.ts
new file mode 100644
index 0000000..2ea26af
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer_test.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-image-viewer';
+import {assert, fixture, html} from '@open-wc/testing';
+import {GrImageViewer} from './gr-image-viewer';
+
+suite('gr-image-viewer tests', () => {
+  let element: GrImageViewer;
+
+  setup(async () => {
+    element = await fixture<GrImageViewer>(
+      html`<gr-image-viewer></gr-image-viewer>`
+    );
+    await element.updateComplete;
+  });
+
+  test('renders', () => {
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <div class="imageArea">
+          <gr-zoomed-image class="base" style="cursor:pointer;">
+            <div id="source-plus-highlight-container">
+              <img
+                class="checkerboard"
+                id="source-image"
+                src=""
+                style="background-color:;"
+              />
+              <img
+                id="highlight-image"
+                style="opacity:0;pointer-events:none;"
+              />
+            </div>
+          </gr-zoomed-image>
+          <div id="spacer" style="width:0px;height:0px;"></div>
+        </div>
+        <div class="dimensions">0 x 0</div>
+        <md-filled-card class="controls">
+          <div id="version-explanation">This file is being deleted.</div>
+          <gr-overview-image>
+            <img class="checkerboard" src="" style="background-color:;" />
+          </gr-overview-image>
+          <md-filled-select id="zoom-control" label="Zoom">
+            <md-select-option
+              data-aria-selected="true"
+              md-menu-item=""
+              selected=""
+              tabindex="0"
+              value="fit"
+            >
+              <div slot="headline">Fit</div>
+            </md-select-option>
+            <md-select-option md-menu-item="" tabindex="-1" value="1">
+              <div slot="headline">100%</div>
+            </md-select-option>
+            <md-select-option md-menu-item="" tabindex="-1" value="1.25">
+              <div slot="headline">125%</div>
+            </md-select-option>
+            <md-select-option md-menu-item="" tabindex="-1" value="1.5">
+              <div slot="headline">150%</div>
+            </md-select-option>
+            <md-select-option md-menu-item="" tabindex="-1" value="1.75">
+              <div slot="headline">175%</div>
+            </md-select-option>
+            <md-select-option md-menu-item="" tabindex="-1" value="2">
+              <div slot="headline">200%</div>
+            </md-select-option>
+          </md-filled-select>
+          <div class="color-picker">
+            <div class="label">Background</div>
+            <div class="options">
+              <div class="color-picker-button selected">
+                <md-icon-button
+                  class="checkerboard color"
+                  touch-target="none"
+                  value=""
+                >
+                </md-icon-button>
+              </div>
+              <div class="color-picker-button">
+                <md-icon-button
+                  class="color"
+                  style="background-color:#fff;"
+                  touch-target="none"
+                  value=""
+                >
+                </md-icon-button>
+              </div>
+              <div class="color-picker-button">
+                <md-icon-button
+                  class="color"
+                  style="background-color:#000;"
+                  touch-target="none"
+                  value=""
+                >
+                </md-icon-button>
+              </div>
+              <div class="color-picker-button">
+                <md-icon-button
+                  class="color"
+                  style="background-color:#aaa;"
+                  touch-target="none"
+                  value=""
+                >
+                </md-icon-button>
+              </div>
+            </div>
+          </div>
+        </md-filled-card>
+      `
+    );
+  });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
index c802c0c..0673621 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
@@ -3,7 +3,7 @@
  * Copyright 2023 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {Observable, combineLatest} from 'rxjs';
+import {combineLatest, Observable} from 'rxjs';
 import {debounceTime, filter, map, withLatestFrom} from 'rxjs/operators';
 import {
   CreateCommentEventDetail,
@@ -16,6 +16,7 @@
   DisplayLine,
   LineNumber,
   LineSelectedEventDetail,
+  RangeSelectedEventDetail,
   RenderPreferences,
   Side,
   SyntaxBlock,
@@ -24,13 +25,13 @@
 import {Model} from '../../../models/base/model';
 import {select} from '../../../utils/observable-util';
 import {
-  FullContext,
-  GrDiffCommentThread,
-  KeyLocations,
   computeContext,
   computeKeyLocations,
   computeLineLength,
+  FullContext,
   getResponsiveMode,
+  GrDiffCommentThread,
+  KeyLocations,
 } from '../gr-diff/gr-diff-utils';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
 import {
@@ -40,14 +41,14 @@
 import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
 import {assert} from '../../../utils/common-util';
 import {countLines, isImageDiff} from '../../../utils/diff-util';
-import {BlameInfo, ImageInfo} from '../../../types/common';
+import {Base64ImageFile, BlameInfo} from '../../../types/common';
 import {fire} from '../../../utils/event-util';
 import {CommentRange} from '../../../api/rest-api';
 
 export interface DiffState {
   diff?: DiffInfo;
-  baseImage?: ImageInfo;
-  revisionImage?: ImageInfo;
+  baseImage?: Base64ImageFile;
+  revisionImage?: Base64ImageFile;
   path?: string;
   renderPrefs: RenderPreferences;
   diffPrefs: DiffPreferencesInfo;
@@ -100,12 +101,12 @@
     countLines(diff, Side.LEFT)
   );
 
-  readonly baseImage$: Observable<ImageInfo | undefined> = select(
+  readonly baseImage$: Observable<Base64ImageFile | undefined> = select(
     this.state$,
     diffState => diffState.baseImage
   );
 
-  readonly revisionImage$: Observable<ImageInfo | undefined> = select(
+  readonly revisionImage$: Observable<Base64ImageFile | undefined> = select(
     this.state$,
     diffState => diffState.revisionImage
   );
@@ -315,4 +316,14 @@
     };
     fire(this.eventTarget, 'create-comment', detail);
   }
+
+  fireRangeSelectedEvent(side: Side, range: CommentRange, isMouseUp: boolean) {
+    const detail: RangeSelectedEventDetail = {
+      side,
+      lineNum: range.end_line,
+      range,
+      isMouseUp,
+    };
+    fire(this.eventTarget, 'range-selected', detail);
+  }
 }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index 7cc0ebd..748c5ea 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -18,7 +18,7 @@
   getSideByLineEl,
   isThreadEl,
 } from '../gr-diff/gr-diff-utils';
-import {getDiffLines, getContentFromDiff} from '../../../utils/diff-util';
+import {getContentFromDiff, getDiffLines} from '../../../utils/diff-util';
 import {fire} from '../../../utils/event-util';
 
 /**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
index b374596..fa0465d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
@@ -11,7 +11,7 @@
 import {GrDiffSelection} from './gr-diff-selection';
 import {createDiff} from '../../../test/test-data-generators';
 import {DiffInfo, Side} from '../../../api/diff';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {mouseDown} from '../../../test/test-utils';
 import {GrDiff} from '../gr-diff/gr-diff';
 import {waitForEventOnce} from '../../../utils/event-util';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts
index b6c4c0b..92c0291 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element.ts
@@ -14,16 +14,16 @@
 import '../gr-diff-builder/gr-diff-builder-image';
 import '../gr-diff-builder/gr-diff-section';
 import '../gr-diff-builder/gr-diff-row';
-import {isResponsive, FullContext, FULL_CONTEXT} from './gr-diff-utils';
-import {ImageInfo} from '../../../types/common';
+import {FULL_CONTEXT, FullContext, isResponsive} from './gr-diff-utils';
+import {Base64ImageFile} from '../../../types/common';
 import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
 import {
+  createDefaultDiffPrefs,
   DiffViewMode,
   Side,
-  createDefaultDiffPrefs,
 } from '../../../constants/constants';
 import {fire} from '../../../utils/event-util';
-import {RenderPreferences, LOST, DiffResponsiveMode} from '../../../api/diff';
+import {DiffResponsiveMode, LOST, RenderPreferences} from '../../../api/diff';
 import {property, query, queryAll, state} from 'lit/decorators.js';
 import {html, LitElement, nothing} from 'lit';
 import {when} from 'lit/directives/when.js';
@@ -31,8 +31,8 @@
 import {expandFileMode} from '../../../utils/file-util';
 import {
   ColumnsToShow,
-  NO_COLUMNS,
   diffModelToken,
+  NO_COLUMNS,
 } from '../gr-diff-model/gr-diff-model';
 import {resolve} from '../../../models/dependency';
 import {getDiffLength, isImageDiff} from '../../../utils/diff-util';
@@ -53,9 +53,9 @@
 
   @state() diff?: DiffInfo;
 
-  @state() baseImage?: ImageInfo;
+  @state() baseImage?: Base64ImageFile;
 
-  @state() revisionImage?: ImageInfo;
+  @state() revisionImage?: Base64ImageFile;
 
   @state() diffPrefs: DiffPreferencesInfo = createDefaultDiffPrefs();
 
@@ -224,7 +224,7 @@
         <table
           id="diffTable"
           class=${classMap(tableClasses)}
-          ?contenteditable=${isContentEditable}
+          contenteditable=${isContentEditable ? 'true' : 'false'}
         >
           ${this.renderColumns()}
           ${when(!this.showWarning(), () =>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
index c9f4882..f1d5a8d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
@@ -8,7 +8,6 @@
 import './gr-diff-element';
 import {GrDiffElement} from './gr-diff-element';
 import {querySelectorAll} from '../../../utils/dom-util';
-import '@polymer/paper-button/paper-button';
 import {
   DiffContent,
   DiffInfo,
@@ -18,8 +17,8 @@
 } from '../../../api/diff';
 import {query, queryAndAssert, waitUntil} from '../../../test/test-utils';
 import {waitForEventOnce} from '../../../utils/event-util';
-import {ImageInfo} from '../../../types/common';
-import {fixture, html, assert} from '@open-wc/testing';
+import {Base64ImageFile} from '../../../types/common';
+import {assert, fixture, html} from '@open-wc/testing';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
 import {DiffModel, diffModelToken} from '../gr-diff-model/gr-diff-model';
 import {wrapInProvider} from '../../../models/di-provider-element';
@@ -58,7 +57,7 @@
         element,
         /* HTML */ `
           <div class="diffContainer sideBySide">
-            <table id="diffTable">
+            <table id="diffTable" contenteditable="false">
               <colgroup>
                 <col class="left" width="48" />
                 <col class="left" />
@@ -83,7 +82,7 @@
         element,
         /* HTML */ `
           <div class="diffContainer unified">
-            <table id="diffTable">
+            <table id="diffTable" contenteditable="false">
               <colgroup>
                 <col class="left" width="48" />
                 <col class="right" width="48" />
@@ -1271,7 +1270,7 @@
         element,
         /* HTML */ `
           <div class="diffContainer sideBySide">
-            <table id="diffTable">
+            <table id="diffTable" contenteditable="false">
               <colgroup>
                 <col class="left" width="48" />
                 <col class="left" />
@@ -2760,7 +2759,7 @@
             <div class="diffContainer sideBySide">
               <gr-diff-section class="left-FILE right-FILE"> </gr-diff-section>
               <gr-diff-row class="left-FILE right-FILE"> </gr-diff-row>
-              <table id="diffTable">
+              <table id="diffTable" contenteditable="false">
                 <colgroup>
                   <col class="left" width="48" />
                   <col class="left" />
@@ -2816,8 +2815,8 @@
     });
 
     suite('image diffs', () => {
-      let mockFile1: ImageInfo;
-      let mockFile2: ImageInfo;
+      let mockFile1: Base64ImageFile;
+      let mockFile2: Base64ImageFile;
       setup(() => {
         mockFile1 = {
           body:
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index e7df002..ccd208c 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -5,7 +5,7 @@
  */
 import {BLANK_LINE, GrDiffLine} from './gr-diff-line';
 import {GrDiffLineType, LineNumber, LineRange, Side} from '../../../api/diff';
-import {assertIsDefined, assert} from '../../../utils/common-util';
+import {assert, assertIsDefined} from '../../../utils/common-util';
 import {isDefined} from '../../../types/types';
 
 export enum GrDiffGroupType {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
index c8446f0..30420a7 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {GrDiffLine, BLANK_LINE} from './gr-diff-line';
+import {BLANK_LINE, GrDiffLine} from './gr-diff-line';
 import {
   GrDiffGroup,
   GrDiffGroupType,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
index 40ce039..d432e2a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
@@ -526,21 +526,6 @@
     background-color: var(--diff-moved-in-background);
   }
 
-  gr-diff-section tbody.dueToMove.changed gr-diff-row .sign.add,
-  gr-diff-section
-    tbody.dueToMove.changed
-    gr-diff-row
-    td.content.add
-    div.contentText,
-  gr-diff-section tbody.dueToMove.changed tr.moveControls.movedIn .sign.right,
-  gr-diff-section tbody.dueToMove.changed tr.moveControls.movedIn td.moveHeader,
-  gr-diff-section
-    tbody.delta.total.dueToMove.changed
-    td.content.add
-    div.contentText {
-    background-color: var(--diff-moved-in-changed-background);
-  }
-
   gr-diff-section tbody.dueToMove .sign.remove,
   gr-diff-section tbody.dueToMove td.content.remove div.contentText,
   gr-diff-section tbody.dueToMove tr.moveControls.movedOut td.moveHeader,
@@ -555,9 +540,6 @@
   gr-diff-section tbody.delta.dueToMove tr.movedIn td.moveHeader {
     --gr-range-header-color: var(--diff-moved-in-label-color);
   }
-  gr-diff-section tbody.delta.dueToMove.changed tr.movedIn td.moveHeader {
-    --gr-range-header-color: var(--diff-moved-in-changed-label-color);
-  }
   gr-diff-section tbody.delta.dueToMove tr.movedOut td.moveHeader {
     --gr-range-header-color: var(--diff-moved-out-label-color);
   }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index c4aaa2d..1423efb 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -11,8 +11,8 @@
   DiffResponsiveMode,
   DisplayLine,
   FILE,
-  LOST,
   LineNumber,
+  LOST,
   RenderPreferences,
 } from '../../../api/diff';
 import {GrDiffGroup} from './gr-diff-group';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 07aeaf0..2f5b077 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -6,16 +6,16 @@
 import {assert} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {
-  getRange,
-  computeKeyLocations,
-  GrDiffCommentThread,
-  getDataFromCommentThreadEl,
   compareComments,
-  GrDiffThreadElement,
   computeContext,
+  computeKeyLocations,
+  computeLineLength,
   FULL_CONTEXT,
   FullContext,
-  computeLineLength,
+  getDataFromCommentThreadEl,
+  getRange,
+  GrDiffCommentThread,
+  GrDiffThreadElement,
 } from './gr-diff-utils';
 import {FILE, LOST, Side} from '../../../api/diff';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index 54c567a..1fb0664 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -16,18 +16,18 @@
 import './gr-diff-element';
 import '../gr-diff-builder/gr-diff-row';
 import {
-  getLineNumber,
-  isThreadEl,
-  getResponsiveMode,
-  isResponsive,
-  getSideByLineEl,
   compareComments,
-  getDataFromCommentThreadEl,
-  FullContext,
   DiffContextExpandedEventDetail,
+  FullContext,
+  getDataFromCommentThreadEl,
+  getLineNumber,
+  getResponsiveMode,
+  getSideByLineEl,
   GrDiffCommentThread,
+  isResponsive,
+  isThreadEl,
 } from '../gr-diff/gr-diff-utils';
-import {BlameInfo, ImageInfo} from '../../../types/common';
+import {Base64ImageFile, BlameInfo} from '../../../types/common';
 import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
 import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
 import {CoverageRange, DiffLayer, isDefined} from '../../../types/types';
@@ -40,16 +40,16 @@
 import {MovedLinkClickedEvent, ValueChangedEvent} from '../../../types/events';
 import {AbortStop} from '../../../api/core';
 import {
-  RenderPreferences,
-  GrDiff as GrDiffApi,
-  DisplayLine,
-  DiffRangesToFocus,
-  FileRangeSelection,
-  LineNumber,
-  ContentLoadNeededEventDetail,
-  DiffContextExpandedExternalDetail,
-  CopyInfoEventDetail,
   CommentRangeLayer,
+  ContentLoadNeededEventDetail,
+  CopyInfoEventDetail,
+  DiffContextExpandedExternalDetail,
+  DiffRangesToFocus,
+  DisplayLine,
+  FileRangeSelection,
+  GrDiff as GrDiffApi,
+  LineNumber,
+  RenderPreferences,
 } from '../../../api/diff';
 import {getShadowOrDocumentSelection} from '../../../utils/dom-util';
 import {assertIsDefined} from '../../../utils/common-util';
@@ -59,7 +59,6 @@
 import {html, LitElement, PropertyValues} from 'lit';
 import {grSyntaxTheme} from '../gr-syntax-themes/gr-syntax-theme';
 import {grRangedCommentTheme} from '../gr-ranged-comment-themes/gr-ranged-comment-theme';
-import {iconStyles} from '../../../styles/gr-icon-styles';
 import {DiffModel, diffModelToken} from '../gr-diff-model/gr-diff-model';
 import {provide} from '../../../models/dependency';
 import {
@@ -79,8 +78,8 @@
 import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
 import {GrFocusLayer} from '../gr-focus-layer/gr-focus-layer';
 import {
-  GrAnnotationImpl,
   getStringLength,
+  GrAnnotationImpl,
 } from '../gr-diff-highlight/gr-annotation';
 import {
   GrDiffGroup,
@@ -224,10 +223,10 @@
   diff?: DiffInfo;
 
   @property({type: Object})
-  baseImage?: ImageInfo;
+  baseImage?: Base64ImageFile;
 
   @property({type: Object})
-  revisionImage?: ImageInfo;
+  revisionImage?: Base64ImageFile;
 
   @property({type: String})
   errorMessage: string | null = null;
@@ -296,7 +295,6 @@
 
   static override get styles() {
     return [
-      iconStyles,
       sharedStyles,
       grSyntaxTheme,
       grRangedCommentTheme,
@@ -369,6 +367,15 @@
   }
 
   protected override willUpdate(changedProperties: PropertyValues<this>): void {
+    if (changedProperties.has('groups')) {
+      if (this.groups?.length > 0) {
+        this.loading = false;
+      }
+    }
+    if (changedProperties.has('diff')) {
+      this.loading = true;
+    }
+
     if (
       changedProperties.has('diff') ||
       changedProperties.has('path') ||
@@ -474,11 +481,6 @@
       // diffChanged relies on diffElement having been rendered.
       this.diffChanged();
     }
-    if (changedProperties.has('groups')) {
-      if (this.groups?.length > 0) {
-        this.loading = false;
-      }
-    }
   }
 
   override render() {
@@ -690,7 +692,6 @@
   }
 
   private diffChanged() {
-    this.loading = true;
     if (this.diff && this.diffElement) {
       this.diffSelection.init(this.diff, this.diffElement);
       this.highlights.init(this.diffElement, this);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index ffb5c90..502d239 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -12,7 +12,6 @@
 } from '../../../test/test-data-generators';
 import './gr-diff';
 import {getComputedStyleValue} from '../../../utils/dom-util';
-import '@polymer/paper-button/paper-button';
 import {
   DiffContent,
   DiffLayer,
@@ -33,11 +32,11 @@
 import {AbortStop} from '../../../api/core';
 import {GrDiff} from './gr-diff';
 import {GrRangedCommentHint} from '../gr-ranged-comment-hint/gr-ranged-comment-hint';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
 import {
-  GrAnnotationImpl,
   getStringLength,
+  GrAnnotationImpl,
 } from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLine} from './gr-diff-line';
 import {testResolver} from '../../../test/common-test-setup';
@@ -47,7 +46,7 @@
 
 suite('gr-diff a11y test', () => {
   test('audit', async () => {
-    assert.isAccessible(await fixture(html`<gr-diff></gr-diff>`));
+    await assert.isAccessible(await fixture(html`<gr-diff></gr-diff>`));
   });
 });
 
@@ -859,13 +858,13 @@
           '.showContext'
         )[0];
       assert.isOk(topExpandCommonButton);
-      assert.include(topExpandCommonButton!.textContent, '+9 common lines');
+      assert.include(topExpandCommonButton.textContent, '+9 common lines');
       let diffRows = queryAll(element, '.diff-row');
       // 5 lines:
       // FILE, LOST, the changed line plus one line of context in each direction
       assert.equal(diffRows.length, 5);
 
-      topExpandCommonButton!.click();
+      topExpandCommonButton.click();
 
       await waitUntil(() => {
         diffRows = queryAll(element, '.diff-row');
diff --git a/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts
index 9687ee8..014d06b 100644
--- a/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts
@@ -9,7 +9,7 @@
 import {assert} from '@open-wc/testing';
 import {SinonStub} from 'sinon';
 
-import {DiffRangesToFocus, Side, GrDiffLineType} from '../../../api/diff';
+import {DiffRangesToFocus, GrDiffLineType, Side} from '../../../api/diff';
 
 import {GrFocusLayer} from './gr-focus-layer';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
diff --git a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts
index 5d1eaa6..c1f1d52 100644
--- a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../elements/shared/gr-icon/gr-icon';
-import {LitElement, css, html} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 
 /**
@@ -49,7 +49,7 @@
       <gr-icon
         class="icon"
         icon=${icon}
-        ?filled=${this.filled}
+        ?filled=${!!this.filled}
         aria-hidden="true"
       ></gr-icon>
       <slot></slot>
diff --git a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts
index b31197d..6279043 100644
--- a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-range-header';
 import {GrRangeHeader} from './gr-range-header';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-range-header test', () => {
   let element: GrRangeHeader;
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
index 906c130..6da5079 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
@@ -5,7 +5,7 @@
  */
 import '../gr-range-header/gr-range-header';
 import {CommentRange} from '../../../types/common';
-import {LitElement, css, html, nothing} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
 import {customElement, property} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grRangedCommentTheme} from '../gr-ranged-comment-themes/gr-ranged-comment-theme';
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint_test.ts
index 87ef3f8..47b254b 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint_test.ts
@@ -9,7 +9,7 @@
 import {GrRangedCommentHint} from './gr-ranged-comment-hint';
 import {queryAndAssert, waitEventLoop} from '../../../test/test-utils';
 import {GrRangeHeader} from '../gr-range-header/gr-range-header';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-ranged-comment-hint tests', () => {
   let element: GrRangedCommentHint;
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index 10c1283..1e68414 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -10,7 +10,7 @@
 import {CommentRange} from '../../../types/common';
 import {DiffLayer, DiffLayerListener} from '../../../types/types';
 import {isLongCommentRange} from '../gr-diff/gr-diff-utils';
-import {GrDiffLineType, CommentRangeLayer} from '../../../api/diff';
+import {CommentRangeLayer, GrDiffLineType} from '../../../api/diff';
 import {rangeId} from '../../../utils/comment-util';
 
 /** Can be used for array functions like `some()`. */
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
index bb00cc5..20c3bee 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -9,7 +9,7 @@
 import './gr-ranged-comment-layer';
 import {GrRangedCommentLayer} from './gr-ranged-comment-layer';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
-import {GrDiffLineType, Side, CommentRangeLayer} from '../../../api/diff';
+import {CommentRangeLayer, GrDiffLineType, Side} from '../../../api/diff';
 import {SinonStub} from 'sinon';
 import {assert} from '@open-wc/testing';
 import {GrAnnotationImpl} from '../gr-diff-highlight/gr-annotation';
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
index 9f08149..2e73565 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -16,6 +16,9 @@
   interface HTMLElementEventMap {
     /** Fired when the comment creation action was taken (click). */
     'create-comment-requested': CustomEvent<{}>;
+
+    /** Fired when the selection action box is visible. */
+    'selection-action-box-visible': CustomEvent<{}>;
   }
 }
 
@@ -96,6 +99,7 @@
       rect.left - parentRect.left + (rect.width - boxRect.width) / 2
     }px`;
     this.invisible = false;
+    fire(this, 'selection-action-box-visible', {});
   }
 
   async placeBelow(el: Text | Element | Range) {
@@ -112,6 +116,7 @@
       rect.left - parentRect.left + (rect.width - boxRect.width) / 2
     }px`;
     this.invisible = false;
+    fire(this, 'selection-action-box-visible', {});
   }
 
   private getParentBoundingClientRect() {
diff --git a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
index 9155e53..2dd18df 100644
--- a/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-selection-action-box/gr-selection-action-box_test.ts
@@ -8,7 +8,7 @@
 import './gr-selection-action-box';
 import {GrSelectionActionBox} from './gr-selection-action-box';
 import {queryAndAssert} from '../../../test/test-utils';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 suite('gr-selection-action-box', () => {
   let container: HTMLDivElement;
@@ -103,7 +103,7 @@
         } as DOMRect);
       assert.isOk(element.tooltip);
       sinon
-        .stub(element.tooltip!, 'getBoundingClientRect')
+        .stub(element.tooltip, 'getBoundingClientRect')
         .returns({width: 10, height: 10} as DOMRect);
     });
 
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index 82f850dc..e08a9f0 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -50,6 +50,7 @@
   ['text/x-fsharp', 'fsharp'],
   ['text/x-gfm', 'markdown'],
   ['text/x-gherkin', 'gherkin'],
+  ['text/x-gn', 'gn'],
   ['text/x-go', 'go'],
   ['text/x-groovy', 'groovy'],
   ['text/x-haml', 'haml'],
diff --git a/polygerrit-ui/app/embed/gr-textarea.ts b/polygerrit-ui/app/embed/gr-textarea.ts
index 9136034..7417687 100644
--- a/polygerrit-ui/app/embed/gr-textarea.ts
+++ b/polygerrit-ui/app/embed/gr-textarea.ts
@@ -3,16 +3,16 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {LitElement, html, css} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {customElement, property, query, queryAsync} from 'lit/decorators.js';
 import {classMap} from 'lit/directives/class-map.js';
 import {ifDefined} from 'lit/directives/if-defined.js';
 import {
+  CursorPositionChangeEventDetail,
   GrTextarea as GrTextareaApi,
   HintAppliedEventDetail,
-  HintShownEventDetail,
   HintDismissedEventDetail,
-  CursorPositionChangeEventDetail,
+  HintShownEventDetail,
 } from '../api/embed';
 import {isSafari} from '../utils/dom-util';
 
diff --git a/polygerrit-ui/app/embed/gr-textarea_test.ts b/polygerrit-ui/app/embed/gr-textarea_test.ts
index 577ee71..b1c654c 100644
--- a/polygerrit-ui/app/embed/gr-textarea_test.ts
+++ b/polygerrit-ui/app/embed/gr-textarea_test.ts
@@ -5,7 +5,7 @@
  */
 import '../test/common-test-setup';
 import './gr-textarea';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 import {waitForEventOnce} from '../utils/event-util';
 import {AUTOCOMPLETE_HINT_CLASS, GrTextarea} from './gr-textarea';
 import {CursorPositionChangeEventDetail} from '../api/embed';
diff --git a/polygerrit-ui/app/eslint-bazel.config.js b/polygerrit-ui/app/eslint-bazel.config.js
new file mode 100644
index 0000000..ad7690e
--- /dev/null
+++ b/polygerrit-ui/app/eslint-bazel.config.js
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// This file has a special settings for bazel.
+// The settings is required because bazel uses different location
+// for node_modules.
+
+const {defineConfig, globalIgnores} = require('eslint/config');
+const js = require('@eslint/js');
+const {FlatCompat} = require('@eslint/eslintrc');
+
+const compat = new FlatCompat({
+  baseDirectory: __dirname,
+  recommendedConfig: js.configs.recommended,
+  allConfig: js.configs.all,
+});
+
+function getBazelSettings() {
+  const runFilesDir = process.env['RUNFILES_DIR'];
+  if (!runFilesDir) {
+    // eslint is executed with 'bazel run ...' to fix the source code. It runs
+    // against real source code, no special paths for node_modules is set.
+    return {};
+  }
+  // eslint is executed with 'bazel test...'. Set path to required node_modules
+  return {
+    'import/resolver': {
+      node: {
+        paths: [
+          `${runFilesDir}/ui_npm/node_modules`,
+          `${runFilesDir}/ui_dev_npm/node_modules`,
+        ],
+      },
+    },
+  };
+}
+
+module.exports = defineConfig([
+  globalIgnores([
+    '**/node_modules',
+    '**/rollup.config.js',
+    '**/node_modules_licenses/',
+    '**/.prettierrc.js',
+    '**/.eslint-ts-resolver.config.js',
+  ]),
+  ...compat.config({
+    extends: './eslint.config.js',
+    settings: getBazelSettings(),
+  }),
+]);
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/eslint.config.js
similarity index 91%
rename from polygerrit-ui/app/.eslintrc.js
rename to polygerrit-ui/app/eslint.config.js
index e0fe3b5..2b69470 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/eslint.config.js
@@ -232,11 +232,17 @@
         message: 'Please use SPDX license headers.',
       }],
     ],
+    'sort-imports': [
+      'error', {
+        ignoreCase: true,
+        ignoreDeclarationSort: true,
+      },
+    ],
   },
 
   overrides: [
     {
-      files: ['.eslintrc.js', '.eslintrc-bazel.js'],
+      files: ['eslint.config.js', 'eslint-bazel.config.js'],
       env: {
         browser: false,
         es6: true,
@@ -273,6 +279,7 @@
       files: ['**/*.ts'],
       extends: [require.resolve('gts/.eslintrc.json')],
       rules: {
+        'no-constant-binary-expression': 'off',
         'regex/invalid': [
           'error', [{
             regex: '\'lit/decorators\'',
@@ -296,6 +303,9 @@
           name: '@polymer/decorators/lib/decorators',
           message: 'Use @polymer/decorators instead',
         }],
+        '@typescript-eslint/no-empty-object-type': 'off',
+        '@typescript-eslint/no-floating-promises': 'off',
+        '@typescript-eslint/no-unsafe-function-type': 'off',
         '@typescript-eslint/no-explicit-any': 'error',
         // See https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/9
         '@typescript-eslint/ban-ts-comment': 'off',
@@ -310,19 +320,21 @@
         ],
         '@typescript-eslint/no-unused-vars': [
           'error',
-          {argsIgnorePattern: '^_'},
+          {argsIgnorePattern: '^_', caughtErrors: 'none'},
         ],
-        // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/es-builtins.md
-        'node/no-unsupported-features/es-builtins': 'off',
-        // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
-        'node/no-unsupported-features/node-builtins': 'off',
+        '@typescript-eslint/no-unused-expressions': 'off',
+        '@typescript-eslint/no-unsafe-declaration-merging': 'off',
+        // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unsupported-features/es-builtins.md
+        'n/no-unsupported-features/es-builtins': 'off',
+        // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unsupported-features/node-builtins.md
+        'n/no-unsupported-features/node-builtins': 'off',
         // Disable no-invalid-this for ts files, because it incorrectly reports
         // errors in some cases (see https://github.com/typescript-eslint/typescript-eslint/issues/491)
-        // At the same tigit llme, we are using typescript in a strict mode and
+        // At the same time, we are using typescript in a strict mode and
         // it catches almost all errors related to invalid usage of this.
         'no-invalid-this': 'off',
 
-        'node/no-extraneous-import': 'off',
+        'n/no-extraneous-import': 'off',
 
         // Typescript already checks for undef
         'no-undef': 'off',
@@ -420,6 +432,16 @@
         'lit/quoted-expressions': ['error', 'never'],
       },
     },
+    {
+      files: ['**/*.ts'],
+      rules: {
+        'es-x/no-array-prototype-at': 'error',
+        'es-x/no-object-hasown': 'error',
+        'es-x/no-regexp-d-flag': 'error',
+        'es-x/no-string-prototype-at': 'error',
+        'es-x/no-error-cause': 'error',
+      },
+    },
   ],
   plugins: [
     'html',
@@ -428,12 +450,13 @@
     'lit',
     'prettier',
     'regex',
+    'es-x',
   ],
   settings: {
     'html/report-bad-indent': 'error',
     'import/resolver': {
       node: {},
-      [path.resolve(__dirname, './.eslint-ts-resolver.js')]: {},
+      [path.resolve(__dirname, './.eslint-ts-resolver.config.js')]: {},
     },
     'jsdoc': {
       tagNamePreference: {
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
index ffae9e5..453058e 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -14,7 +14,7 @@
   waitEventLoop,
 } from '../../test/test-utils';
 import {findActiveElement, Key} from '../../utils/dom-util';
-import {fixture, html, assert} from '@open-wc/testing';
+import {assert, fixture, html} from '@open-wc/testing';
 
 const base = HovercardMixin(LitElement);
 
diff --git a/polygerrit-ui/app/models/accounts/accounts-model_test.ts b/polygerrit-ui/app/models/accounts/accounts-model_test.ts
index e84723c..b618dc8 100644
--- a/polygerrit-ui/app/models/accounts/accounts-model_test.ts
+++ b/polygerrit-ui/app/models/accounts/accounts-model_test.ts
@@ -54,7 +54,7 @@
   test('invalid account makes only one request', () => {
     const response = {...new Response(), status: 404};
     const getAccountDetails = stubRestApi('getAccountDetails').callsFake(
-      (_, errFn) => {
+      (_: any, errFn: any) => {
         if (errFn !== undefined) {
           errFn(response);
         }
diff --git a/polygerrit-ui/app/models/browser/browser-model.ts b/polygerrit-ui/app/models/browser/browser-model.ts
index 2186647..8132a1f 100644
--- a/polygerrit-ui/app/models/browser/browser-model.ts
+++ b/polygerrit-ui/app/models/browser/browser-model.ts
@@ -3,7 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {Observable, combineLatest} from 'rxjs';
+import {combineLatest, Observable} from 'rxjs';
 import {define} from '../dependency';
 import {DiffViewMode} from '../../api/diff';
 import {UserModel} from '../user/user-model';
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index f90bcb7..52bb4d8 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -4,24 +4,24 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {
-  ChangeInfo,
-  NumericChangeId,
-  ChangeStatus,
-  ReviewerState,
   AccountId,
   AccountInfo,
+  ChangeInfo,
+  ChangeStatus,
   GroupInfo,
   Hashtag,
+  NumericChangeId,
+  ReviewerState,
 } from '../../api/rest-api';
 import {Model} from '../base/model';
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {define} from '../dependency';
 import {select} from '../../utils/observable-util';
 import {
-  ReviewInput,
-  ReviewerInput,
   AttentionSetInput,
   RelatedChangeAndCommitInfo,
+  ReviewerInput,
+  ReviewInput,
   ReviewResult,
 } from '../../types/common';
 import {getUserId} from '../../utils/account-util';
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index fc0430d..5169305 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -11,14 +11,14 @@
   createRevisions,
 } from '../../test/test-data-generators';
 import {
-  ChangeInfo,
-  NumericChangeId,
-  ChangeStatus,
-  HttpMethod,
   AccountInfo,
-  ReviewerState,
+  ChangeInfo,
+  ChangeStatus,
   GroupInfo,
   Hashtag,
+  HttpMethod,
+  NumericChangeId,
+  ReviewerState,
 } from '../../api/rest-api';
 import {BulkActionsModel, LoadingState} from './bulk-actions-model';
 import {getAppContext} from '../../services/app-context';
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index d1a798f..a71d225 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -6,27 +6,35 @@
 import {
   BasePatchSetNum,
   ChangeInfo,
-  EditInfo,
-  EDIT,
-  PARENT,
-  NumericChangeId,
-  PatchSetNum,
-  PreferencesInfo,
-  RevisionPatchSetNum,
-  PatchSetNumber,
-  CommitId,
-  RevisionInfo,
-  ListChangesOption,
   ChangeViewChangeInfo,
+  CommitId,
+  EDIT,
+  EditInfo,
+  FileInfo,
+  ListChangesOption,
+  NumericChangeId,
+  PARENT,
+  PatchSetNum,
+  PatchSetNumber,
+  PreferencesInfo,
+  RevisionInfo,
+  RevisionPatchSetNum,
 } from '../../types/common';
 import {ChangeStatus, DefaultBase} from '../../constants/constants';
-import {combineLatest, from, Observable, forkJoin, of} from 'rxjs';
 import {
-  map,
-  filter,
-  withLatestFrom,
-  switchMap,
+  BehaviorSubject,
+  combineLatest,
+  forkJoin,
+  from,
+  Observable,
+  of,
+} from 'rxjs';
+import {
   catchError,
+  filter,
+  map,
+  switchMap,
+  withLatestFrom,
 } from 'rxjs/operators';
 import {
   computeAllPatchSets,
@@ -35,14 +43,23 @@
   findEdit,
   sortRevisions,
 } from '../../utils/patch-set-util';
-import {isDefined, LoadingStatus, ParsedChangeInfo} from '../../types/types';
-import {fireAlert, fireTitleChange} from '../../utils/event-util';
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {
+  EditRevisionInfo,
+  isDefined,
+  LoadingStatus,
+  ParsedChangeInfo,
+} from '../../types/types';
+import {fire, fireAlert, fireTitleChange} from '../../utils/event-util';
+import {
+  RestApiService,
+  SubmittabilityInfo,
+} from '../../services/gr-rest-api/gr-rest-api';
 import {select} from '../../utils/observable-util';
 import {assertIsDefined} from '../../utils/common-util';
 import {Model} from '../base/model';
 import {UserModel} from '../user/user-model';
 import {define} from '../dependency';
+import {FlagsService, KnownExperimentId} from '../../services/flags/flags';
 import {
   isOwner,
   isUploader,
@@ -60,9 +77,18 @@
 import {ReportingService} from '../../services/gr-reporting/gr-reporting';
 import {Timing} from '../../constants/reporting';
 import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+import {throttleWrap} from '../../utils/async-util';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
 
+const ReloadToastMessage = {
+  NEWER_REVISION: 'A newer patch set has been uploaded',
+  RESTORED: 'This change has been restored',
+  ABANDONED: 'This change has been abandoned',
+  MERGED: 'This change has been merged',
+  NEW_MESSAGE: 'There are new messages on this change',
+};
+
 export interface ChangeState {
   /**
    * If `change` is undefined, this must be either NOT_LOADED or LOADING.
@@ -71,6 +97,12 @@
   loadingStatus: LoadingStatus;
   change?: ParsedChangeInfo;
   /**
+   * Information about submittablity and evaluation of SRs
+   *
+   * Corresponding values in `change` are always kept in sync.
+   */
+  submittabilityInfo?: SubmittabilityInfo;
+  /**
    * The list of reviewed files, kept in the model because we want changes made
    * in one view to reflect on other views without re-rendering the other views.
    * Undefined means it's still loading and empty set means no files reviewed.
@@ -85,6 +117,101 @@
   mergeable?: boolean;
 }
 
+export enum RevisionFileUpdateStatus {
+  // Indicates request error.
+  UNKNOWN = 'UNKNOWN',
+  // File is identical to previous patchset
+  SAME = 'SAME',
+  // File has been changed in comparison to previous patchset.
+  MODIFIED = 'MODIFIED',
+}
+
+export type RevisionUpdatedFiles = {
+  [revisionId: string]: {[filename: string]: RevisionFileUpdateStatus};
+};
+
+/**
+ * Calculates whether the file is modified in relation to the previous patchset.
+ *
+ * The comparison is done based on SHA-1 of file contents.
+ */
+function computeRevisionFileUpdateStatus(
+  filename: string,
+  info: FileInfo | undefined,
+  prevRevId: string | undefined,
+  fileInfos: {[revId: string]: {[filename: string]: FileInfo}}
+) {
+  // The file info is missing means it's not changed vs. patchset base.
+  if (!info) {
+    if (!prevRevId) {
+      return RevisionFileUpdateStatus.SAME;
+    } else {
+      // Check if modified in previous patchset, but not in current.
+      return filename in fileInfos[prevRevId]
+        ? RevisionFileUpdateStatus.MODIFIED
+        : RevisionFileUpdateStatus.SAME;
+    }
+  }
+
+  if (!prevRevId || !(filename in fileInfos[prevRevId])) {
+    return RevisionFileUpdateStatus.MODIFIED;
+  }
+
+  const prevSha = fileInfos[prevRevId][filename].new_sha;
+  if (!!info.new_sha && !!prevSha) {
+    return info.new_sha === prevSha
+      ? RevisionFileUpdateStatus.SAME
+      : RevisionFileUpdateStatus.MODIFIED;
+  }
+  return RevisionFileUpdateStatus.UNKNOWN;
+}
+
+/**
+ * For every revision and every file calculates if that file is modified when
+ * compared to the previous revision.
+ *
+ * The comparison is done based on SHA-1 of file contents.
+ *
+ * @param fileInfos for every revision contains the list FileInfo of files which
+ *   are modified compared to the revision's parent.
+ */
+export function computeRevisionUpdatedFiles(
+  change: ParsedChangeInfo | undefined,
+  fileInfos: {[revId: string]: {[filename: string]: FileInfo}} | undefined
+): RevisionUpdatedFiles | undefined {
+  if (!change || !fileInfos) {
+    // We set change to undefined when user navigates away from change
+    // page. So we should reset state to undefined in this case.
+    return undefined;
+  }
+  const patchsetToRevision: {[ps: number]: string} = {};
+  const revisionToPatchset: {[revId: string]: number} = {};
+  const allFiles = new Set<string>();
+  for (const [revId, rev] of Object.entries<RevisionInfo | EditRevisionInfo>(
+    change.revisions
+  )) {
+    revisionToPatchset[revId] = rev._number as number;
+    patchsetToRevision[rev._number as number] = revId;
+    Object.keys(fileInfos[revId] ?? {}).forEach(x => allFiles.add(x));
+  }
+  const revisionUpdatedFiles: RevisionUpdatedFiles = {};
+  for (const [revId, files] of Object.entries(fileInfos)) {
+    revisionUpdatedFiles[revId] = {};
+    const ps = revisionToPatchset[revId];
+    const prevRevId = ps === 1 ? undefined : patchsetToRevision[ps - 1];
+    for (const filename of allFiles) {
+      const info = files[filename];
+      revisionUpdatedFiles[revId][filename] = computeRevisionFileUpdateStatus(
+        filename,
+        info,
+        prevRevId,
+        fileInfos
+      );
+    }
+  }
+  return revisionUpdatedFiles;
+}
+
 /**
  * `change.revisions` is a dictionary mapping the revision sha to RevisionInfo,
  * but the info object itself does not contain the sha, which is a problem when
@@ -144,6 +271,32 @@
 }
 
 /**
+ * Returns new change object with the fields with submittability related fields
+ * updated.
+ *
+ * - if change is undefined return undefined.
+ * - if change number is different than the one in submittability info, no
+ * updates made
+ */
+export function fillFromSubmittabilityInfo(
+  change?: ParsedChangeInfo,
+  submittabilityInfo?: SubmittabilityInfo
+): ParsedChangeInfo | undefined {
+  if (
+    !change ||
+    !submittabilityInfo ||
+    submittabilityInfo.changeNum !== change._number
+  ) {
+    return change;
+  }
+  return {
+    ...change,
+    submittable: submittabilityInfo.submittable,
+    submit_requirements: submittabilityInfo.submitRequirements,
+  };
+}
+
+/**
  * Derives the base patchset number from all the data that can potentially
  * influence it. Mostly just returns `viewModelBasePatchNum` or PARENT, but has
  * some special logic when looking at merge commits.
@@ -198,17 +351,38 @@
 export class ChangeModel extends Model<ChangeState> {
   private change?: ParsedChangeInfo;
 
+  private submittabilityInfo?: SubmittabilityInfo;
+
   private patchNum?: RevisionPatchSetNum;
 
   private basePatchNum?: BasePatchSetNum;
 
   private latestPatchNum?: PatchSetNumber;
 
+  private readonly reloadSubmittabilityTrigger$ = new BehaviorSubject<void>(
+    undefined
+  );
+
   public readonly change$ = select(
     this.state$,
     changeState => changeState.change
   );
 
+  public readonly submittabilityInfo$ = select(
+    this.state$,
+    changeState => changeState.submittabilityInfo
+  );
+
+  public readonly submittable$ = select(
+    this.state$,
+    changeState => changeState.submittabilityInfo?.submittable
+  );
+
+  public readonly submitRequirements$ = select(
+    this.state$,
+    changeState => changeState.submittabilityInfo?.submitRequirements
+  );
+
   public readonly changeLoadingStatus$ = select(
     this.state$,
     changeState => changeState.loadingStatus
@@ -277,6 +451,28 @@
   );
 
   /**
+   * For every filename F and every revision R (corresponding to patchset P),
+   * stores whether the file is modified in relation to patchset P - 1 (or base
+   * if P = 1).
+   */
+  public readonly revisionUpdatedFiles$: Observable<
+    RevisionUpdatedFiles | undefined
+  > = select(
+    this.change$.pipe(
+      switchMap(change => {
+        if (!change) {
+          return of([change, undefined]);
+        }
+        return forkJoin([
+          Promise.resolve(change),
+          this.restApiService.getAllRevisionFiles(change._number),
+        ]);
+      })
+    ),
+    ([change, fileInfos]) => computeRevisionUpdatedFiles(change, fileInfos)
+  );
+
+  /**
    * Emits the current patchset number. If the route does not define the current
    * patchset num, then this selector waits for the change to be defined and
    * returns the number of the latest patchset.
@@ -326,13 +522,18 @@
 
   public readonly revertingChangeIds$;
 
+  public throttledShowUpdateChangeNotification: () => void;
+
+  private isViewCurrent: boolean = false;
+
   constructor(
     private readonly navigation: NavigationService,
     private readonly viewModel: ChangeViewModel,
     private readonly restApiService: RestApiService,
     private readonly userModel: UserModel,
     private readonly pluginLoader: PluginLoader,
-    private readonly reporting: ReportingService
+    private readonly reporting: ReportingService,
+    private readonly flagsService: FlagsService
   ) {
     super(initialState);
     this.patchNum$ = select(
@@ -388,6 +589,19 @@
     this.latestRevisionWithEdit$ = this.selectRevision(
       this.latestPatchNumWithEdit$
     );
+    this.change$.subscribe(change => {
+      const changeUpdatesPlugins =
+        this.pluginLoader.pluginsModel.getChangeUpdatesPlugins();
+      for (const plugin of changeUpdatesPlugins) {
+        plugin.publisher.unsubscribe();
+      }
+      if (!change) return;
+      for (const plugin of changeUpdatesPlugins) {
+        plugin.publisher.subscribe(change.project, change._number, () =>
+          this.throttledShowUpdateChangeNotification()
+        );
+      }
+    });
     this.isOwner$ = select(
       combineLatest([this.change$, this.userModel.account$]),
       ([change, account]) => isOwner(change, account)
@@ -402,6 +616,7 @@
     );
     this.subscriptions = [
       this.loadChange(),
+      this.loadSubmittabilityInfo(),
       this.loadMergeable(),
       this.loadReviewedFiles(),
       this.setOverviewTitle(),
@@ -412,6 +627,9 @@
       this.refuseEditForOpenChange(),
       this.refuseEditForClosedChange(),
       this.change$.subscribe(change => (this.change = change)),
+      this.submittabilityInfo$.subscribe(
+        submittabilityInfo => (this.submittabilityInfo = submittabilityInfo)
+      ),
       this.patchNum$.subscribe(patchNum => (this.patchNum = patchNum)),
       this.basePatchNum$.subscribe(
         basePatchNum => (this.basePatchNum = basePatchNum)
@@ -419,7 +637,14 @@
       this.latestPatchNum$.subscribe(
         latestPatchNum => (this.latestPatchNum = latestPatchNum)
       ),
+      this.viewModel.childView$.subscribe(childView => {
+        this.isViewCurrent = childView === ChangeChildView.OVERVIEW;
+      }),
     ];
+    this.throttledShowUpdateChangeNotification = throttleWrap(
+      () => this.showRefreshChangeNotification(),
+      60 * 1000
+    );
   }
 
   private reportChangeReload() {
@@ -586,6 +811,48 @@
       .subscribe(mergeable => this.updateState({mergeable}));
   }
 
+  public reloadSubmittability() {
+    this.reloadSubmittabilityTrigger$.next();
+  }
+
+  private loadSubmittabilityInfo() {
+    // Use the same trigger as loadChange, to run SR loading in parallel.
+    return combineLatest([
+      this.viewModel.changeNum$,
+      this.reloadSubmittabilityTrigger$,
+    ])
+      .pipe(
+        map(([changeNum, _]) => changeNum),
+        switchMap(changeNum => {
+          if (!changeNum) {
+            // On change reload changeNum is set to undefined to reset change
+            // state. We propagate undefined and reset the state in this case.
+            return of(undefined);
+          }
+          return from(this.restApiService.getSubmittabilityInfo(changeNum));
+        })
+      )
+      .subscribe(submittabilityInfo => {
+        // TODO(b/445644919): Remove once the submit_requirements is never
+        // requested as part of the change detail.
+        if (
+          !this.flagsService.isEnabled(
+            KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+          )
+        ) {
+          return;
+        }
+        const change = fillFromSubmittabilityInfo(
+          this.change,
+          submittabilityInfo
+        );
+        this.updateState({
+          change,
+          submittabilityInfo,
+        });
+      });
+  }
+
   private loadChange() {
     return this.viewModel.changeNum$
       .pipe(
@@ -733,6 +1000,49 @@
     this.navigation.setUrl(url);
   }
 
+  // private but used in tests
+  async showRefreshChangeNotification() {
+    const change = this.change;
+    if (!change) return;
+    const toastMessage = await this.getChangeUpdateToastMessage(change);
+    if (!toastMessage) return;
+    // We have to make sure that the update is still relevant for the user.
+    // Since starting to fetch the change update the user may have sent a
+    // reply, or the change might have been reloaded, or it could be in the
+    // process of being reloaded.
+    if (change !== this.change || !this.isViewCurrent) {
+      return;
+    }
+    fire(document, 'show-alert', {
+      message: toastMessage,
+      // Persist this alert.
+      dismissOnNavigation: true,
+      showDismiss: true,
+      action: 'Reload',
+      callback: () => this.navigateToChangeResetReload(),
+    });
+  }
+
+  async getChangeUpdateToastMessage(change: ParsedChangeInfo) {
+    const result = await this.fetchChangeUpdates(change);
+    let toastMessage = null;
+    if (!result.isLatest) {
+      toastMessage = ReloadToastMessage.NEWER_REVISION;
+    } else if (result.newStatus === ChangeStatus.MERGED) {
+      toastMessage = ReloadToastMessage.MERGED;
+    } else if (result.newStatus === ChangeStatus.ABANDONED) {
+      toastMessage = ReloadToastMessage.ABANDONED;
+    } else if (result.newStatus === ChangeStatus.NEW) {
+      toastMessage = ReloadToastMessage.RESTORED;
+    } else if (result.newMessages) {
+      toastMessage = ReloadToastMessage.NEW_MESSAGE;
+      if (result.newMessages.author?.name) {
+        toastMessage += ` from ${result.newMessages.author.name}`;
+      }
+    }
+    return toastMessage;
+  }
+
   /**
    * Check whether there are new updates on the change.
    *
@@ -807,11 +1117,24 @@
     if (this.change && change?._number !== this.change?._number) {
       return;
     }
+    if (!change) {
+      this.updateState({
+        change: undefined,
+        loadingStatus: LoadingStatus.NOT_LOADED,
+      });
+      return;
+    }
     change = updateRevisionsWithCommitShas(change);
+    // TODO(b/445644919): Remove once the submit_requirements is never requested
+    // as part of the change detail.
+    if (
+      this.flagsService.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)
+    ) {
+      change = fillFromSubmittabilityInfo(change, this.submittabilityInfo);
+    }
     this.updateState({
       change,
-      loadingStatus:
-        change === undefined ? LoadingStatus.NOT_LOADED : LoadingStatus.LOADED,
+      loadingStatus: LoadingStatus.LOADED,
     });
   }
 }
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index 29113bc..ce3533e 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -8,18 +8,21 @@
 import {ChangeStatus} from '../../constants/constants';
 import '../../test/common-test-setup';
 import {
-  TEST_NUMERIC_CHANGE_ID,
   createChange,
   createChangeMessageInfo,
   createChangeViewState,
   createEditInfo,
+  createFileInfo,
   createMergeable,
   createParsedChange,
   createRevision,
+  createSubmitRequirementResultInfo,
+  TEST_NUMERIC_CHANGE_ID,
 } from '../../test/test-data-generators';
 import {
   mockPromise,
   stubRestApi,
+  waitUntil,
   waitUntilObserved,
 } from '../../test/test-utils';
 import {
@@ -40,6 +43,8 @@
 import {getAppContext} from '../../services/app-context';
 import {
   ChangeState,
+  computeRevisionUpdatedFiles,
+  RevisionFileUpdateStatus,
   updateChangeWithEdit,
   updateRevisionsWithCommitShas,
 } from './change-model';
@@ -56,6 +61,8 @@
 import {SinonStub} from 'sinon';
 import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
 import {ShowChangeDetail} from '../../elements/shared/gr-js-api-interface/gr-js-api-types';
+import {SubmittabilityInfo} from '../../services/gr-rest-api/gr-rest-api';
+import {FlagsService, KnownExperimentId} from '../../services/flags/flags';
 
 suite('updateRevisionsWithCommitShas() tests', () => {
   test('undefined edit', async () => {
@@ -99,11 +106,142 @@
   });
 });
 
+suite('computeRevisionUpdatedFiles() tests', () => {
+  test('undefined change', async () => {
+    assert.isUndefined(updateChangeWithEdit());
+  });
+
+  test('calculate status for all files and revisions', async () => {
+    const change = {
+      ...createChange(),
+      revisions: {
+        rev1: createRevision(1),
+        rev2: createRevision(2),
+        rev3: createRevision(3),
+      },
+      current_revision: 'rev3' as CommitId,
+    };
+    const fileInfos = {
+      rev1: {
+        'a.txt': {
+          ...createFileInfo('a.txt'),
+          old_sha: 'sha00',
+          new_sha: 'sha01',
+        },
+        'c.txt': {
+          ...createFileInfo('c.txt'),
+          old_sha: 'sha20',
+          new_sha: 'sha21',
+        },
+      },
+      rev2: {
+        'a.txt': {
+          ...createFileInfo('a.txt'),
+          old_sha: 'sha00',
+          new_sha: 'sha01',
+        },
+        'b.txt': {
+          ...createFileInfo('b.txt'),
+          old_sha: 'sha10',
+          new_sha: 'sha11',
+        },
+      },
+      rev3: {
+        'a.txt': {
+          ...createFileInfo('a.txt'),
+          old_sha: 'sha00',
+          new_sha: 'sha02',
+        },
+        'b.txt': {
+          ...createFileInfo('b.txt'),
+          old_sha: 'sha10',
+          new_sha: 'sha11',
+        },
+        'c.txt': {
+          ...createFileInfo('c.txt'),
+          old_sha: 'sha20',
+          new_sha: 'sha21',
+        },
+      },
+    };
+    const result = computeRevisionUpdatedFiles(change, fileInfos);
+    assert.deepEqual(result, {
+      rev1: {
+        'a.txt': RevisionFileUpdateStatus.MODIFIED,
+        'b.txt': RevisionFileUpdateStatus.SAME,
+        'c.txt': RevisionFileUpdateStatus.MODIFIED,
+      },
+      rev2: {
+        'a.txt': RevisionFileUpdateStatus.SAME,
+        'b.txt': RevisionFileUpdateStatus.MODIFIED,
+        'c.txt': RevisionFileUpdateStatus.MODIFIED,
+      },
+      rev3: {
+        'a.txt': RevisionFileUpdateStatus.MODIFIED,
+        'b.txt': RevisionFileUpdateStatus.SAME,
+        'c.txt': RevisionFileUpdateStatus.MODIFIED,
+      },
+    });
+  });
+
+  test('no known sha, status unknown', async () => {
+    const change = {
+      ...createChange(),
+      revisions: {
+        rev1: createRevision(1),
+        rev2: createRevision(2),
+      },
+      current_revision: 'rev2' as CommitId,
+    };
+    const fileInfos = {
+      rev1: {
+        'a.txt': {
+          ...createFileInfo('a.txt'),
+          old_sha: undefined,
+          new_sha: undefined,
+        },
+      },
+      rev2: {
+        'a.txt': {
+          ...createFileInfo('a.txt'),
+          old_sha: 'sha00',
+          new_sha: 'sha01',
+        },
+      },
+    };
+    const result = computeRevisionUpdatedFiles(change, fileInfos);
+    assert.deepEqual(result, {
+      rev1: {
+        // Presence in fileInfos indicate change against BASE
+        'a.txt': RevisionFileUpdateStatus.MODIFIED,
+      },
+      rev2: {
+        'a.txt': RevisionFileUpdateStatus.UNKNOWN,
+      },
+    });
+  });
+});
+
+class TestFlagService implements FlagsService {
+  public experiments: Set<string> = new Set();
+
+  finalize() {}
+
+  isEnabled(experimentId: string): boolean {
+    return this.experiments.has(experimentId);
+  }
+
+  get enabledExperiments() {
+    return [...this.experiments];
+  }
+}
+
 suite('change model tests', () => {
   let changeViewModel: ChangeViewModel;
   let changeModel: ChangeModel;
   let knownChange: ParsedChangeInfo;
   let knownChangeNoRevision: ChangeInfo;
+  let testFlagService: TestFlagService;
   const testCompleted = new Subject<void>();
 
   async function waitForLoadingStatus(
@@ -117,6 +255,7 @@
   }
 
   setup(() => {
+    testFlagService = new TestFlagService();
     changeViewModel = testResolver(changeViewModelToken);
     changeModel = new ChangeModel(
       testResolver(navigationToken),
@@ -124,7 +263,8 @@
       getAppContext().restApiService,
       testResolver(userModelToken),
       testResolver(pluginLoaderToken),
-      getAppContext().reportingService
+      getAppContext().reportingService,
+      testFlagService
     );
     knownChangeNoRevision = {
       ...createChange(),
@@ -234,6 +374,48 @@
     });
   });
 
+  test('subscribes to change updates provider and shows notification to refetch the change', async () => {
+    await waitForLoadingStatus(LoadingStatus.NOT_LOADED);
+    const pluginLoader = testResolver(pluginLoaderToken);
+    const unsubscribeStub = sinon.stub();
+    const callbacks: (() => void)[] = [];
+    let subscribeCallCount = 0;
+    const subscribeStub = (
+      _repo: string,
+      _changeNum: number,
+      callback: () => void
+    ) => {
+      subscribeCallCount++;
+      callbacks.push(callback);
+    };
+    stubRestApi('getChangeDetail').returns(
+      Promise.resolve(createParsedChange())
+    );
+    sinon.stub(pluginLoader.pluginsModel, 'getChangeUpdatesPlugins').returns([
+      {
+        pluginName: 'plugin',
+        publisher: {
+          unsubscribe: unsubscribeStub,
+          subscribe: subscribeStub,
+        },
+      },
+    ]);
+
+    changeModel.updateStateChange(createParsedChange());
+    assert.isTrue(unsubscribeStub.calledOnce);
+    assert.equal(subscribeCallCount, 1);
+
+    const showRefreshChangeNotificationStub = sinon.stub(
+      changeModel,
+      'showRefreshChangeNotification'
+    );
+
+    for (const callback of callbacks) {
+      callback();
+    }
+    await waitUntil(() => showRefreshChangeNotificationStub.callCount === 1);
+  });
+
   test('fireShowChange from overview', async () => {
     await waitForLoadingStatus(LoadingStatus.NOT_LOADED);
     const pluginLoader = testResolver(pluginLoaderToken);
@@ -325,6 +507,132 @@
     assert.deepEqual(state?.change, updateRevisionsWithCommitShas(knownChange));
   });
 
+  test('load submit requirements (SRs load first)', async () => {
+    testFlagService.experiments.add(
+      KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+    );
+    const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+    const stubDetail = stubRestApi('getChangeDetail').callsFake(
+      () => promiseDetail
+    );
+    const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+    const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+      () => promiseSrs
+    );
+    testResolver(changeViewModelToken).setState(createChangeViewState());
+    promiseSrs.resolve({
+      changeNum: knownChange._number,
+      submittable: false,
+      submitRequirements: [createSubmitRequirementResultInfo()],
+    });
+    await waitUntilObserved(
+      changeModel.state$,
+      state => state.submittabilityInfo !== undefined,
+      'SubmitRequirements was never loaded'
+    );
+    promiseDetail.resolve(knownChange);
+    const state = await waitForLoadingStatus(LoadingStatus.LOADED);
+    assert.isTrue(state.submittabilityInfo?.submittable === false);
+    assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+    assert.isTrue(state.change?.submittable === false);
+    assert.isTrue(state.change?.submit_requirements?.length === 1);
+    assert.equal(stubDetail.callCount, 1);
+    assert.equal(stubSrs.callCount, 1);
+  });
+
+  test('load submit requirements (Detail load first, experiment enabled)', async () => {
+    testFlagService.experiments.add(
+      KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+    );
+    const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+    const stubDetail = stubRestApi('getChangeDetail').callsFake(
+      () => promiseDetail
+    );
+    const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+    const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+      () => promiseSrs
+    );
+    let state: ChangeState;
+    testResolver(changeViewModelToken).setState(createChangeViewState());
+    promiseDetail.resolve(knownChange);
+    state = await waitForLoadingStatus(LoadingStatus.LOADED);
+    promiseSrs.resolve({
+      changeNum: knownChange._number,
+      submittable: false,
+      submitRequirements: [createSubmitRequirementResultInfo()],
+    });
+    state = await waitUntilObserved(
+      changeModel.state$,
+      state => state.submittabilityInfo !== undefined,
+      'SubmitRequirements was never loaded'
+    );
+    assert.isTrue(state.submittabilityInfo?.submittable === false);
+    assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+    assert.isTrue(state.change?.submittable === false);
+    assert.isTrue(state.change?.submit_requirements?.length === 1);
+    assert.equal(stubDetail.callCount, 1);
+    assert.equal(stubSrs.callCount, 1);
+  });
+
+  test('load submit requirements (experiment disabled)', async () => {
+    const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+    const stubDetail = stubRestApi('getChangeDetail').callsFake(
+      () => promiseDetail
+    );
+    const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+    const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+      () => promiseSrs
+    );
+    testResolver(changeViewModelToken).setState(createChangeViewState());
+    promiseSrs.resolve(undefined);
+    promiseDetail.resolve({
+      ...knownChange,
+      submittable: false,
+      submit_requirements: [createSubmitRequirementResultInfo()],
+    });
+    const state = await waitForLoadingStatus(LoadingStatus.LOADED);
+    // Validate that submit requirements didn't get reset to undefined.
+    assert.isTrue(state.change?.submittable === false);
+    assert.isTrue(state.change?.submit_requirements?.length === 1);
+    assert.equal(stubDetail.callCount, 1);
+    assert.equal(stubSrs.callCount, 1);
+  });
+
+  test('reload submit requirements', async () => {
+    testFlagService.experiments.add(
+      KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS
+    );
+    // Set initial state
+    const stubDetail = stubRestApi('getChangeDetail').resolves(knownChange);
+    const stubSrs = stubRestApi('getSubmittabilityInfo');
+    stubSrs.resolves({
+      changeNum: knownChange._number,
+      submittable: false,
+      submitRequirements: [createSubmitRequirementResultInfo()],
+    });
+    testResolver(changeViewModelToken).setState(createChangeViewState());
+    await waitUntilObserved(
+      changeModel.state$,
+      state => state.submittabilityInfo !== undefined,
+      'SubmitRequirements was never loaded'
+    );
+    await waitForLoadingStatus(LoadingStatus.LOADED);
+    stubSrs.resolves({
+      changeNum: knownChange._number,
+      submittable: true,
+      submitRequirements: [createSubmitRequirementResultInfo()],
+    });
+    changeModel.reloadSubmittability();
+    const state = await waitUntilObserved(
+      changeModel.state$,
+      state => state.submittabilityInfo?.submittable === true,
+      'Submittability never reloaded'
+    );
+    assert.isTrue(state.change?.submittable);
+    assert.equal(stubDetail.callCount, 1);
+    assert.equal(stubSrs.callCount, 2);
+  });
+
   test('navigating to another change', async () => {
     // setting up a loaded change
     let promise = mockPromise<ParsedChangeInfo | undefined>();
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index c3953cc..4e0af0a 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -12,8 +12,8 @@
   PatchSetNumber,
   RevisionPatchSetNum,
 } from '../../types/common';
-import {combineLatest, of, from, Observable} from 'rxjs';
-import {switchMap, map} from 'rxjs/operators';
+import {combineLatest, from, Observable, of} from 'rxjs';
+import {map, switchMap} from 'rxjs/operators';
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {select} from '../../utils/observable-util';
 import {FileInfoStatus, SpecialFilePath} from '../../constants/constants';
diff --git a/polygerrit-ui/app/models/change/related-changes-model_test.ts b/polygerrit-ui/app/models/change/related-changes-model_test.ts
index 0bf7a76..4efbaaf 100644
--- a/polygerrit-ui/app/models/change/related-changes-model_test.ts
+++ b/polygerrit-ui/app/models/change/related-changes-model_test.ts
@@ -18,11 +18,11 @@
 } from '../../types/common';
 import {stubRestApi, waitUntilObserved} from '../../test/test-utils';
 import {
-  createParsedChange,
-  createRelatedChangesInfo,
-  createRelatedChangeAndCommitInfo,
   createChange,
   createChangeMessage,
+  createParsedChange,
+  createRelatedChangeAndCommitInfo,
+  createRelatedChangesInfo,
 } from '../../test/test-data-generators';
 import {ChangeStatus, ReviewInputTag, TopicName} from '../../api/rest-api';
 import {MessageTag} from '../../constants/constants';
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index b9f94bb..c6bdba2 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -29,21 +29,21 @@
   take,
   takeUntil,
   takeWhile,
-  timeout,
   throttleTime,
+  timeout,
   withLatestFrom,
 } from 'rxjs/operators';
 import {
   Action,
+  Category,
+  ChangeData,
   CheckResult as CheckResultApi,
   CheckRun as CheckRunApi,
-  Link,
-  ChangeData,
   ChecksApiConfig,
   ChecksProvider,
   FetchResponse,
+  Link,
   ResponseCode,
-  Category,
   RunStatus,
 } from '../../api/checks';
 import {ChangeModel} from '../change/change-model';
@@ -52,7 +52,7 @@
 import {getShaByPatchNum} from '../../utils/patch-set-util';
 import {ReportingService} from '../../services/gr-reporting/gr-reporting';
 import {Execution, Interaction, Timing} from '../../constants/reporting';
-import {fireAlert, fire} from '../../utils/event-util';
+import {fire, fireAlert} from '../../utils/event-util';
 import {Model} from '../base/model';
 import {define} from '../dependency';
 import {
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index fdaacd2..c43f7a15 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -11,8 +11,8 @@
   ChecksModel,
   ChecksPatchset,
   ChecksProviderState,
-  RunResult,
   collectRunResults,
+  RunResult,
 } from './checks-model';
 import {
   Action,
@@ -99,6 +99,7 @@
   });
 
   test('register and fetch', async () => {
+    const clock = sinon.useFakeTimers();
     let change: ParsedChangeInfo | undefined = undefined;
     testResolver(changeModelToken).change$.subscribe(c => (change = c));
     const provider = createProvider();
@@ -114,6 +115,9 @@
     const testChange = updateRevisionsWithCommitShas(createParsedChange());
     testResolver(changeModelToken).updateStateChange(testChange);
     await waitUntil(() => deepEqual(change, testChange));
+
+    // Advance time by 600ms to trigger the throttled fetch
+    clock.tick(600);
     await waitUntilCalled(fetchSpy, 'fetch');
 
     assert.equal(
@@ -122,6 +126,8 @@
         ._number as PatchSetNumber
     );
     assert.equal(model.changeNum, testChange!._number);
+
+    clock.restore();
   });
 
   test('fetch throttle', async () => {
@@ -281,7 +287,7 @@
     );
     assert.equal(
       current.runs[0].results![0].summary,
-      RUNS[0]!.results![0].summary
+      RUNS[0].results![0].summary
     );
     const result = RUNS[0].results![0];
     const updatedResult = {...result, summary: 'new'};
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 70149e1..51e9dfd 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -15,15 +15,15 @@
   RunStatus,
 } from '../../api/checks';
 import {
+  FixReplacementInfo,
   PatchSetNumber,
   RevisionPatchSetNum,
-  FixReplacementInfo,
 } from '../../api/rest-api';
 import {CommentSide} from '../../constants/constants';
-import {FixSuggestionInfo, DraftInfo} from '../../types/common';
+import {DraftInfo, FixSuggestionInfo} from '../../types/common';
 import {OpenFixPreviewEventDetail} from '../../types/events';
 import {isDefined} from '../../types/types';
-import {PROVIDED_FIX_ID, createNew} from '../../utils/comment-util';
+import {createNew, PROVIDED_FIX_ID} from '../../utils/comment-util';
 import {assert, assertIsDefined, assertNever} from '../../utils/common-util';
 import {fire} from '../../utils/event-util';
 import {CheckResult, CheckRun, RunResult} from './checks-model';
diff --git a/polygerrit-ui/app/models/checks/checks-util_test.ts b/polygerrit-ui/app/models/checks/checks-util_test.ts
index 3677462..e88721a 100644
--- a/polygerrit-ui/app/models/checks/checks-util_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-util_test.ts
@@ -9,8 +9,8 @@
 import {
   ALL_ATTEMPTS,
   AttemptChoice,
-  LATEST_ATTEMPT,
   computeIsExpandable,
+  LATEST_ATTEMPT,
   rectifyFix,
   sortAttemptChoices,
   stringToAttemptChoice,
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index f11e352..aaa4662 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -5,22 +5,20 @@
  */
 import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api';
 import {
-  CommentInfo,
-  NumericChangeId,
-  RevisionId,
-  UrlEncodedCommentId,
-  RobotCommentInfo,
-  PathToRobotCommentsInfoMap,
   AccountInfo,
-  DraftInfo,
   Comment,
-  SavingState,
-  isSaving,
-  isError,
-  isDraft,
-  isNew,
-  PatchSetNumber,
+  CommentInfo,
   CommentThread,
+  DraftInfo,
+  isDraft,
+  isError,
+  isNew,
+  isSaving,
+  NumericChangeId,
+  PatchSetNumber,
+  RevisionId,
+  SavingState,
+  UrlEncodedCommentId,
 } from '../../types/common';
 import {
   addPath,
@@ -51,7 +49,7 @@
 import {CURRENT} from '../../utils/patch-set-util';
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {ChangeModel} from '../change/change-model';
-import {Interaction, Timing} from '../../constants/reporting';
+import {Execution, Interaction, Timing} from '../../constants/reporting';
 import {assert, assertIsDefined} from '../../utils/common-util';
 import {debounce, DelayedTask} from '../../utils/async-util';
 import {ReportingService} from '../../services/gr-reporting/gr-reporting';
@@ -74,8 +72,6 @@
 export interface CommentState {
   /** undefined means 'still loading' */
   comments?: {[path: string]: CommentInfo[]};
-  /** undefined means 'still loading' */
-  robotComments?: {[path: string]: RobotCommentInfo[]};
   // All drafts are DraftInfo objects and have `state` state set.
   /** undefined means 'still loading' */
   drafts?: {[path: string]: DraftInfo[]};
@@ -106,7 +102,6 @@
 
 const initialState: CommentState = {
   comments: undefined,
-  robotComments: undefined,
   drafts: undefined,
   portedComments: undefined,
   portedDrafts: undefined,
@@ -168,19 +163,6 @@
 }
 
 // Private but used in tests.
-export function setRobotComments(
-  state: CommentState,
-  robotComments?: {
-    [path: string]: RobotCommentInfo[];
-  }
-): CommentState {
-  if (deepEqual(robotComments, state.robotComments)) return state;
-  const nextState = {...state};
-  nextState.robotComments = addPath(robotComments) || {};
-  return nextState;
-}
-
-// Private but used in tests.
 export function setDrafts(
   state: CommentState,
   drafts?: {[path: string]: DraftInfo[]}
@@ -282,6 +264,34 @@
   return setDiscardedDraft(nextState, discardedDraft);
 }
 
+/**
+ * Remove drafts that correspond to the existing published comments.
+ *
+ * Deletion of drafts is handled asynchronously by the server, because of that
+ * it's possible that after the draft is published the server still returns it
+ * as part of the drafts.
+ */
+export function filterOutPublishedDrafts(
+  drafts: {[path: string]: DraftInfo[]},
+  commentIds: Set<UrlEncodedCommentId>
+): {drafts: {[path: string]: DraftInfo[]}; removedCnt: number} {
+  let removedCnt = 0;
+  const filteredDrafts: {[path: string]: DraftInfo[]} = {};
+  for (const [path, pathDrafts] of Object.entries(drafts)) {
+    for (const draft of pathDrafts) {
+      if (draft.id && commentIds.has(draft.id)) {
+        ++removedCnt;
+        continue;
+      }
+      if (!(path in filteredDrafts)) {
+        filteredDrafts[path] = [];
+      }
+      filteredDrafts[path].push(draft);
+    }
+  }
+  return {drafts: filteredDrafts, removedCnt};
+}
+
 export const commentsModelToken = define<CommentsModel>('comments-model');
 /**
  * Model that maintains the state of all comments and drafts for the current
@@ -291,9 +301,7 @@
   public readonly commentsLoading$ = select(
     this.state$,
     commentState =>
-      commentState.comments === undefined ||
-      commentState.robotComments === undefined ||
-      commentState.drafts === undefined
+      commentState.comments === undefined || commentState.drafts === undefined
   );
 
   public readonly comments$ = select(
@@ -301,16 +309,6 @@
     commentState => commentState.comments
   );
 
-  public readonly robotComments$ = select(
-    this.state$,
-    commentState => commentState.robotComments
-  );
-
-  public readonly robotCommentCount$ = select(
-    this.robotComments$,
-    robotComments => Object.values(robotComments ?? {}).flat().length
-  );
-
   public readonly drafts$ = select(
     this.state$,
     commentState => commentState.drafts
@@ -416,7 +414,6 @@
     commentState =>
       new ChangeComments(
         commentState.comments,
-        commentState.robotComments,
         commentState.drafts,
         commentState.portedComments,
         commentState.portedDrafts
@@ -534,16 +531,13 @@
             if (!changeNum) return of([undefined, undefined, undefined]);
             return forkJoin([
               this.restApiService.getDiffComments(changeNum),
-              this.restApiService.getDiffRobotComments(changeNum),
               this.restApiService.getDiffDrafts(changeNum),
             ]);
           })
         )
-        .subscribe(([comments, robotComments, drafts]) => {
-          this.reportRobotCommentStats(robotComments);
+        .subscribe(([comments, drafts]) => {
           this.modifyState(s => {
             s = setComments(s, comments);
-            s = setRobotComments(s, robotComments);
             return setDrafts(s, drafts);
           });
         })
@@ -598,32 +592,33 @@
     this.setState(reducer({...this.getState()}));
   }
 
-  private reportRobotCommentStats(obj?: PathToRobotCommentsInfoMap) {
-    if (!obj) return;
-    const comments = Object.values(obj).flat();
-    if (comments.length === 0) return;
-    const ids = comments.map(c => c.robot_id);
-    const latestPatchset = comments.reduce(
-      (latestPs, comment) =>
-        Math.max(latestPs, (comment?.patch_set as number) ?? 0),
-      0
-    );
-    const commentsLatest = comments.filter(c => c.patch_set === latestPatchset);
-    const commentsFixes = comments
-      .map(c => c.fix_suggestions?.length ?? 0)
-      .filter(l => l > 0);
-    const details = {
-      firstId: ids[0],
-      ids: [...new Set(ids)],
-      count: comments.length,
-      countLatest: commentsLatest.length,
-      countFixes: commentsFixes.length,
-    };
-    this.reporting.reportInteraction(
-      Interaction.ROBOT_COMMENTS_STATS,
-      details,
-      {deduping: Deduping.EVENT_ONCE_PER_CHANGE}
-    );
+  override setState(state: CommentState): void {
+    const commentIds = new Set<UrlEncodedCommentId>();
+    if (state.comments) {
+      Object.values(state.comments)
+        .flatMap(x => x)
+        .forEach(c => commentIds.add(c.id));
+    }
+    if (state.drafts) {
+      const filterResult = filterOutPublishedDrafts(state.drafts, commentIds);
+      state.drafts = filterResult.drafts;
+      if (filterResult.removedCnt !== 0) {
+        this.reporting.reportExecution(
+          Execution.PUBLISHED_DRAFTS_DEDUPLICATED,
+          {
+            message: `Detected ${filterResult.removedCnt} drafts that are also published comments`,
+            count: filterResult.removedCnt,
+          }
+        );
+      }
+    }
+    if (state.portedDrafts) {
+      state.portedDrafts = filterOutPublishedDrafts(
+        state.portedDrafts,
+        commentIds
+      ).drafts;
+    }
+    super.setState(state);
   }
 
   private reportCommentStats(
diff --git a/polygerrit-ui/app/models/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
index b3dbc08..5689a3b 100644
--- a/polygerrit-ui/app/models/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -12,12 +12,17 @@
 import {
   AccountInfo,
   CommentInfo,
+  DraftInfo,
   EmailAddress,
   NumericChangeId,
   Timestamp,
   UrlEncodedCommentId,
 } from '../../types/common';
-import {CommentsModel, deleteDraft} from './comments-model';
+import {
+  CommentsModel,
+  deleteDraft,
+  filterOutPublishedDrafts,
+} from './comments-model';
 import {Subscription} from 'rxjs';
 import {
   createComment,
@@ -39,7 +44,6 @@
     draft.id = '1' as UrlEncodedCommentId;
     const state = {
       comments: {},
-      robotComments: {},
       drafts: {
         [draft.path!]: [draft],
       },
@@ -50,7 +54,6 @@
     const output = deleteDraft(state, draft);
     assert.deepEqual(output, {
       comments: {},
-      robotComments: {},
       drafts: {
         'abc.txt': [],
       },
@@ -83,9 +86,6 @@
     const diffCommentsSpy = stubRestApi('getDiffComments').returns(
       Promise.resolve({'foo.c': [createComment()]})
     );
-    const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
-      Promise.resolve({})
-    );
     const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
       Promise.resolve({})
     );
@@ -106,7 +106,6 @@
     testResolver(changeModelToken).updateStateChange(createParsedChange());
 
     await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
-    await waitUntilCalled(diffRobotCommentsSpy, 'diffRobotCommentsSpy');
     await waitUntilCalled(diffDraftsSpy, 'diffDraftsSpy');
     await waitUntilCalled(portedCommentsSpy, 'portedCommentsSpy');
     await waitUntilCalled(portedDraftsSpy, 'portedDraftsSpy');
@@ -125,6 +124,87 @@
     assert.equal(portedComments['foo.c'][0].id, '12345');
   });
 
+  test('published drafts and ported drafts are filtered out', async () => {
+    const draft1 = {
+      ...createDraft(),
+      id: 'draft1' as UrlEncodedCommentId,
+      path: 'path1',
+    };
+    const draft2 = {
+      ...createDraft(),
+      id: 'draft2' as UrlEncodedCommentId,
+      path: 'path2',
+    };
+    const draft3 = {
+      ...createDraft(),
+      id: 'draft3' as UrlEncodedCommentId,
+      path: 'path1',
+    };
+    const comment1 = {
+      ...createComment(),
+      id: 'draft1' as UrlEncodedCommentId,
+      path: 'path1',
+    };
+    const comment2 = {
+      ...createComment(),
+      id: 'draft3' as UrlEncodedCommentId,
+      path: 'path1',
+    };
+    const comment3 = {
+      ...createComment(),
+      id: 'non-draft' as UrlEncodedCommentId,
+      path: 'path',
+    };
+
+    const model = new CommentsModel(
+      testResolver(changeViewModelToken),
+      testResolver(changeModelToken),
+      testResolver(accountsModelToken),
+      getAppContext().restApiService,
+      getAppContext().reportingService,
+      testResolver(navigationToken)
+    );
+    const diffCommentsSpy = stubRestApi('getDiffComments').returns(
+      Promise.resolve({path1: [comment1, comment2], path2: [comment3]})
+    );
+    const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
+      Promise.resolve({path1: [draft1, draft3], path2: [draft2]})
+    );
+    const portedCommentsSpy = stubRestApi('getPortedComments').returns(
+      Promise.resolve({path1: [comment2]})
+    );
+    const portedDraftsSpy = stubRestApi('getPortedDrafts').returns(
+      Promise.resolve({path1: [draft3], path2: [draft2]})
+    );
+    let drafts: {[path: string]: DraftInfo[]} = {};
+    let portedDrafts: {[path: string]: DraftInfo[]} = {};
+    subscriptions.push(
+      model.state$.subscribe(s => {
+        drafts = s.drafts ?? {};
+        portedDrafts = s.portedDrafts ?? {};
+      })
+    );
+
+    testResolver(changeViewModelToken).setState(createChangeViewState());
+    testResolver(changeModelToken).updateStateChange(createParsedChange());
+
+    await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
+    await waitUntilCalled(diffDraftsSpy, 'diffDraftsSpy');
+    await waitUntilCalled(portedCommentsSpy, 'portedCommentsSpy');
+    await waitUntilCalled(portedDraftsSpy, 'portedDraftsSpy');
+    await waitUntil(
+      () => Object.keys(drafts).length > 0,
+      'comment in model not set'
+    );
+    await waitUntil(
+      () => Object.keys(portedDrafts).length > 0,
+      'ported comment in model not set'
+    );
+
+    assert.deepEqual(drafts, {path2: [draft2]});
+    assert.deepEqual(portedDrafts, {path2: [draft2]});
+  });
+
   test('duplicate mentions are filtered out', async () => {
     const account = {
       ...createAccountWithEmail('abcd@def.com' as EmailAddress),
@@ -223,4 +303,29 @@
         'Comment is deleted'
     );
   });
+
+  test('filterOutPublishedDrafts removes drafts that are comments', () => {
+    const draft1 = {...createDraft(), id: 'draft1' as UrlEncodedCommentId};
+    const draft2 = {...createDraft(), id: 'draft2' as UrlEncodedCommentId};
+    const draft3 = {...createDraft(), id: 'draft3' as UrlEncodedCommentId};
+    const comment1 = {...createComment(), id: 'draft1' as UrlEncodedCommentId};
+    const comment2 = {...createComment(), id: 'draft3' as UrlEncodedCommentId};
+    const comment3 = {
+      ...createComment(),
+      id: 'non-draft' as UrlEncodedCommentId,
+    };
+
+    const drafts = {path1: [draft1, draft2], path2: [draft3]};
+    const commentIds = new Set<UrlEncodedCommentId>([
+      comment1.id,
+      comment2.id,
+      comment3.id,
+    ]);
+    const {drafts: filteredDrafts, removedCnt} = filterOutPublishedDrafts(
+      drafts,
+      commentIds
+    );
+    assert.deepEqual(filteredDrafts, {path1: [draft2]});
+    assert.equal(removedCnt, 2);
+  });
 });
diff --git a/polygerrit-ui/app/models/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
index 1636ad4..61b3bec 100644
--- a/polygerrit-ui/app/models/config/config-model.ts
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -57,9 +57,10 @@
     serverConfig => serverConfig?.change?.mergeability_computation_behavior
   );
 
-  public enableRobotComments$ = select(
+  public allowMarkdownBase64ImagesInComments$ = select(
     this.serverConfig$,
-    serverConfig => !!serverConfig?.change?.enable_robot_comments
+    serverConfig =>
+      !!serverConfig?.change?.allow_markdown_base64_images_in_comments
   );
 
   public docsBaseUrl$ = select(
diff --git a/polygerrit-ui/app/models/dependency_test.ts b/polygerrit-ui/app/models/dependency_test.ts
index c52765d..9c53adc 100644
--- a/polygerrit-ui/app/models/dependency_test.ts
+++ b/polygerrit-ui/app/models/dependency_test.ts
@@ -7,7 +7,7 @@
 import {html, LitElement} from 'lit';
 import {customElement, property, query} from 'lit/decorators.js';
 import '../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 
 interface FooService {
   value: string;
@@ -109,7 +109,7 @@
 
     element.showBarProvider = true;
     await element.updateComplete;
-    assert.isDefined(element.bar?.litChild?.barRef());
+    assert.isDefined((element.bar as BarProviderElement).litChild?.barRef());
   });
 });
 
diff --git a/polygerrit-ui/app/models/di-provider-element_test.ts b/polygerrit-ui/app/models/di-provider-element_test.ts
index 6f0ac4a..e4ad38f 100644
--- a/polygerrit-ui/app/models/di-provider-element_test.ts
+++ b/polygerrit-ui/app/models/di-provider-element_test.ts
@@ -7,7 +7,7 @@
 import {customElement, state} from 'lit/decorators.js';
 import {define, resolve} from './dependency';
 import '../test/common-test-setup';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 import {DIProviderElement, wrapInProvider} from './di-provider-element';
 import {BehaviorSubject} from 'rxjs';
 import {waitUntilObserved} from '../test/test-utils';
diff --git a/polygerrit-ui/app/models/flows/flows-model.ts b/polygerrit-ui/app/models/flows/flows-model.ts
new file mode 100644
index 0000000..460cec6
--- /dev/null
+++ b/polygerrit-ui/app/models/flows/flows-model.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {BehaviorSubject, combineLatest, from, of} from 'rxjs';
+import {catchError, map, switchMap} from 'rxjs/operators';
+import {ChangeModel} from '../change/change-model';
+import {FlowInfo, FlowInput} from '../../api/rest-api';
+import {Model} from '../base/model';
+import {define} from '../dependency';
+
+import {NumericChangeId} from '../../types/common';
+import {getAppContext} from '../../services/app-context';
+import {KnownExperimentId} from '../../services/flags/flags';
+
+export interface FlowsState {
+  flows: FlowInfo[];
+  loading: boolean;
+  errorMessage?: string;
+}
+
+export const flowsModelToken = define<FlowsModel>('flows-model');
+
+export class FlowsModel extends Model<FlowsState> {
+  readonly flows$ = this.state$.pipe(map(s => s.flows));
+
+  readonly loading$ = this.state$.pipe(map(s => s.loading));
+
+  private readonly reload$ = new BehaviorSubject<void>(undefined);
+
+  private changeNum?: NumericChangeId;
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  private flagService = getAppContext().flagsService;
+
+  constructor(private readonly changeModel: ChangeModel) {
+    super({
+      flows: [],
+      loading: true,
+    });
+
+    this.subscriptions.push(
+      this.changeModel.changeNum$.subscribe(changeNum => {
+        this.changeNum = changeNum;
+      })
+    );
+
+    this.subscriptions.push(
+      combineLatest([this.changeModel.changeNum$, this.reload$])
+        .pipe(
+          switchMap(([changeNum]) => {
+            if (
+              !changeNum ||
+              !this.flagService.isEnabled(KnownExperimentId.SHOW_FLOWS_TAB)
+            )
+              return of([]);
+            this.setState({...this.getState(), loading: true});
+            return from(this.restApiService.listFlows(changeNum)).pipe(
+              catchError(err => {
+                this.setState({
+                  ...this.getState(),
+                  errorMessage: `Failed to load flows: ${err}`,
+                  loading: false,
+                });
+                return of([]);
+              })
+            );
+          })
+        )
+        .subscribe(flows => {
+          this.setState({
+            ...this.getState(),
+            flows: flows ?? [],
+            loading: false,
+          });
+        })
+    );
+  }
+
+  reload() {
+    this.reload$.next();
+  }
+
+  async deleteFlow(flowId: string) {
+    if (!this.changeNum) return;
+    await this.restApiService.deleteFlow(this.changeNum, flowId);
+    this.reload();
+  }
+
+  async createFlow(flowInput: FlowInput) {
+    if (!this.changeNum) return;
+    await this.restApiService.createFlow(this.changeNum, flowInput);
+    this.reload();
+  }
+}
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 880bcd0..9df61bc 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -14,6 +14,7 @@
 import {select} from '../../utils/observable-util';
 import {CoverageProvider, TokenHoverListener} from '../../api/annotation';
 import {SuggestionsProvider} from '../../api/suggestions';
+import {ChangeUpdatesPublisher} from '../../api/change-updates';
 
 export interface CoveragePlugin {
   pluginName: string;
@@ -26,6 +27,11 @@
   config: ChecksApiConfig;
 }
 
+export interface ChangeUpdatesPlugin {
+  pluginName: string;
+  publisher: ChangeUpdatesPublisher;
+}
+
 export interface SuggestionPlugin {
   pluginName: string;
   provider: SuggestionsProvider;
@@ -54,6 +60,11 @@
    */
   coveragePlugins: CoveragePlugin[];
   /**
+   * List of plugins that have registered a publisher for change updated events.
+   */
+  changeUpdatesPlugins: ChangeUpdatesPlugin[];
+
+  /**
    * List of plugins that have called checks().register().
    */
   checksPlugins: ChecksPlugin[];
@@ -89,6 +100,11 @@
 
   public coveragePlugins$ = select(this.state$, state => state.coveragePlugins);
 
+  public changeUpdatesPlugins$ = select(
+    this.state$,
+    state => state.changeUpdatesPlugins
+  );
+
   public suggestionsPlugins$ = select(
     this.state$,
     state => state.suggestionsPlugins
@@ -100,6 +116,7 @@
     super({
       pluginsLoaded: false,
       coveragePlugins: [],
+      changeUpdatesPlugins: [],
       checksPlugins: [],
       suggestionsPlugins: [],
       tokenHighlightPlugins: [],
@@ -122,6 +139,26 @@
     this.setState(nextState);
   }
 
+  getChangeUpdatesPlugins() {
+    return this.getState().changeUpdatesPlugins;
+  }
+
+  changeUpdatesRegister(plugin: ChangeUpdatesPlugin) {
+    const nextState = {...this.getState()};
+    nextState.changeUpdatesPlugins = [...nextState.changeUpdatesPlugins];
+    const alreadyRegistered = nextState.changeUpdatesPlugins.some(
+      p => p.pluginName === plugin.pluginName
+    );
+    if (alreadyRegistered) {
+      console.warn(
+        `${plugin.pluginName} tried to register twice as a change updates provider. Ignored.`
+      );
+      return;
+    }
+    nextState.changeUpdatesPlugins.push(plugin);
+    this.setState(nextState);
+  }
+
   checksRegister(plugin: ChecksPlugin) {
     const nextState = {...this.getState()};
     nextState.checksPlugins = [...nextState.checksPlugins];
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index d8cbab8..c8ec819 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -3,7 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {from, of, Observable} from 'rxjs';
+import {from, Observable, of} from 'rxjs';
 import {filter, switchMap, tap} from 'rxjs/operators';
 import {
   DiffPreferencesInfo as DiffPreferencesInfoAPI,
@@ -18,11 +18,11 @@
   TopMenuItemInfo,
 } from '../../types/common';
 import {
-  createDefaultPreferences,
-  createDefaultDiffPrefs,
-  createDefaultEditPrefs,
   AppTheme,
   ColumnNames,
+  createDefaultDiffPrefs,
+  createDefaultEditPrefs,
+  createDefaultPreferences,
 } from '../../constants/constants';
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {DiffPreferencesInfo} from '../../types/diff';
diff --git a/polygerrit-ui/app/models/views/admin.ts b/polygerrit-ui/app/models/views/admin.ts
index 164858d..0f6c4eb 100644
--- a/polygerrit-ui/app/models/views/admin.ts
+++ b/polygerrit-ui/app/models/views/admin.ts
@@ -9,10 +9,10 @@
 import {Model} from '../base/model';
 import {Route, ViewState} from './base';
 import {
-  RepoName,
-  GroupId,
-  AccountDetailInfo,
   AccountCapabilityInfo,
+  AccountDetailInfo,
+  GroupId,
+  RepoName,
 } from '../../types/common';
 import {hasOwnProperty} from '../../utils/common-util';
 import {MenuLink} from '../../api/admin';
@@ -43,7 +43,7 @@
   viewableToAll?: boolean;
   section?: string;
   capability?: string;
-  target?: string | null;
+  target?: '_blank' | '_parent' | '_self' | '_top' | null;
   subsection?: SubsectionInterface;
   children?: SubsectionInterface[];
 }
@@ -150,7 +150,7 @@
 
   // Append top-level links that are defined by plugins.
   links.push(
-    ...getAdminMenuLinks().map((link: MenuLink) => {
+    ...getAdminMenuLinks().map((link: MenuLink): NavLink => {
       return {
         url: link.url,
         name: link.text,
@@ -275,6 +275,18 @@
         detailType: RepoDetailView.DASHBOARDS,
         url: createRepoUrl({repo, detail: RepoDetailView.DASHBOARDS}),
       },
+      {
+        name: 'Submit Requirements',
+        view: GerritView.REPO,
+        detailType: RepoDetailView.SUBMIT_REQUIREMENTS,
+        url: createRepoUrl({repo, detail: RepoDetailView.SUBMIT_REQUIREMENTS}),
+      },
+      {
+        name: 'Labels',
+        view: GerritView.REPO,
+        detailType: RepoDetailView.LABELS,
+        url: createRepoUrl({repo, detail: RepoDetailView.LABELS}),
+      },
     ],
   };
 }
diff --git a/polygerrit-ui/app/models/views/admin_test.ts b/polygerrit-ui/app/models/views/admin_test.ts
index eab362a..fff77e9 100644
--- a/polygerrit-ui/app/models/views/admin_test.ts
+++ b/polygerrit-ui/app/models/views/admin_test.ts
@@ -10,11 +10,11 @@
 import {GerritView} from '../../services/router/router-model';
 import {
   AdminChildView,
+  AdminNavLinksOption,
   AdminViewState,
   createAdminUrl,
-  PLUGIN_LIST_ROUTE,
-  AdminNavLinksOption,
   getAdminLinks,
+  PLUGIN_LIST_ROUTE,
 } from './admin';
 import {
   AccountDetailInfo,
@@ -63,7 +63,7 @@
 
     if (expected.projectPageShown) {
       assert.isOk(res.links[0].subsection);
-      assert.equal(res.links[0].subsection!.children!.length, 6);
+      assert.equal(res.links[0].subsection.children!.length, 8);
     } else {
       assert.isNotOk(res.links[0].subsection);
     }
@@ -71,7 +71,7 @@
     if (expected.groupPageShown) {
       assert.isOk(res.links[1].subsection);
       assert.equal(
-        res.links[1].subsection!.children!.length,
+        res.links[1].subsection.children!.length,
         expected.groupSubpageLength
       );
     } else if (expected.totalLength > 1) {
@@ -87,9 +87,9 @@
 
         // External links should open in new tab.
         if (link.url[0] !== '/') {
-          assert.equal(linkMatch!.target, '_blank');
+          assert.equal(linkMatch.target, '_blank');
         } else {
-          assert.isNotOk(linkMatch!.target);
+          assert.isNotOk(linkMatch.target);
         }
       }
     }
@@ -97,13 +97,13 @@
     // Current section
     if (expected.projectPageShown || expected.groupPageShown) {
       assert.isOk(res.expandedSection);
-      assert.isOk(res.expandedSection!.children);
+      assert.isOk(res.expandedSection.children);
     } else {
       assert.isNotOk(res.expandedSection);
     }
     if (expected.projectPageShown) {
       assert.equal(res.expandedSection!.name, 'my-repo');
-      assert.equal(res.expandedSection!.children!.length, 6);
+      assert.equal(res.expandedSection!.children!.length, 8);
     } else if (expected.groupPageShown) {
       assert.equal(res.expandedSection!.name, 'my-group');
       assert.equal(
@@ -114,7 +114,6 @@
   };
 
   suite('logged out', () => {
-    let account: AccountDetailInfo;
     let expected: any;
 
     setup(() => {
@@ -132,7 +131,7 @@
         totalLength: 1,
         projectPageShown: false,
       });
-      await testAdminLinks(account, options, expected);
+      await testAdminLinks(undefined, options, expected);
     });
 
     test('with a repo', async () => {
@@ -141,7 +140,7 @@
         totalLength: 1,
         projectPageShown: true,
       });
-      await testAdminLinks(account, options, expected);
+      await testAdminLinks(undefined, options, expected);
     });
 
     test('with plugin generated links', async () => {
@@ -156,7 +155,7 @@
         projectPageShown: false,
         pluginGeneratedLinks: generatedLinks,
       });
-      await testAdminLinks(account, options, expected);
+      await testAdminLinks(undefined, options, expected);
     });
   });
 
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 61c6115..03c944f 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -4,14 +4,14 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {
-  NumericChangeId,
-  RepoName,
-  RevisionPatchSetNum,
   BasePatchSetNum,
   ChangeInfo,
-  PatchSetNumber,
   EDIT,
+  NumericChangeId,
   PARENT,
+  PatchSetNumber,
+  RepoName,
+  RevisionPatchSetNum,
 } from '../../api/rest-api';
 import {Tab} from '../../constants/constants';
 import {GerritView} from '../../services/router/router-model';
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index 8e671d1..e05adb6 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -17,10 +17,10 @@
   createEditViewState,
 } from '../../test/test-data-generators';
 import {
+  ChangeViewState,
   createChangeUrl,
   createDiffUrl,
   createEditUrl,
-  ChangeViewState,
 } from './change';
 
 suite('change view state tests', () => {
diff --git a/polygerrit-ui/app/models/views/dashboard.ts b/polygerrit-ui/app/models/views/dashboard.ts
index f5fc8b4..f9e60b4 100644
--- a/polygerrit-ui/app/models/views/dashboard.ts
+++ b/polygerrit-ui/app/models/views/dashboard.ts
@@ -5,7 +5,7 @@
  */
 import {RepoName} from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
-import {DashboardId} from '../../types/common';
+import {DashboardId, UserId} from '../../types/common';
 import {DashboardSection} from '../../utils/dashboard-util';
 import {encodeURL, getBaseUrl} from '../../utils/url-util';
 import {define} from '../dependency';
@@ -38,7 +38,7 @@
   type: DashboardType;
   project?: RepoName;
   dashboard?: DashboardId;
-  user?: string;
+  user?: UserId | 'self';
   sections?: DashboardSection[];
   title?: string;
 }
diff --git a/polygerrit-ui/app/models/views/dashboard_test.ts b/polygerrit-ui/app/models/views/dashboard_test.ts
index 47f4868a..0d4d0ba 100644
--- a/polygerrit-ui/app/models/views/dashboard_test.ts
+++ b/polygerrit-ui/app/models/views/dashboard_test.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {assert} from '@open-wc/testing';
-import {RepoName} from '../../api/rest-api';
+import {EmailAddress, RepoName} from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
 import '../../test/common-test-setup';
 import {assertRouteFalse, assertRouteState} from '../../test/test-utils';
@@ -56,8 +56,11 @@
 
     test('user dashboard', () => {
       assert.equal(
-        createDashboardUrl({type: DashboardType.USER, user: 'user'}),
-        '/dashboard/user'
+        createDashboardUrl({
+          type: DashboardType.USER,
+          user: 'user@email.com' as EmailAddress,
+        }),
+        '/dashboard/user@email.com'
       );
     });
 
@@ -94,13 +97,13 @@
     test('custom user dashboard, with title', () => {
       const state = {
         type: DashboardType.CUSTOM,
-        user: 'user',
+        user: 'user@email.com' as EmailAddress,
         sections: [{name: 'name', query: 'query'}],
         title: 'custom dashboard',
       };
       assert.equal(
         createDashboardUrl(state),
-        '/dashboard/user?name=query&title=custom+dashboard'
+        '/dashboard/user@email.com?name=query&title=custom+dashboard'
       );
     });
 
diff --git a/polygerrit-ui/app/models/views/repo.ts b/polygerrit-ui/app/models/views/repo.ts
index 1629449..d7ff955 100644
--- a/polygerrit-ui/app/models/views/repo.ts
+++ b/polygerrit-ui/app/models/views/repo.ts
@@ -17,6 +17,8 @@
   COMMANDS = 'commands',
   DASHBOARDS = 'dashboards',
   TAGS = 'tags',
+  SUBMIT_REQUIREMENTS = 'submit-requirements',
+  LABELS = 'labels',
 }
 
 export interface RepoViewState extends ViewState {
@@ -49,6 +51,10 @@
     url += ',commands';
   } else if (state.detail === RepoDetailView.DASHBOARDS) {
     url += ',dashboards';
+  } else if (state.detail === RepoDetailView.SUBMIT_REQUIREMENTS) {
+    url += ',submit-requirements';
+  } else if (state.detail === RepoDetailView.LABELS) {
+    url += ',labels';
   }
   return getBaseUrl() + url;
 }
diff --git a/polygerrit-ui/app/models/views/search.ts b/polygerrit-ui/app/models/views/search.ts
index 6fb3f82..98e30fb 100644
--- a/polygerrit-ui/app/models/views/search.ts
+++ b/polygerrit-ui/app/models/views/search.ts
@@ -12,7 +12,7 @@
   tap,
   withLatestFrom,
 } from 'rxjs/operators';
-import {RepoName, BranchName, TopicName, ChangeInfo} from '../../api/rest-api';
+import {BranchName, ChangeInfo, RepoName, TopicName} from '../../api/rest-api';
 import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {GerritView} from '../../services/router/router-model';
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index abc87c7..55d3477 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -56,12 +56,6 @@
     sharedLicenseFile: 'polymer-2015.txt',
   };
 
-  public static Polymer2016: LicenseInfo = {
-    name: 'Polymer-2016',
-    type: LicenseTypes.Bsd3,
-    sharedLicenseFile: 'polymer-2016.txt',
-  };
-
   public static Polymer2017: LicenseInfo = {
     name: 'Polymer-2017',
     type: LicenseTypes.Bsd3,
@@ -137,18 +131,10 @@
     filesFilter: fontsRobotomonoFilter,
   },
   {
-    name: '@polymer/iron-a11y-announcer',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/iron-a11y-keys-behavior',
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/iron-autogrow-textarea',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/iron-behaviors',
     license: SharedLicenses.Polymer2015,
   },
@@ -157,14 +143,6 @@
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/iron-dropdown',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/iron-fit-behavior',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/iron-flex-layout',
     license: SharedLicenses.Polymer2015,
   },
@@ -173,54 +151,14 @@
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/iron-icon',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/iron-iconset-svg',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/iron-image',
-    license: SharedLicenses.Polymer2016,
-  },
-  {
-    name: '@polymer/iron-input',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/iron-menu-behavior',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/iron-meta',
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/iron-overlay-behavior',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/iron-resizable-behavior',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/iron-selector',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/iron-validatable-behavior',
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/marked-element',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/neon-animation',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/paper-behaviors',
     license: SharedLicenses.Polymer2015,
   },
@@ -229,54 +167,10 @@
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/paper-card',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-checkbox',
-    license: SharedLicenses.Polymer2016,
-  },
-  {
-    name: '@polymer/paper-dialog',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-dialog-behavior',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-dialog-scrollable',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-dropdown-menu',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-fab',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-icon-button',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-input',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/paper-item',
     license: SharedLicenses.Polymer2015,
   },
   {
-    name: '@polymer/paper-listbox',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-menu-button',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/paper-ripple',
     license: SharedLicenses.Polymer2014,
   },
@@ -285,22 +179,18 @@
     license: SharedLicenses.Polymer2014,
   },
   {
-    name: '@polymer/paper-tabs',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-toggle-button',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
-    name: '@polymer/paper-tooltip',
-    license: SharedLicenses.Polymer2015,
-  },
-  {
     name: '@polymer/polymer',
     license: SharedLicenses.Polymer2017,
   },
   {
+    name: '@material/web',
+    license: {
+      name: '@material/web',
+      type: LicenseTypes.Apache2_0,
+      packageLicenseFile: 'LICENSE',
+    },
+  },
+  {
     name: '@types/resemblejs',
     license: {
       name: 'DefinitelyTyped',
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses/polymer-2016.txt b/polygerrit-ui/app/node_modules_licenses/licenses/polymer-2016.txt
deleted file mode 100644
index 077af5e..0000000
--- a/polygerrit-ui/app/node_modules_licenses/licenses/polymer-2016.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
-
-This code may only be used under the BSD style license found at
-http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
-http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
-found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
-part of the polymer project is also subject to an additional IP rights grant
-found at http://polymer.github.io/PATENTS.txt
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/polygerrit-ui/app/node_modules_licenses/tsconfig.json b/polygerrit-ui/app/node_modules_licenses/tsconfig.json
index f0a540b..de1eb24 100644
--- a/polygerrit-ui/app/node_modules_licenses/tsconfig.json
+++ b/polygerrit-ui/app/node_modules_licenses/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021' or 'ESNEXT'. */
+    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 94d51fd..314c99d 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -3,44 +3,25 @@
   "description": "Gerrit Code Review - Polygerrit dependencies",
   "browser": true,
   "dependencies": {
+    "@material/web": "^2.4.0",
     "@polymer/decorators": "^3.0.0",
     "@polymer/font-roboto-local": "^3.0.2",
-    "@polymer/iron-a11y-announcer": "^3.2.0",
-    "@polymer/iron-autogrow-textarea": "^3.0.3",
-    "@polymer/iron-dropdown": "^3.0.1",
-    "@polymer/iron-fit-behavior": "^3.1.0",
-    "@polymer/iron-icon": "^3.0.1",
-    "@polymer/iron-iconset-svg": "^3.0.1",
-    "@polymer/iron-input": "^3.0.1",
-    "@polymer/iron-selector": "^3.0.1",
-    "@polymer/marked-element": "^3.0.1",
     "@polymer/paper-button": "^3.0.1",
-    "@polymer/paper-card": "^3.0.1",
-    "@polymer/paper-checkbox": "^3.1.0",
-    "@polymer/paper-dialog": "^3.0.1",
-    "@polymer/paper-dialog-behavior": "^3.0.1",
-    "@polymer/paper-dialog-scrollable": "^3.0.1",
-    "@polymer/paper-dropdown-menu": "^3.2.0",
-    "@polymer/paper-fab": "^3.0.1",
-    "@polymer/paper-input": "^3.2.1",
     "@polymer/paper-item": "^3.0.1",
-    "@polymer/paper-listbox": "^3.0.1",
     "@polymer/paper-styles": "^3.0.1",
-    "@polymer/paper-tabs": "^3.1.0",
-    "@polymer/paper-toggle-button": "^3.0.1",
-    "@polymer/paper-tooltip": "^3.0.1",
-    "@polymer/polymer": "3.5.1",
+    "@polymer/polymer": "3.5.2",
     "@types/resemblejs": "^4.1.3",
     "@types/resize-observer-browser": "^0.1.11",
     "@webcomponents/shadycss": "^1.11.2",
-    "@webcomponents/webcomponentsjs": "^1.3.3",
+    "@webcomponents/webcomponentsjs": "^2.8.0",
     "highlight.js": "^11.11.1",
     "highlightjs-closure-templates": "https://github.com/highlightjs/highlightjs-closure-templates#02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba",
     "highlightjs-epp": "https://github.com/highlightjs/highlightjs-epp#9f9e1a92f37c217c68899c7d3bdccb4d134681b9",
     "highlightjs-structured-text": "https://github.com/highlightjs/highlightjs-structured-text#e68dd7aa829529fb6c40d6287585f43273605a9e",
     "highlightjs-vue": "https://github.com/paladox/highlightjs-vue#44eed074ea0110d1ad03d2cbd77d27027cf7bb04",
     "immer": "^9.0.21",
-    "lit": "^3.2.1",
+    "lit": "^3.3.1",
+    "marked": "^0.5.0",
     "polymer-bridges": "file:../../polymer-bridges",
     "polymer-resin": "^2.0.1",
     "resemblejs": "^5.0.0",
@@ -52,9 +33,13 @@
     "@polymer/polymer": [
       "There is a an issue with release 3.5.2. Tests are failing with:",
       "NotSupportedError: Failed to execute 'define' on 'CustomElementRegistry':",
-      "the name 'dom-module' has already been used with this registry at ..."
+      "the name 'dom-module' has already been used with this registry at ...",
+      "We fix this by forcing all packages that use polymer to use 3.5.2."
     ]
   },
+  "resolutions": {
+    "@polymer/polymer": "3.5.2"
+  },
   "license": "Apache-2.0",
   "private": true
 }
\ No newline at end of file
diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
index 9400b12..ab43ef0 100644
--- a/polygerrit-ui/app/rollup.config.js
+++ b/polygerrit-ui/app/rollup.config.js
@@ -28,9 +28,9 @@
   return require(pluginPath);
 }
 
-const resolve = requirePlugin('rollup-plugin-node-resolve');
-const {terser} = requirePlugin('rollup-plugin-terser');
-const define = requirePlugin('rollup-plugin-define');
+const {nodeResolve} = requirePlugin('@rollup/plugin-node-resolve');
+const terser = requirePlugin('@rollup/plugin-terser');
+const replace = requirePlugin('@rollup/plugin-replace');
 
 // @polymer/font-roboto-local uses import.meta.url value
 // as a base path to fonts. We should substitute a correct javascript
@@ -59,8 +59,14 @@
   output: {
     format: 'iife',
     compact: true,
+    generatedCode: 'es2015',
     plugins: [
       terser({
+        ecma: 2021,
+        compress: {
+          // An extra pass can squeeze out an extra byte or two.
+          passes: 2,
+        },
         output: {
           comments: false,
         },
@@ -69,17 +75,16 @@
   },
   // Context must be set to window to correctly processing global variables
   context: 'window',
-  plugins: [resolve({
-    customResolveOptions: {
-      // By default, it tries to use page.mjs file instead of page.js
-      // when importing 'page/page'.
-      // TODO: page.was removed. Is something obsolete here?
-      extensions: ['.js'],
-      moduleDirectory: 'external/ui_npm/node_modules',
-    },
+  plugins: [nodeResolve({
+    // By default, it tries to use page.mjs file instead of page.js
+    // when importing 'page/page'.
+    // TODO: page.was removed. Is something obsolete here?
+    extensions: ['.js'],
+    modulePaths: [path.join(process.cwd(), 'external/ui_npm/node_modules')]
   }),
-  define({
-    replacements: {
+  replace({
+    preventAssignment: true,
+    values: {
       'process.env.NODE_ENV': JSON.stringify('production'),
     },
   }),
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 70b4eb8..593c898 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -23,42 +23,51 @@
     rollup_bundle(
         name = app_name + "-bundle-js",
         srcs = [app_name + "-full-src"],
+        args = [
+            "--bundleConfigAsCjs=true",
+        ],
         config_file = ":rollup.config.js",
         entry_point = entry_point,
         rollup_bin = "//tools/node_tools:rollup-bin",
         silent = True,
         sourcemap = "hidden",
         deps = [
-            "@tools_npm//rollup-plugin-define",
-            "@tools_npm//rollup-plugin-node-resolve",
+            "@tools_npm//@rollup/plugin-replace",
+            "@tools_npm//@rollup/plugin-node-resolve",
         ],
     )
 
     rollup_bundle(
         name = "syntax-worker",
         srcs = [app_name + "-full-src"],
+        args = [
+            "--bundleConfigAsCjs=true",
+        ],
         config_file = ":rollup.config.js",
         entry_point = "_pg_ts_out/workers/syntax-worker.js",
         rollup_bin = "//tools/node_tools:rollup-bin",
         silent = True,
         sourcemap = "hidden",
         deps = [
-            "@tools_npm//rollup-plugin-define",
-            "@tools_npm//rollup-plugin-node-resolve",
+            "@tools_npm//@rollup/plugin-replace",
+            "@tools_npm//@rollup/plugin-node-resolve",
         ],
     )
 
     rollup_bundle(
         name = "service-worker",
         srcs = [app_name + "-full-src"],
+        args = [
+            "--bundleConfigAsCjs=true",
+        ],
         config_file = ":rollup.config.js",
         entry_point = "_pg_ts_out/workers/service-worker.js",
         rollup_bin = "//tools/node_tools:rollup-bin",
         silent = True,
         sourcemap = "hidden",
         deps = [
-            "@tools_npm//rollup-plugin-define",
-            "@tools_npm//rollup-plugin-node-resolve",
+            "@tools_npm//@rollup/plugin-replace",
+            "@tools_npm//@rollup/plugin-node-resolve",
         ],
     )
 
@@ -102,8 +111,8 @@
             "//lib/fonts:robotofonts",
             "//lib/js:highlightjs__files",
             "//lib/js:emojis__files",
-            "@ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js",
-            "@ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js.map",
+            "@ui_npm//@webcomponents/webcomponentsjs",
+            "@ui_npm//:node_modules/@webcomponents/webcomponentsjs/package.json",
             "@ui_npm//:node_modules/resemblejs/resemble.js",
             "@ui_npm//@polymer/font-roboto-local",
             "@ui_npm//:node_modules/@polymer/font-roboto-local/package.json",
@@ -111,7 +120,8 @@
         outs = outs,
         cmd = " && ".join([
             "FONT_DIR=$$(dirname $(location @ui_npm//:node_modules/@polymer/font-roboto-local/package.json))/fonts",
-            "mkdir -p $$TMP/polygerrit_ui/{workers,styles/themes,fonts/{roboto,robotomono},bower_components/{emojis,highlightjs,webcomponentsjs,resemblejs},elements}",
+            "WEBCOMPONENTJS_DIR=$$(dirname $(location @ui_npm//:node_modules/@webcomponents/webcomponentsjs/package.json))",
+            "mkdir -p $$TMP/polygerrit_ui/{workers,styles/themes,fonts/{roboto,robotomono},bower_components/{emojis,highlightjs,webcomponentsjs,webcomponentsjs/bundles,resemblejs},elements}",
             "for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + app_name + ".$$ext; done",
             "cp $(locations //lib/fonts:robotofonts) $$TMP/polygerrit_ui/fonts/",
             "cp $(locations //lib/fonts:material-icons) $$TMP/polygerrit_ui/fonts/",
@@ -120,8 +130,8 @@
             "for f in $(locations " + name + "_worker_sources); do cp $$f $$TMP/polygerrit_ui/workers; done",
             "for f in $(locations //lib/js:highlightjs__files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
             "for f in $(locations //lib/js:emojis__files); do cp $$f $$TMP/polygerrit_ui/bower_components/emojis/ ; done",
-            "cp $(location @ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js) $$TMP/polygerrit_ui/bower_components/webcomponentsjs/webcomponents-lite.js",
-            "cp $(location @ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js.map) $$TMP/polygerrit_ui/bower_components/webcomponentsjs/webcomponents-lite.js.map",
+            "cp $$WEBCOMPONENTJS_DIR/webcomponents-loader.js $$TMP/polygerrit_ui/bower_components/webcomponentsjs/webcomponents-loader.js",
+            "cp $$WEBCOMPONENTJS_DIR/bundles/* $$TMP/polygerrit_ui/bower_components/webcomponentsjs/bundles/",
             "cp $(location @ui_npm//:node_modules/resemblejs/resemble.js) $$TMP/polygerrit_ui/bower_components/resemblejs/resemble.js",
             "cp $$FONT_DIR/roboto/*.ttf $$TMP/polygerrit_ui/fonts/roboto/",
             "cp $$FONT_DIR/robotomono/*.ttf $$TMP/polygerrit_ui/fonts/robotomono/",
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 9996c75..a0ab3a3 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -75,6 +75,7 @@
 import {Finalizable} from '../types/types';
 import {GrSuggestionsService} from './suggestions/suggestions-service_impl';
 import {suggestionsServiceToken} from './suggestions/suggestions-service';
+import {FlowsModel, flowsModelToken} from '../models/flows/flows-model';
 /**
  * The AppContext lazy initializator for all services
  */
@@ -109,7 +110,7 @@
 ): Map<DependencyToken<unknown>, Creator<unknown>> {
   return new Map<DependencyToken<unknown>, Creator<unknown>>([
     [authServiceToken, () => appContext.authService],
-    [routerModelToken, () => new RouterModel()],
+    [routerModelToken, () => new RouterModel(resolver(pluginLoaderToken))],
     [userModelToken, () => new UserModel(appContext.restApiService)],
     [browserModelToken, () => new BrowserModel(resolver(userModelToken))],
     [accountsModelToken, () => new AccountsModel(appContext.restApiService)],
@@ -160,7 +161,8 @@
           appContext.restApiService,
           resolver(userModelToken),
           resolver(pluginLoaderToken),
-          appContext.reportingService
+          appContext.reportingService,
+          appContext.flagsService
         ),
     ],
     [
@@ -249,5 +251,6 @@
           resolver(changeModelToken)
         ),
     ],
+    [flowsModelToken, () => new FlowsModel(resolver(changeModelToken))],
   ]);
 }
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index b32d396..499a0bf 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -18,9 +18,12 @@
   NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
   CHECKS_DEVELOPER = 'UiFeature__checks_developer',
   PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
-  REVISION_PARENTS_DATA = 'UiFeature__revision_parents_data',
-  COMMENT_AUTOCOMPLETION = 'UiFeature__comment_autocompletion_enabled',
-  SAVE_PROJECT_CONFIG_FOR_REVIEW = 'UiFeature__save_project_config_for_review',
+  ML_SUGGESTED_EDIT_V2 = 'UiFeature__ml_suggested_edit_v2',
+  GET_AI_PROMPT = 'UiFeature__get_ai_prompt',
   PARALLEL_DASHBOARD_REQUESTS = 'UiFeature__parallel_dashboard_requests',
-  GET_AI_FIX = 'UiFeature__get_ai_fix',
+  ML_SUGGESTED_EDIT_UNCHECK_BY_DEFAULT = 'UiFeature__ml_suggested_edit_uncheck_by_default',
+  ML_SUGGESTED_EDIT_FEEDBACK = 'UiFeature__ml_suggested_edit_feedback',
+  ML_SUGGESTED_EDIT_EDITABLE_SUGGESTION = 'UiFeature__ml_suggested_edit_editable_suggestion',
+  SHOW_FLOWS_TAB = 'UiFeature__show_flows_tab',
+  ASYNC_SUBMIT_REQUIREMENTS = 'UiFeature__async_submit_requirements',
 }
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index af83699..4ce0dd7 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -15,7 +15,7 @@
   LifeCycle,
   Timing,
 } from '../../constants/reporting';
-import {onCLS, onLCP, Metric, onINP} from 'web-vitals';
+import {Metric, onCLS, onINP, onLCP} from 'web-vitals';
 import {getEventPath, isElementTarget} from '../../utils/dom-util';
 import {Finalizable} from '../../types/types';
 
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
index 86e1cd1..69decf2 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
@@ -6,8 +6,8 @@
 import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {
-  GrReporting,
   DEFAULT_STARTUP_TIMERS,
+  GrReporting,
   initErrorReporter,
   InteractionReporter,
 } from './gr-reporting_impl';
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index ee1fd9b..cd9fcc1 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -20,6 +20,7 @@
   AccountInfo,
   AccountStateInfo,
   ActionNameToActionInfoMap,
+  AuthTokenInfo,
   Base64File,
   Base64FileContent,
   Base64ImageFile,
@@ -46,15 +47,14 @@
   DeleteDraftCommentsInput,
   DiffPreferenceInput,
   DocResult,
-  EditInfo,
+  DraftInfo,
   EDIT,
+  EditInfo,
   EditPreferencesInfo,
   EmailAddress,
   EmailInfo,
-  EncodedGroupId,
   FileNameToFileInfoMap,
   FilePathToDiffInfoMap,
-  FixId,
   GitRef,
   GpgKeyId,
   GpgKeyInfo,
@@ -70,42 +70,40 @@
   HashtagsInput,
   ImagesForDiff,
   IncludedInInfo,
+  ListChangesOption,
   MergeableInfo,
   NameToProjectInfoMap,
   NumericChangeId,
   PARENT,
   ParsedJSON,
-  Password,
   PatchRange,
   PatchSetNum,
-  PathToRobotCommentsInfoMap,
   PluginInfo,
   PreferencesInfo,
   PreferencesInput,
   ProjectAccessInfo,
-  RepoAccessInfoMap,
   ProjectAccessInput,
   ProjectInfo,
   ProjectInfoWithName,
   ProjectInput,
   ProjectWatchInfo,
   RelatedChangesInfo,
+  RepoAccessInfoMap,
   RepoName,
   RequestPayload,
   ReviewInput,
+  ReviewResult,
   RevisionId,
   ServerInfo,
-  ValidationOptionsInfo,
   SshKeyInfo,
+  SubmitRequirementInfo,
   SubmittedTogetherInfo,
   SuggestedReviewerInfo,
   TagInfo,
   TagInput,
   TopMenuEntryInfo,
   UrlEncodedCommentId,
-  DraftInfo,
-  ListChangesOption,
-  ReviewResult,
+  ValidationOptionsInfo,
 } from '../../types/common';
 import {
   DiffInfo,
@@ -114,8 +112,8 @@
 } from '../../types/diff';
 import {
   GetDiffCommentsOutput,
-  GetDiffRobotCommentsOutput,
   RestApiService,
+  SubmittabilityInfo,
 } from './gr-rest-api';
 import {
   CommentSide,
@@ -134,7 +132,19 @@
 import {escapeAndWrapSearchOperatorValue} from '../../utils/string-util';
 import {FlagsService, KnownExperimentId} from '../flags/flags';
 import {RetryScheduler} from '../scheduler/retry-scheduler';
-import {FixReplacementInfo} from '../../api/rest-api';
+import {
+  BatchLabelInput,
+  BatchSubmitRequirementInput,
+  DeleteLabelInput,
+  FileInfo,
+  FixReplacementInfo,
+  FlowInfo,
+  FlowInput,
+  IsFlowsEnabledInfo,
+  LabelDefinitionInfo,
+  LabelDefinitionInput,
+  SubmitRequirementInput,
+} from '../../api/rest-api';
 import {
   FetchParams,
   FetchPromisesCache,
@@ -146,6 +156,7 @@
   SiteBasedCache,
   throwingErrorCallback,
 } from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {getAppContext} from '../app-context';
 
 const MAX_PROJECT_RESULTS = 25;
 
@@ -259,6 +270,8 @@
   // Used to serialize requests for certain RPCs
   readonly _serialScheduler: Scheduler<Response>;
 
+  private readonly flags = getAppContext().flagsService;
+
   constructor(
     private readonly authService: AuthService,
     private readonly flagService: FlagsService
@@ -405,7 +418,10 @@
     });
   }
 
-  createGroup(config: GroupInput & {name: string}): Promise<Response> {
+  createGroup(config: GroupInput): Promise<Response> {
+    if (!config.name) {
+      throw new Error('Group name is required');
+    }
     const encodeName = encodeURIComponent(config.name);
     return this._restApiHelper.fetch({
       fetchOptions: getFetchOptions({
@@ -583,8 +599,19 @@
     });
   }
 
+  deleteGroup(groupName: GroupName): Promise<Response> {
+    const encodeName = encodeURIComponent(groupName);
+    return this._restApiHelper.fetch({
+      fetchOptions: {
+        method: HttpMethod.DELETE,
+      },
+      url: `/groups/${encodeName}`,
+      anonymizedUrl: '/groups/*',
+    });
+  }
+
   getGroupAuditLog(
-    group: EncodedGroupId,
+    group: GroupId,
     errFn?: ErrorCallback
   ): Promise<GroupAuditEventInfo[] | undefined> {
     return this._restApiHelper.fetchCacheJSON({
@@ -1332,6 +1359,31 @@
     );
   }
 
+  async getAllRevisionFiles(
+    changeNum?: NumericChangeId
+  ): Promise<
+    {[revisionId: string]: {[filename: string]: FileInfo}} | undefined
+  > {
+    if (!changeNum) return;
+    const optionsHex = listChangesOptionsToHex(
+      ListChangesOption.ALL_REVISIONS,
+      ListChangesOption.ALL_FILES
+    );
+
+    const change = await this.getChange(changeNum, undefined, optionsHex);
+    if (!change?.revisions) {
+      return undefined;
+    }
+    const result: {[revisionId: string]: {[filename: string]: FileInfo}} = {};
+    for (const revId in change.revisions) {
+      if (!change.revisions[revId]?.files) {
+        continue;
+      }
+      result[revId] = change.revisions[revId].files!;
+    }
+    return result;
+  }
+
   private getListChangesOptionsHex() {
     if (
       window.DEFAULT_DETAIL_HEXES &&
@@ -1374,17 +1426,17 @@
       ListChangesOption.DOWNLOAD_COMMANDS,
       ListChangesOption.MESSAGES,
       ListChangesOption.REVIEWER_UPDATES,
-      ListChangesOption.SUBMITTABLE,
       ListChangesOption.WEB_LINKS,
       ListChangesOption.SKIP_DIFFSTAT,
-      ListChangesOption.SUBMIT_REQUIREMENTS,
+      ListChangesOption.PARENTS,
     ];
-    if (this.flagService.isEnabled(KnownExperimentId.REVISION_PARENTS_DATA)) {
-      options.push(ListChangesOption.PARENTS);
-    }
     if (config?.receive?.enable_signed_push) {
       options.push(ListChangesOption.PUSH_CERTIFICATES);
     }
+    if (!this.flags.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)) {
+      options.push(ListChangesOption.SUBMITTABLE);
+      options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
+    }
     return options;
   }
 
@@ -1443,6 +1495,36 @@
     );
   }
 
+  async getSubmittabilityInfo(
+    changeNum: NumericChangeId
+  ): Promise<SubmittabilityInfo | undefined> {
+    if (!this.flags.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)) {
+      return undefined;
+    }
+    const optionsHex = listChangesOptionsToHex(
+      ListChangesOption.SUBMITTABLE,
+      ListChangesOption.SUBMIT_REQUIREMENTS,
+      ListChangesOption.SKIP_DIFFSTAT
+    );
+    const change = await this.getChange(
+      changeNum,
+      /* errFn=*/ undefined,
+      optionsHex
+    );
+    if (
+      !change ||
+      change.submittable === undefined ||
+      change.submit_requirements === undefined
+    ) {
+      return undefined;
+    }
+    return {
+      changeNum,
+      submittable: change.submittable,
+      submitRequirements: change.submit_requirements,
+    };
+  }
+
   async getChangeCommitInfo(
     changeNum: NumericChangeId,
     patchNum: PatchSetNum
@@ -1533,10 +1615,10 @@
 
   getChangeSuggestedReviewers(
     changeNum: NumericChangeId,
-    inputVal: string,
+    inputVal?: string,
     errFn?: ErrorCallback
   ) {
-    return this._getChangeSuggestedGroup(
+    return this.getChangeSuggestedGroup(
       ReviewerState.REVIEWER,
       changeNum,
       inputVal,
@@ -1546,10 +1628,10 @@
 
   getChangeSuggestedCCs(
     changeNum: NumericChangeId,
-    inputVal: string,
+    inputVal?: string,
     errFn?: ErrorCallback
   ) {
-    return this._getChangeSuggestedGroup(
+    return this.getChangeSuggestedGroup(
       ReviewerState.CC,
       changeNum,
       inputVal,
@@ -1557,10 +1639,10 @@
     );
   }
 
-  async _getChangeSuggestedGroup(
+  private async getChangeSuggestedGroup(
     reviewerState: ReviewerState,
     changeNum: NumericChangeId,
-    inputVal: string,
+    inputVal?: string, // If undefined, then default suggestions are returned
     errFn?: ErrorCallback
   ): Promise<SuggestedReviewerInfo[] | undefined> {
     // More suggestions may obscure content underneath in the reply dialog,
@@ -1773,6 +1855,189 @@
     });
   }
 
+  getRepoSubmitRequirements(
+    repoName: RepoName,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo[] | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(repoName)}/submit_requirements`,
+      errFn,
+      anonymizedUrl: '/projects/*/submit_requirements',
+    }) as Promise<SubmitRequirementInfo[] | undefined>;
+  }
+
+  createSubmitRequirement(
+    repoName: RepoName,
+    input: SubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/submit_requirements/${encodeURIComponent(input.name)}`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.PUT,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/submit_requirements/*',
+    }) as Promise<SubmitRequirementInfo | undefined>;
+  }
+
+  updateSubmitRequirement(
+    repoName: RepoName,
+    submitRequirementName: string,
+    input: SubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/submit_requirements/${encodeURIComponent(submitRequirementName)}`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.PUT,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/submit_requirements/*',
+    }) as Promise<SubmitRequirementInfo | undefined>;
+  }
+
+  deleteSubmitRequirement(
+    repoName: RepoName,
+    submitRequirementName: string,
+    errFn?: ErrorCallback
+  ): Promise<Response> {
+    return this._restApiHelper.fetch({
+      fetchOptions: {method: HttpMethod.DELETE},
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/submit_requirements/${encodeURIComponent(submitRequirementName)}`,
+      errFn,
+      anonymizedUrl: '/projects/*/submit_requirements/*',
+      reportServerError: true,
+    });
+  }
+
+  saveRepoSubmitRequirementsForReview(
+    repoName: RepoName,
+    input: BatchSubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<ChangeInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/submit_requirements:review`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.POST,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/submit_requirements:review',
+    }) as Promise<ChangeInfo | undefined>;
+  }
+
+  getRepoLabels(
+    repoName: RepoName,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo[] | undefined> {
+    const params: {[key: string]: string} = {};
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(repoName)}/labels/`,
+      params,
+      errFn,
+      anonymizedUrl: '/projects/*/labels/',
+    }) as Promise<LabelDefinitionInfo[] | undefined>;
+  }
+
+  getRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/labels/${encodeURIComponent(labelName)}`,
+      errFn,
+      anonymizedUrl: '/projects/*/labels/*',
+    }) as Promise<LabelDefinitionInfo | undefined>;
+  }
+
+  createRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    input: LabelDefinitionInput,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/labels/${encodeURIComponent(labelName)}`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.PUT,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/labels/*',
+    }) as Promise<LabelDefinitionInfo | undefined>;
+  }
+
+  saveRepoLabelsForReview(
+    repoName: RepoName,
+    input: BatchLabelInput,
+    errFn?: ErrorCallback
+  ): Promise<ChangeInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(repoName)}/labels:review`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.POST,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/labels:review',
+    }) as Promise<ChangeInfo | undefined>;
+  }
+
+  updateRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    input: LabelDefinitionInput,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/labels/${encodeURIComponent(labelName)}`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.PUT,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/labels/*',
+    }) as Promise<LabelDefinitionInfo | undefined>;
+  }
+
+  deleteRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    input?: DeleteLabelInput,
+    errFn?: ErrorCallback
+  ): Promise<Response> {
+    return this._restApiHelper.fetch({
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.DELETE,
+        body: input,
+      }),
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/labels/${encodeURIComponent(labelName)}`,
+      errFn,
+      anonymizedUrl: '/projects/*/labels/*',
+      reportServerError: true,
+    });
+  }
+
   getRepoAccessRights(
     repoName: RepoName,
     errFn?: ErrorCallback
@@ -2389,19 +2654,6 @@
     }) as Promise<FilePathToDiffInfoMap | undefined>;
   }
 
-  async getRobotCommentFixPreview(
-    changeNum: NumericChangeId,
-    patchNum: PatchSetNum,
-    fixId: FixId
-  ): Promise<FilePathToDiffInfoMap | undefined> {
-    const url = await this._changeBaseURL(changeNum, patchNum);
-    const endpoint = `/fixes/${encodeURIComponent(fixId)}/preview`;
-    return this._restApiHelper.fetchJSON({
-      url: `${url}${endpoint}`,
-      anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}${endpoint}`,
-    }) as Promise<FilePathToDiffInfoMap | undefined>;
-  }
-
   async applyFixSuggestion(
     changeNum: NumericChangeId,
     fixPatchNum: PatchSetNum,
@@ -2435,21 +2687,6 @@
     });
   }
 
-  async applyRobotFixSuggestion(
-    changeNum: NumericChangeId,
-    patchNum: PatchSetNum,
-    fixId: string
-  ): Promise<Response> {
-    const url = await this._changeBaseURL(changeNum, patchNum);
-    const endpoint = `/fixes/${encodeURIComponent(fixId)}/apply`;
-    return this._restApiHelper.fetch({
-      fetchOptions: {method: HttpMethod.POST},
-      url: `${url}${endpoint}`,
-      anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}${endpoint}`,
-      reportServerError: true,
-    });
-  }
-
   async publishChangeEdit(changeNum: NumericChangeId) {
     const url = await this._changeBaseURL(changeNum);
     return this._restApiHelper.fetch({
@@ -2621,37 +2858,6 @@
     );
   }
 
-  getDiffRobotComments(
-    changeNum: NumericChangeId
-  ): Promise<PathToRobotCommentsInfoMap | undefined>;
-
-  getDiffRobotComments(
-    changeNum: NumericChangeId,
-    basePatchNum: BasePatchSetNum,
-    patchNum: PatchSetNum,
-    path: string
-  ): Promise<GetDiffRobotCommentsOutput>;
-
-  getDiffRobotComments(
-    changeNum: NumericChangeId,
-    basePatchNum?: BasePatchSetNum,
-    patchNum?: PatchSetNum,
-    path?: string
-  ) {
-    if (!basePatchNum && !patchNum && !path) {
-      return this._getDiffComments(changeNum, '/robotcomments');
-    }
-
-    return this._getDiffComments(
-      changeNum,
-      '/robotcomments',
-      undefined,
-      basePatchNum,
-      patchNum,
-      path
-    );
-  }
-
   async getDiffDrafts(
     changeNum: NumericChangeId
   ): Promise<{[path: string]: DraftInfo[]} | undefined> {
@@ -2695,11 +2901,6 @@
 
   _getDiffComments(
     changeNum: NumericChangeId,
-    endpoint: '/robotcomments'
-  ): Promise<PathToRobotCommentsInfoMap | undefined>;
-
-  _getDiffComments(
-    changeNum: NumericChangeId,
     endpoint: '/comments' | '/drafts',
     params?: FetchParams,
     basePatchNum?: BasePatchSetNum,
@@ -2707,15 +2908,6 @@
     path?: string
   ): Promise<GetDiffCommentsOutput>;
 
-  _getDiffComments(
-    changeNum: NumericChangeId,
-    endpoint: '/robotcomments',
-    params?: FetchParams,
-    basePatchNum?: BasePatchSetNum,
-    patchNum?: PatchSetNum,
-    path?: string
-  ): Promise<GetDiffRobotCommentsOutput>;
-
   /**
    * Fetches the comments for a given patchNum.
    * Helper function to make promises more legible.
@@ -2728,11 +2920,7 @@
     patchNum?: PatchSetNum,
     path?: string
   ): Promise<
-    | GetDiffCommentsOutput
-    | GetDiffRobotCommentsOutput
-    | {[path: string]: CommentInfo[]}
-    | PathToRobotCommentsInfoMap
-    | undefined
+    GetDiffCommentsOutput | {[path: string]: CommentInfo[]} | undefined
   > {
     // We don't want to add accept header, since preloading of comments is
     // working only without accept header.
@@ -2747,9 +2935,7 @@
           },
           noAcceptHeader
         )
-      ) as Promise<
-        {[path: string]: CommentInfo[]} | PathToRobotCommentsInfoMap | undefined
-      >;
+      ) as Promise<{[path: string]: CommentInfo[]} | undefined>;
 
     if (!basePatchNum && !patchNum && !path) {
       return fetchComments();
@@ -3129,24 +3315,45 @@
     }) as unknown as Promise<Hashtag[] | undefined>;
   }
 
-  deleteAccountHttpPassword(): Promise<Response> {
+  getAccountAuthTokens(): Promise<AuthTokenInfo[] | undefined> {
+    const errFn: ErrorCallback = (response?: Response | null, _err?: Error) => {
+      if (!response || response.status === 405 || response.status === 404) {
+        return;
+      }
+      fireServerError(response, req);
+    };
+    const req = {
+      url: '/accounts/self/tokens',
+      reportUrlAsIs: true,
+      reportServerError: true,
+      errFn,
+    };
+    return this._restApiHelper.fetchJSON(req) as Promise<
+      AuthTokenInfo[] | undefined
+    >;
+  }
+
+  deleteAccountAuthToken(tokenId: string): Promise<Response> {
     return this._restApiHelper.fetch({
       fetchOptions: {method: HttpMethod.DELETE},
-      url: '/accounts/self/password.http',
+      url: `/accounts/self/tokens/${tokenId}`,
       reportUrlAsIs: true,
       reportServerError: true,
     });
   }
 
-  generateAccountHttpPassword(): Promise<Password | undefined> {
+  generateAccountAuthToken(
+    tokenId: string,
+    lifetime: string
+  ): Promise<AuthTokenInfo | undefined> {
     return this._restApiHelper.fetchJSON({
       fetchOptions: getFetchOptions({
         method: HttpMethod.PUT,
-        body: {generate: true},
+        body: {id: tokenId, lifetime},
       }),
-      url: '/accounts/self/password.http',
+      url: `/accounts/self/tokens/${tokenId}`,
       reportUrlAsIs: true,
-    }) as Promise<unknown> as Promise<Password>;
+    }) as Promise<unknown> as Promise<AuthTokenInfo>;
   }
 
   getAccountSSHKeys() {
@@ -3442,6 +3649,28 @@
     }) as Promise<BlameInfo[] | undefined>;
   }
 
+  async getPatchContent(
+    changeNum: NumericChangeId,
+    patchNum: PatchSetNum,
+    context?: number
+  ): Promise<string | undefined> {
+    const url = await this._changeBaseURL(changeNum, patchNum);
+    const params: {[key: string]: string | number} = {
+      download: '',
+      raw: '',
+    };
+    if (context !== undefined && context !== 3) {
+      params['context'] = context;
+    }
+    const response = await this._restApiHelper.fetch({
+      url: `${url}/patch`,
+      params,
+      anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/patch`,
+    });
+    if (!response?.ok) return undefined;
+    return await response.text();
+  }
+
   /**
    * Modify the given create draft request promise so that it fails and throws
    * an error if the response bears HTTP status 200 instead of HTTP 201.
@@ -3523,4 +3752,74 @@
       url: '/accounts/self/drafts:delete',
     });
   }
+
+  async getFlow(
+    changeNum: NumericChangeId,
+    flowId: string,
+    errFn?: ErrorCallback
+  ): Promise<FlowInfo | undefined> {
+    const url = await this._changeBaseURL(changeNum);
+    return this._restApiHelper.fetchJSON({
+      url: `${url}/flows/${flowId}`,
+      errFn,
+      anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/flows/*`,
+    }) as Promise<FlowInfo | undefined>;
+  }
+
+  async listFlows(
+    changeNum: NumericChangeId,
+    errFn?: ErrorCallback
+  ): Promise<FlowInfo[] | undefined> {
+    const url = await this._changeBaseURL(changeNum);
+    return this._restApiHelper.fetchJSON({
+      url: `${url}/flows`,
+      errFn,
+      anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/flows`,
+    }) as Promise<FlowInfo[] | undefined>;
+  }
+
+  async getIfFlowsIsEnabled(
+    changeNum: NumericChangeId,
+    errFn?: ErrorCallback
+  ): Promise<IsFlowsEnabledInfo | undefined> {
+    const url = await this._changeBaseURL(changeNum);
+    return this._restApiHelper.fetchJSON({
+      url: `${url}/is-flows-enabled`,
+      errFn,
+      anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/is-flows-enabled`,
+    }) as Promise<IsFlowsEnabledInfo | undefined>;
+  }
+
+  async createFlow(
+    changeNum: NumericChangeId,
+    flow: FlowInput,
+    errFn?: ErrorCallback
+  ): Promise<FlowInfo | undefined> {
+    const url = await this._changeBaseURL(changeNum);
+    return this._restApiHelper.fetchJSON({
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.POST,
+        body: flow,
+      }),
+      url: `${url}/flows`,
+      errFn,
+      anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/flows`,
+    }) as Promise<FlowInfo | undefined>;
+  }
+
+  async deleteFlow(
+    changeNum: NumericChangeId,
+    flowId: string,
+    errFn?: ErrorCallback
+  ): Promise<Response> {
+    const url = await this._changeBaseURL(changeNum);
+    return this._restApiHelper.fetch({
+      fetchOptions: {
+        method: HttpMethod.DELETE,
+      },
+      url: `${url}/flows/${flowId}`,
+      errFn,
+      anonymizedUrl: `${ANONYMIZED_CHANGE_BASE_URL}/flows/*`,
+    });
+  }
 }
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
index b3447a1..78ac93f 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -33,6 +33,7 @@
   createDefaultEditPrefs,
   HttpMethod,
 } from '../../constants/constants';
+import {FlowInput} from '../../api/rest-api';
 import {
   AccountDetailInfo,
   BasePatchSetNum,
@@ -50,7 +51,6 @@
   PatchSetNum,
   RepoName,
   RevisionPatchSetNum,
-  RobotCommentInfo,
   Timestamp,
   UrlEncodedCommentId,
 } from '../../types/common';
@@ -128,13 +128,13 @@
       message: 'how did this work in the first place?',
       path: 'sieve.go',
       updated: '2017-02-03 22:33:28.000000000' as Timestamp,
-    } as RobotCommentInfo);
+    } as CommentInfo);
     assert.equal(obj.comments.length, 1);
     assert.deepEqual(obj.comments[0], {
       message: 'this isn’t quite right',
       path: 'sieve.go',
       updated: '2017-02-03 22:32:28.000000000' as Timestamp,
-    } as RobotCommentInfo);
+    } as CommentInfo);
   });
 
   test('_setRange', () => {
@@ -296,18 +296,18 @@
       message: 'this isn’t quite right',
       path: 'sieve.go',
       updated: '2017-02-03 22:32:28.000000000' as Timestamp,
-    } as RobotCommentInfo);
+    } as CommentInfo);
     assert.equal(obj.comments.length, 2);
     assert.deepEqual(obj.comments[0], {
       message: 'What on earth are you thinking, here?',
       path: 'sieve.go',
       updated: '2017-02-03 22:32:28.000000000' as Timestamp,
-    } as RobotCommentInfo);
+    } as CommentInfo);
     assert.deepEqual(obj.comments[1], {
       message: '¯\\_(ツ)_/¯',
       path: 'sieve.go',
       updated: '2017-02-04 22:33:28.000000000' as Timestamp,
-    } as RobotCommentInfo);
+    } as CommentInfo);
   });
 
   test('legacy n,z key in change url is replaced', async () => {
@@ -627,9 +627,11 @@
       {username: 'john'}
     );
     assert.deepEqual(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.username,
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).username,
       'john'
     );
   });
@@ -649,9 +651,11 @@
       HttpMethod.PUT
     );
     assert.isUndefined(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.username
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).username
     );
   });
 
@@ -675,9 +679,11 @@
       {display_name: 'john'}
     );
     assert.deepEqual(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.display_name,
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).display_name,
       'john'
     );
   });
@@ -697,9 +703,11 @@
       HttpMethod.PUT
     );
     assert.isUndefined(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.display_name
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).display_name
     );
   });
 
@@ -723,9 +731,11 @@
       {name: 'john'}
     );
     assert.deepEqual(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.name,
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).name,
       'john'
     );
   });
@@ -745,9 +755,11 @@
       HttpMethod.PUT
     );
     assert.isUndefined(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.name
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).name
     );
   });
 
@@ -771,9 +783,11 @@
       {status: 'OOO'}
     );
     assert.deepEqual(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.status,
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).status,
       'OOO'
     );
   });
@@ -793,9 +807,11 @@
       HttpMethod.PUT
     );
     assert.isUndefined(
-      (element._cache.get(
-        '/accounts/self/detail'
-      ) as unknown as AccountDetailInfo)!.status
+      (
+        element._cache.get(
+          '/accounts/self/detail'
+        ) as unknown as AccountDetailInfo
+      ).status
     );
   });
 
@@ -1655,20 +1671,20 @@
     });
     assert.isTrue(fetchStub.calledOnce);
     assert.sameDeepMembers(
-      JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string).add!,
+      JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string).add,
       ['foo-bar']
     );
   });
 
-  test('generateAccountHttpPassword', async () => {
+  test('generateAccountAuthToken', async () => {
     const fetchStub = sinon
       .stub(element._restApiHelper, 'fetchJSON')
       .resolves();
-    await element.generateAccountHttpPassword();
+    await element.generateAccountAuthToken('token1', '3d');
     assert.isTrue(fetchStub.calledOnce);
     assert.deepEqual(
       JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
-      {generate: true}
+      {id: 'token1', lifetime: '3d'}
     );
   });
 
@@ -1704,8 +1720,8 @@
         '/changes/test-project~123/revisions/5/files'
       );
       assert.isOk(fetchStub.lastCall.args[0].params);
-      assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
-      assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+      assert.equal(fetchStub.lastCall.args[0].params.base, 4);
+      assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
     });
 
     test('parent index', async () => {
@@ -1724,8 +1740,8 @@
         '/changes/test-project~123/revisions/5/files'
       );
       assert.isOk(fetchStub.lastCall.args[0].params);
-      assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
-      assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
+      assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+      assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
     });
   });
 
@@ -1747,8 +1763,8 @@
         '/changes/test-project~123/revisions/2/files/foo%2Fbar.baz/diff'
       );
       assert.isOk(fetchStub.lastCall.args[0].params);
-      assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
-      assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+      assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+      assert.isNotOk(fetchStub.lastCall.args[0].params.base);
     });
 
     test('simple range', async () => {
@@ -1768,8 +1784,8 @@
         '/changes/test-project~123/revisions/5/files/foo%2Fbar.baz/diff'
       );
       assert.isOk(fetchStub.lastCall.args[0].params);
-      assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
-      assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
+      assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+      assert.equal(fetchStub.lastCall.args[0].params.base, 4);
     });
 
     test('parent index', async () => {
@@ -1789,8 +1805,8 @@
         '/changes/test-project~123/revisions/5/files/foo%2Fbar.baz/diff'
       );
       assert.isOk(fetchStub.lastCall.args[0].params);
-      assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
-      assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
+      assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+      assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
     });
   });
 
@@ -2037,4 +2053,73 @@
       assert.deepEqual(body.original_patchset_for_fix, 1);
     });
   });
+
+  suite('flow api', () => {
+    const changeNum = 123 as NumericChangeId;
+    const flowId = 'test-flow-id';
+    let fetchJSONStub: sinon.SinonStub;
+    let fetchStub: sinon.SinonStub;
+
+    setup(() => {
+      element.addRepoNameToCache(changeNum, TEST_PROJECT_NAME);
+      fetchJSONStub = sinon
+        .stub(element._restApiHelper, 'fetchJSON')
+        .resolves();
+      fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
+    });
+
+    test('getFlow', async () => {
+      await element.getFlow(changeNum, flowId);
+      assert.isTrue(fetchJSONStub.calledOnce);
+      assert.equal(
+        fetchJSONStub.lastCall.args[0].url,
+        `/changes/test-project~${changeNum}/flows/${flowId}`
+      );
+    });
+
+    test('listFlows', async () => {
+      await element.listFlows(changeNum);
+      assert.isTrue(fetchJSONStub.calledOnce);
+      assert.equal(
+        fetchJSONStub.lastCall.args[0].url,
+        `/changes/test-project~${changeNum}/flows`
+      );
+    });
+
+    test('getIfFlowsIsEnabled', async () => {
+      await element.getIfFlowsIsEnabled(changeNum);
+      assert.isTrue(fetchJSONStub.calledOnce);
+      assert.equal(
+        fetchJSONStub.lastCall.args[0].url,
+        `/changes/test-project~${changeNum}/is-flows-enabled`
+      );
+    });
+
+    test('createFlow', async () => {
+      const flow: FlowInput = {
+        stage_expressions: [{condition: 'branch:refs/heads/main'}],
+      };
+      await element.createFlow(changeNum, flow);
+      assert.isTrue(fetchJSONStub.calledOnce);
+      assert.equal(
+        fetchJSONStub.lastCall.args[0].url,
+        `/changes/test-project~${changeNum}/flows`
+      );
+      assert.equal(fetchJSONStub.lastCall.args[0].fetchOptions.method, 'POST');
+      assert.deepEqual(
+        JSON.parse(fetchJSONStub.lastCall.args[0].fetchOptions.body),
+        flow
+      );
+    });
+
+    test('deleteFlow', async () => {
+      await element.deleteFlow(changeNum, flowId);
+      assert.isTrue(fetchStub.calledOnce);
+      assert.equal(
+        fetchStub.lastCall.args[0].url,
+        `/changes/test-project~${changeNum}/flows/${flowId}`
+      );
+      assert.equal(fetchStub.lastCall.args[0].fetchOptions!.method, 'DELETE');
+    });
+  });
 });
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 8dbf1e9..3a37599 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -12,6 +12,7 @@
   AccountInfo,
   AccountStateInfo,
   ActionNameToActionInfoMap,
+  AuthTokenInfo,
   Base64FileContent,
   BasePatchSetNum,
   BlameInfo,
@@ -33,14 +34,13 @@
   DashboardInfo,
   DiffPreferenceInput,
   DocResult,
+  DraftInfo,
   EditInfo,
   EditPreferencesInfo,
   EmailAddress,
   EmailInfo,
-  EncodedGroupId,
   FileNameToFileInfoMap,
   FilePathToDiffInfoMap,
-  FixId,
   GitRef,
   GpgKeyId,
   GpgKeyInfo,
@@ -59,29 +59,27 @@
   MergeableInfo,
   NameToProjectInfoMap,
   NumericChangeId,
-  Password,
   PatchRange,
   PatchSetNum,
-  PathToRobotCommentsInfoMap,
   PluginInfo,
   PreferencesInfo,
   PreferencesInput,
   ProjectAccessInfo,
-  RepoAccessInfoMap,
   ProjectAccessInput,
   ProjectInfo,
   ProjectInfoWithName,
   ProjectInput,
   ProjectWatchInfo,
   RelatedChangesInfo,
+  RepoAccessInfoMap,
   RepoName,
   RequestPayload,
   ReviewInput,
+  ReviewResult,
   RevisionId,
-  RobotCommentInfo,
   ServerInfo,
-  ValidationOptionsInfo,
   SshKeyInfo,
+  SubmitRequirementInfo,
   SubmittedTogetherInfo,
   SuggestedReviewerInfo,
   TagInfo,
@@ -89,8 +87,7 @@
   TopMenuEntryInfo,
   UrlEncodedCommentId,
   UserId,
-  DraftInfo,
-  ReviewResult,
+  ValidationOptionsInfo,
 } from '../../types/common';
 import {
   DiffInfo,
@@ -99,16 +96,30 @@
 } from '../../types/diff';
 import {Finalizable, ParsedChangeInfo} from '../../types/types';
 import {ErrorCallback} from '../../api/rest';
-import {FixReplacementInfo} from '../../api/rest-api';
+import {
+  BatchLabelInput,
+  BatchSubmitRequirementInput,
+  DeleteLabelInput,
+  FileInfo,
+  FixReplacementInfo,
+  FlowInfo,
+  FlowInput,
+  IsFlowsEnabledInfo,
+  LabelDefinitionInfo,
+  LabelDefinitionInput,
+  SubmitRequirementInput,
+  SubmitRequirementResultInfo,
+} from '../../api/rest-api';
 
 export interface GetDiffCommentsOutput {
   baseComments: CommentInfo[];
   comments: CommentInfo[];
 }
 
-export interface GetDiffRobotCommentsOutput {
-  baseComments: RobotCommentInfo[];
-  comments: RobotCommentInfo[];
+export interface SubmittabilityInfo {
+  changeNum: NumericChangeId;
+  submittable: boolean;
+  submitRequirements: SubmitRequirementResultInfo[];
 }
 
 export interface RestApiService extends Finalizable {
@@ -203,6 +214,23 @@
   ): Promise<ParsedChangeInfo | undefined>;
 
   /**
+   * Returns information about submittability and Submit Requirements.
+   */
+  getSubmittabilityInfo(
+    changeNum: NumericChangeId
+  ): Promise<SubmittabilityInfo | undefined>;
+
+  /**
+   * For every revision of the change returns the list of FileInfo for files
+   * which are modified compared to revision's parent.
+   */
+  getAllRevisionFiles(
+    changeNum?: NumericChangeId
+  ): Promise<
+    {[revisionId: string]: {[filename: string]: FileInfo}} | undefined
+  >;
+
+  /**
    * Given a changeNum, gets the change.
    *
    * If the project is known for the specified changeNum uses
@@ -285,6 +313,74 @@
     errFn?: ErrorCallback
   ): Promise<string[] | undefined>;
 
+  getRepoSubmitRequirements(
+    repoName: RepoName,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo[] | undefined>;
+
+  createSubmitRequirement(
+    repoName: RepoName,
+    input: SubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo | undefined>;
+
+  updateSubmitRequirement(
+    repoName: RepoName,
+    submitRequirementName: string,
+    input: SubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo | undefined>;
+
+  deleteSubmitRequirement(
+    repoName: RepoName,
+    submitRequirementName: string,
+    errFn?: ErrorCallback
+  ): Promise<Response>;
+
+  saveRepoSubmitRequirementsForReview(
+    repoName: RepoName,
+    input: BatchSubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<ChangeInfo | undefined>;
+
+  getRepoLabels(
+    repoName: RepoName,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo[] | undefined>;
+
+  getRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo | undefined>;
+
+  createRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    input: LabelDefinitionInput,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo | undefined>;
+
+  updateRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    input: LabelDefinitionInput,
+    errFn?: ErrorCallback
+  ): Promise<LabelDefinitionInfo | undefined>;
+
+  saveRepoLabelsForReview(
+    repoName: RepoName,
+    input: BatchLabelInput,
+    errFn?: ErrorCallback
+  ): Promise<ChangeInfo | undefined>;
+
+  deleteRepoLabel(
+    repoName: RepoName,
+    labelName: string,
+    input?: DeleteLabelInput,
+    errFn?: ErrorCallback
+  ): Promise<Response>;
+
   getRepoAccessRights(
     repoName: RepoName,
     errFn?: ErrorCallback
@@ -358,6 +454,8 @@
     options: GroupOptionsInput
   ): Promise<Response>;
 
+  deleteGroup(name: GroupName): Promise<Response>;
+
   saveChangeReview(
     changeNum: ChangeId | NumericChangeId,
     patchNum: RevisionId,
@@ -443,24 +541,6 @@
     | Promise<{[path: string]: CommentInfo[]} | undefined>
     | Promise<GetDiffCommentsOutput>;
 
-  getDiffRobotComments(
-    changeNum: NumericChangeId
-  ): Promise<PathToRobotCommentsInfoMap | undefined>;
-  getDiffRobotComments(
-    changeNum: NumericChangeId,
-    basePatchNum: PatchSetNum,
-    patchNum: PatchSetNum,
-    path: string
-  ): Promise<GetDiffRobotCommentsOutput>;
-  getDiffRobotComments(
-    changeNum: NumericChangeId,
-    basePatchNum?: BasePatchSetNum,
-    patchNum?: PatchSetNum,
-    path?: string
-  ):
-    | Promise<GetDiffRobotCommentsOutput>
-    | Promise<PathToRobotCommentsInfoMap | undefined>;
-
   /**
    * If the user is logged in, fetch the user's draft diff comments. If there
    * is no logged in user, the request is not made and the promise yields an
@@ -470,7 +550,7 @@
     changeNum: NumericChangeId
   ): Promise<{[path: string]: DraftInfo[]} | undefined>;
 
-  createGroup(config: GroupInput & {name: string}): Promise<Response>;
+  createGroup(config: GroupInput): Promise<Response>;
 
   getPlugins(
     filter: string,
@@ -522,7 +602,14 @@
 
   saveAccountAgreement(name: ContributorAgreementInput): Promise<Response>;
 
-  generateAccountHttpPassword(): Promise<Password | undefined>;
+  getAccountAuthTokens(): Promise<AuthTokenInfo[] | undefined>;
+
+  deleteAccountAuthToken(tokenId: string): Promise<Response>;
+
+  generateAccountAuthToken(
+    tokenId: string,
+    lifetime: string
+  ): Promise<AuthTokenInfo | undefined>;
 
   setAccountName(name: string): Promise<void>;
 
@@ -594,7 +681,7 @@
   ): Promise<Response | undefined>;
 
   getGroupAuditLog(
-    group: EncodedGroupId,
+    group: GroupId,
     errFn?: ErrorCallback
   ): Promise<GroupAuditEventInfo[] | undefined>;
 
@@ -701,17 +788,6 @@
   awaitPendingDiffDrafts(): Promise<void>;
 
   /**
-   * Preview Stored Fix
-   * Gets the diffs of all files for a certain {fix-id} associated with apply fix.
-   * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#preview-stored-fix
-   */
-  getRobotCommentFixPreview(
-    changeNum: NumericChangeId,
-    patchNum: PatchSetNum,
-    fixId: FixId
-  ): Promise<FilePathToDiffInfoMap | undefined>;
-
-  /**
    * Preview Provided fix
    * Gets the diffs of all files for a provided fix replacements infos
    * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#preview-provided-fix
@@ -735,16 +811,6 @@
   ): Promise<Response>;
 
   /**
-   * Apply Stored Fix
-   * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#apply-stored-fix
-   */
-  applyRobotFixSuggestion(
-    changeNum: NumericChangeId,
-    patchNum: PatchSetNum,
-    fixId: string
-  ): Promise<Response>;
-
-  /**
    * @param basePatchNum Negative values specify merge parent
    * index.
    * @param whitespace the ignore-whitespace level for the diff
@@ -772,6 +838,12 @@
     base?: boolean
   ): Promise<BlameInfo[] | undefined>;
 
+  getPatchContent(
+    changeNum: NumericChangeId,
+    patchNum: PatchSetNum,
+    context?: number
+  ): Promise<string | undefined>;
+
   getImagesForDiff(
     changeNum: NumericChangeId,
     diff: DiffInfo,
@@ -880,4 +952,32 @@
   getValidationOptions(
     changeNum: NumericChangeId
   ): Promise<ValidationOptionsInfo | undefined>;
+
+  getFlow(
+    changeNum: NumericChangeId,
+    flowId: string,
+    errFn?: ErrorCallback
+  ): Promise<FlowInfo | undefined>;
+
+  listFlows(
+    changeNum: NumericChangeId,
+    errFn?: ErrorCallback
+  ): Promise<FlowInfo[] | undefined>;
+
+  getIfFlowsIsEnabled(
+    changeNum: NumericChangeId,
+    errFn?: ErrorCallback
+  ): Promise<IsFlowsEnabledInfo | undefined>;
+
+  createFlow(
+    changeNum: NumericChangeId,
+    flow: FlowInput,
+    errFn?: ErrorCallback
+  ): Promise<FlowInfo | undefined>;
+
+  deleteFlow(
+    changeNum: NumericChangeId,
+    flowId: string,
+    errFn?: ErrorCallback
+  ): Promise<Response>;
 }
diff --git a/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
index 34187fe..7181977 100644
--- a/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
+++ b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
@@ -13,7 +13,7 @@
   SuggestedReviewerInfo,
   Suggestion,
 } from '../../types/common';
-import {AutocompleteSuggestion} from '../../elements/shared/gr-autocomplete/gr-autocomplete';
+import {AutocompleteSuggestion} from '../../utils/autocomplete-util';
 import {allSettled, isFulfilled} from '../../utils/async-util';
 import {isDefined, ParsedChangeInfo} from '../../types/types';
 import {
diff --git a/polygerrit-ui/app/services/highlight/highlight-service.ts b/polygerrit-ui/app/services/highlight/highlight-service.ts
index d59431b..6232a89 100644
--- a/polygerrit-ui/app/services/highlight/highlight-service.ts
+++ b/polygerrit-ui/app/services/highlight/highlight-service.ts
@@ -5,11 +5,11 @@
  */
 import {define} from '../../models/dependency';
 import {
-  SyntaxWorkerRequest,
-  SyntaxWorkerInit,
-  SyntaxWorkerResult,
-  SyntaxWorkerMessageType,
   SyntaxLayerLine,
+  SyntaxWorkerInit,
+  SyntaxWorkerMessageType,
+  SyntaxWorkerRequest,
+  SyntaxWorkerResult,
 } from '../../types/syntax-worker-api';
 import {Finalizable} from '../../types/types';
 import {prependOrigin} from '../../utils/url-util';
@@ -48,10 +48,10 @@
  */
 export class HighlightService implements Finalizable {
   // visible for testing
-  poolIdle: Set<Worker> = new Set();
+  poolIdle: Set<Worker | undefined> = new Set();
 
   // visible for testing
-  poolBusy: Set<Worker> = new Set();
+  poolBusy: Set<Worker | undefined> = new Set();
 
   // visible for testing
   /** Queue for waiting that a worker becomes available. */
@@ -105,7 +105,7 @@
    * If there is worker in the idle pool, then return it. Otherwise wait for a
    * worker to become a available.
    */
-  private async requestWorker(): Promise<Worker> {
+  private async requestWorker(): Promise<Worker | undefined> {
     if (this.poolIdle.size > 0) {
       const worker = this.moveIdleToBusy();
       return Promise.resolve(worker);
@@ -138,6 +138,7 @@
     if (!language || !code) return [];
     if (code.length > CODE_MAX_LENGTH) return [];
     const worker = await this.requestWorker();
+    if (!worker) return [];
     const message: SyntaxWorkerRequest = {
       type: SyntaxWorkerMessageType.REQUEST,
       language,
@@ -152,11 +153,11 @@
 
   finalize() {
     for (const worker of this.poolIdle) {
-      worker.terminate();
+      worker?.terminate();
     }
     this.poolIdle.clear();
     for (const worker of this.poolBusy) {
-      worker.terminate();
+      worker?.terminate();
     }
     this.poolBusy.clear();
     this.queueForResult.clear();
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index 09ba661..5506fb1 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -7,6 +7,7 @@
 import {Model} from '../../models/base/model';
 import {select} from '../../utils/observable-util';
 import {define} from '../../models/dependency';
+import {PluginLoader} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
 
 export enum GerritView {
   ADMIN = 'admin',
@@ -39,7 +40,12 @@
     state => state.view
   );
 
-  constructor() {
+  constructor(private readonly pluginLoader: PluginLoader) {
     super({});
+    this.subscriptions = [
+      this.routerView$.subscribe(view => {
+        this.pluginLoader.jsApiService.handleViewChange(view);
+      }),
+    ];
   }
 }
diff --git a/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts b/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
index ee02cfc..2609002 100644
--- a/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
+++ b/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
@@ -7,7 +7,7 @@
 import '../../test/common-test-setup';
 import {assertFails, waitEventLoop} from '../../test/test-utils';
 import {Scheduler} from './scheduler';
-import {RetryScheduler, RetryError} from './retry-scheduler';
+import {RetryError, RetryScheduler} from './retry-scheduler';
 import {FakeScheduler} from './fake-scheduler';
 import {SinonFakeTimers} from 'sinon';
 import {assert} from '@open-wc/testing';
diff --git a/polygerrit-ui/app/services/service-worker-installer.ts b/polygerrit-ui/app/services/service-worker-installer.ts
index 2bede59..a4d0732 100644
--- a/polygerrit-ui/app/services/service-worker-installer.ts
+++ b/polygerrit-ui/app/services/service-worker-installer.ts
@@ -46,12 +46,12 @@
 }
 
 export class ServiceWorkerInstaller extends Model<ServiceWorkerInstallerState> {
-  readonly initialized$: Observable<Boolean | undefined> = select(
+  readonly initialized$: Observable<boolean | undefined> = select(
     this.state$,
     state => state.initialized
   );
 
-  readonly shouldShowPrompt$: Observable<Boolean | undefined> = select(
+  readonly shouldShowPrompt$: Observable<boolean | undefined> = select(
     this.initialized$,
     _ => this.shouldShowPrompt()
   );
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
index 75fad6d..446d2d9 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -5,7 +5,7 @@
  */
 
 /** Enum for all special shortcuts */
-import {ComboKey, Key, Modifier, Binding} from '../../utils/dom-util';
+import {Binding, ComboKey, Key, Modifier} from '../../utils/dom-util';
 
 export enum SPECIAL_SHORTCUT {
   DOC_ONLY = 'DOC_ONLY',
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index a352f68..2407fba 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -4,7 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {Subscription} from 'rxjs';
-import {map, distinctUntilChanged} from 'rxjs/operators';
+import {distinctUntilChanged, map} from 'rxjs/operators';
 import {
   createShortcutConfig,
   Shortcut,
@@ -12,14 +12,14 @@
   ShortcutSection,
 } from './shortcuts-config';
 import {
+  Binding,
   ComboKey,
   eventMatchesShortcut,
   isElementTarget,
   Key,
   Modifier,
-  Binding,
-  shouldSuppress,
   ShortcutOptions,
+  shouldSuppress,
 } from '../../utils/dom-util';
 import {ReportingService} from '../gr-reporting/gr-reporting';
 import {UserModel} from '../../models/user/user-model';
diff --git a/polygerrit-ui/app/services/storage/gr-storage_impl.ts b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
index 9ddba01..9397c05 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_impl.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
@@ -113,7 +113,7 @@
       if (exc instanceof DOMException) {
         // Catch for QuotaExceededError and disable writes on local storage the
         // first time that it occurs.
-        if (exc.code === 22) {
+        if (exc.name === 'QuotaExceededError') {
           this.exceededQuota = true;
           console.warn('Local storage quota exceeded: disabling');
           return;
diff --git a/polygerrit-ui/app/services/storage/gr-storage_test.ts b/polygerrit-ui/app/services/storage/gr-storage_test.ts
index 6c22005..3022d8e 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_test.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_test.ts
@@ -31,11 +31,11 @@
 
   setup(() => {
     grStorage = new GrStorageService();
-    grStorage.storage = mockStorage(false);
+    grStorage.storage = mockStorage(/* quotaExceeded=*/ false);
   });
 
   test('exceeded quota disables storage', () => {
-    grStorage.storage = mockStorage(true);
+    grStorage.storage = mockStorage(/* quotaExceeded=*/ true);
     assert.isFalse(grStorage.exceededQuota);
 
     const key = grStorage.getEditableContentKey('test-key');
@@ -56,14 +56,14 @@
     // Setting the draft stores it under the expected key.
     const item = grStorage.storage.getItem(computedKey);
     assert.isOk(item);
-    assert.equal(JSON.parse(item!).message, 'my content');
-    assert.isOk(JSON.parse(item!).updated);
+    assert.equal(JSON.parse(item).message, 'my content');
+    assert.isOk(JSON.parse(item).updated);
 
     // getEditableContentItem performs as expected.
     const obj = grStorage.getEditableContentItem(key);
     assert.isOk(obj);
-    assert.equal(obj!.message, 'my content');
-    assert.isOk(obj!.updated);
+    assert.equal(obj.message, 'my content');
+    assert.isOk(obj.updated);
     assert.isTrue(cleanupStub.called);
 
     // eraseEditableContentItem performs as expected.
@@ -83,8 +83,8 @@
       'editablecontent:c50_psedit_index.php'
     );
     assert.isOk(item);
-    assert.equal(JSON.parse(item!).message, 'my content test 1');
-    assert.isOk(JSON.parse(item!).updated);
+    assert.equal(JSON.parse(item).message, 'my content test 1');
+    assert.isOk(JSON.parse(item).updated);
 
     // We have to add getItem, removeItem and setItem to the array.
     // Typically these functions don't get outputed in .storage,
diff --git a/polygerrit-ui/app/services/suggestions/suggestions-service_impl.ts b/polygerrit-ui/app/services/suggestions/suggestions-service_impl.ts
index 76b8c91..100a13e 100644
--- a/polygerrit-ui/app/services/suggestions/suggestions-service_impl.ts
+++ b/polygerrit-ui/app/services/suggestions/suggestions-service_impl.ts
@@ -59,6 +59,7 @@
       !!path &&
       path !== SpecialFilePath.PATCHSET_LEVEL_COMMENTS &&
       path !== SpecialFilePath.COMMIT_MESSAGE &&
+      path !== SpecialFilePath.MERGE_LIST &&
       this.change.is_private !== true &&
       (!this.suggestionsProvider.supportedFileExtensions ||
         this.suggestionsProvider.supportedFileExtensions.includes(
@@ -148,7 +149,7 @@
       range: comment.range,
       lineNumber: comment.line,
       generatedSuggestionId,
-      commentId: comment.id,
+      commentId: id(comment),
       reportSource,
     });
   }
@@ -187,7 +188,6 @@
       comment,
       comments
     );
-    if (!response?.completion) return;
     return context;
   }
 
@@ -211,6 +211,7 @@
       draftInSyncWithSuggestionLength: 0,
       modelVersion: response.modelVersion ?? '',
       outcome: response.outcome,
+      responseCode: response.responseCode,
       requestDurationMs,
     };
   }
diff --git a/polygerrit-ui/app/styles/form-styles.ts b/polygerrit-ui/app/styles/form-styles.ts
index 47f610e03..1b9d3a4 100644
--- a/polygerrit-ui/app/styles/form-styles.ts
+++ b/polygerrit-ui/app/styles/form-styles.ts
@@ -18,18 +18,18 @@
   }
   /* prettier formatter removes semi-colons after css mixins. */
   /* prettier-ignore */
-  iron-autogrow-textarea {
+  gr-autogrow-textarea {
     background-color: inherit;
     color: var(--primary-text-color);
     border: 1px solid var(--border-color);
     border-radius: var(--border-radius);
     padding: 0;
     box-sizing: border-box;
-    /* iron-autogrow-textarea has a "-webkit-appearance: textarea" :host
+    /* gr-autogrow-textarea has a "-webkit-appearance: textarea" :host
         css rule, which prevents overriding the border color. Clear that. */
     -webkit-appearance: none;
-    --iron-autogrow-textarea_-_box-sizing: border-box;
-    --iron-autogrow-textarea_-_padding: var(--spacing-s);
+    --gr-autogrow-textarea-box-sizing: border-box;
+    --gr-autogrow-textarea-padding: var(--spacing-s);
   }
   input,
   textarea,
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
index 33aa2d7..7778c08 100644
--- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
@@ -30,7 +30,7 @@
     max-width: 20em;
     padding-left: var(--metadata-horizontal-padding);
     padding-right: var(--metadata-horizontal-padding);
-    word-break: break-word;
+    overflow-wrap: anywhere;
   }
 `;
 
diff --git a/polygerrit-ui/app/styles/gr-form-styles.ts b/polygerrit-ui/app/styles/gr-form-styles.ts
index 283dadc..4792dcf 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.ts
+++ b/polygerrit-ui/app/styles/gr-form-styles.ts
@@ -30,6 +30,7 @@
     display: flex;
     margin: var(--spacing-s) 0;
     min-height: 2em;
+    align-items: center;
   }
   .gr-form-styles section * {
     vertical-align: middle;
@@ -82,7 +83,7 @@
   .gr-form-styles td:last-child gr-button {
     width: 100%;
   }
-  .gr-form-styles iron-autogrow-textarea {
+  .gr-form-styles gr-autogrow-textarea {
     height: auto;
     min-height: 4em;
   }
diff --git a/polygerrit-ui/app/styles/gr-icon-styles.ts b/polygerrit-ui/app/styles/gr-icon-styles.ts
deleted file mode 100644
index 9865825..0000000
--- a/polygerrit-ui/app/styles/gr-icon-styles.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {css} from 'lit';
-
-export const iconStyles = css`
-  iron-icon {
-    display: inline-block;
-    vertical-align: top;
-    width: 20px;
-    height: 20px;
-  }
-`;
diff --git a/polygerrit-ui/app/styles/gr-material-styles.ts b/polygerrit-ui/app/styles/gr-material-styles.ts
new file mode 100644
index 0000000..0621432
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-material-styles.ts
@@ -0,0 +1,133 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {css} from 'lit';
+
+export const materialStyles = css`
+  md-switch {
+    --md-sys-color-surface-container-highest: var(
+      --switch-color-surface-container-highest
+    );
+    --md-sys-color-on-surface: var(--switch-color-on-surface);
+    --md-sys-color-on-surface-variant: var(--switch-color-on-surface-variant);
+    --md-sys-color-outline: var(--switch-color-outline);
+    --md-sys-color-primary: var(--switch-color-primary);
+    --md-sys-color-on-primary: var(--switch-color-on-primary);
+    --md-sys-color-primary-container: var(--switch-color-primary-container);
+    --md-switch-disabled-track-color: var(--switch-disabled-track-color);
+    --md-focus-ring-duration: 0s;
+    --md-switch-handle-height: 12px;
+    --md-switch-handle-width: 12px;
+    --md-switch-selected-handle-height: 12px;
+    --md-switch-selected-handle-width: 12px;
+    --md-switch-pressed-handle-height: 12px;
+    --md-switch-pressed-handle-width: 12px;
+    --md-switch-track-height: 16px;
+    --md-switch-track-outline-width: 0;
+    --md-switch-track-width: 32px;
+    --md-switch-state-layer-size: 32px;
+    padding-right: 0.5em;
+  }
+
+  md-tabs {
+    font-size: var(--font-size-h3);
+    font-weight: var(--font-weight-h3);
+    line-height: var(--line-height-h3);
+    --md-sys-typescale-title-small-font: var(--header-font-family);
+    --md-sys-typescale-title-small-size: var(--font-size-h3);
+    --md-sys-typescale-title-small-line-height: var(--line-height-h3);
+    --md-sys-color-on-surface: var(--tabs-color-on-surface);
+    --md-secondary-tab-active-hover-state-layer-color: var(
+      --deemphasized-text-color
+    );
+    --md-sys-color-on-surface-variant: var(--deemphasized-text-color);
+    --md-sys-color-primary: var(--link-color);
+    --md-sys-color-surface: transparent;
+  }
+  md-tabs::part(divider) {
+    display: none;
+  }
+
+  md-outlined-text-field {
+    background-color: var(--view-background-color);
+    color: var(--primary-text-color);
+    --md-sys-color-primary: var(--primary-text-color);
+    --md-sys-color-on-surface: var(--primary-text-color);
+    --md-sys-color-on-surface-variant: var(--deemphasized-text-color);
+    --md-outlined-text-field-label-text-color: var(--deemphasized-text-color);
+    --md-outlined-text-field-focus-label-text-color: var(
+      --deemphasized-text-color
+    );
+    --md-outlined-text-field-hover-label-text-color: var(
+      --deemphasized-text-color
+    );
+    border-radius: var(--border-radius);
+    --md-outlined-text-field-container-shape: var(--border-radius);
+    --md-outlined-text-field-focus-outline-color: var(
+      --prominent-border-color,
+      var(--border-color)
+    );
+    --md-outlined-text-field-outline-color: var(
+      --prominent-border-color,
+      var(--border-color)
+    );
+    --md-outlined-text-field-hover-outline-color: var(
+      --prominent-border-color,
+      var(--border-color)
+    );
+    --md-sys-color-outline: var(--prominent-border-color, var(--border-color));
+    --md-outlined-field-top-space: var(--spacing-s);
+    --md-outlined-field-bottom-space: var(--spacing-s);
+    --md-outlined-text-field-outline-width: 1px;
+    --md-outlined-text-field-hover-outline-width: 1px;
+    --md-outlined-text-field-focus-outline-width: 0;
+    --md-outlined-field-leading-space: 8px;
+  }
+
+  md-outlined-text-field.showBlueFocusBorder {
+    --md-outlined-text-field-focus-outline-width: 2px;
+    --md-outlined-text-field-focus-outline-color: var(
+      --input-focus-border-color
+    );
+  }
+
+  /* These colours come from paper-checkbox */
+  md-checkbox {
+    background-color: var(--background-color-primary);
+    --md-sys-color-primary: var(--checkbox-primary);
+    --md-sys-color-on-primary: var(--checkbox-on-primary);
+    --md-sys-color-on-surface: var(--checkbox-on-surface);
+    --md-sys-color-on-surface-variant: var(--checkbox-on-surface-variant);
+    --md-checkbox-container-shape: 0px;
+  }
+
+  md-radio {
+    --md-sys-color-primary: var(--radio-primary);
+    --md-sys-color-on-primary: var(--radio-on-primary);
+    --md-sys-color-on-surface: var(--radio-on-surface);
+    --md-sys-color-on-surface-variant: var(--radio-on-surface-variant);
+  }
+
+  md-outlined-select {
+    min-width: unset;
+    --md-outlined-field-top-space: 4px;
+    --md-outlined-field-bottom-space: 4px;
+    --md-sys-color-surface-container: var(--select-surface-container);
+    --md-sys-color-on-secondary-container: var(--select-on-secondary-container);
+    --md-sys-color-secondary-container: var(--select-secondary-container);
+    --md-sys-color-primary: var(--select-primary);
+    --md-sys-color-on-surface: var(--select-on-surface);
+  }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-material-styles">
+  <template>
+    <style>
+    ${materialStyles.cssText}
+    </style>
+  </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/gr-paper-styles.ts b/polygerrit-ui/app/styles/gr-paper-styles.ts
deleted file mode 100644
index 301c02d..0000000
--- a/polygerrit-ui/app/styles/gr-paper-styles.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * @license
- * Copyright 2021 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {css} from 'lit';
-
-export const paperStyles = css`
-  paper-toggle-button {
-    --paper-toggle-button-checked-bar-color: var(--link-color);
-    --paper-toggle-button-checked-button-color: var(--link-color);
-  }
-  /* prettier formatter removes semi-colons after css mixins. */
-  /* prettier-ignore */
-  paper-tabs {
-    font-size: var(--font-size-h3);
-    font-weight: var(--font-weight-h3);
-    line-height: var(--line-height-h3);
-    --paper-font-common-base: {
-      font-family: var(--header-font-family);
-      -webkit-font-smoothing: initial;
-    };
-    --paper-tab-content: {
-      margin-bottom: var(--spacing-s);
-    };
-    --paper-tab-content-focused: {
-      /* paper-tabs uses 700 here, which can look awkward */
-      font-weight: var(--font-weight-h3);
-      background: var(--gray-background-focus);
-    };
-    --paper-tab-content-unselected: {
-      /* paper-tabs uses 0.8 here, but we want to control the color directly */
-      opacity: 1;
-      color: var(--deemphasized-text-color);
-    };
-  }
-  paper-tab:focus {
-    padding-left: 0px;
-    padding-right: 0px;
-  }
-`;
-
-const $_documentContainer = document.createElement('template');
-$_documentContainer.innerHTML = `<dom-module id="gr-paper-styles">
-  <template>
-    <style>
-    ${paperStyles.cssText}
-    </style>
-  </template>
-</dom-module>`;
-document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 1582f6c..10bfb37 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -131,13 +131,6 @@
     border-spacing: 0;
   }
 
-  iron-icon {
-    color: var(--deemphasized-text-color);
-    vertical-align: top;
-    --iron-icon-height: 20px;
-    --iron-icon-width: 20px;
-  }
-
   /* Stopgap solution until we remove hidden$ attributes. */
 
   :host([hidden]),
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 97993c1..60b5026 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -300,6 +300,7 @@
     --selection-background-color: rgba(161, 194, 250, 0.1);
     --tooltip-background-color: var(--gray-900);
     --section-header-background-color: var(--blue-50);
+    --gr-autogrow-textarea-disabled-color: lightgrey;
 
     /* dashboard size background colors */
     --dashboard-size-xs: var(--gray-200);
@@ -313,7 +314,6 @@
 
     /* comment background colors */
     --comment-background-color: var(--gray-200);
-    --robot-comment-background-color: var(--blue-50);
     --unresolved-comment-background-color: #fef7e0;
 
 
@@ -447,8 +447,6 @@
 
     --diff-moved-in-background: var(--cyan-50);
     --diff-moved-in-label-color: var(--cyan-900);
-    --diff-moved-in-changed-background: var(--cyan-50);
-    --diff-moved-in-changed-label-color: var(--cyan-900);
     --diff-moved-out-background: var(--purple-50);
     --diff-moved-out-label-color: var(--purple-900);
 
@@ -528,14 +526,40 @@
     --border-radius: 4px;
     --line-length-indicator-color: #681da8;
 
-    /* paper component overrides */
-    --paper-tooltip-delay-in: 200ms;
-    --paper-tooltip-delay-out: 0;
-    --paper-tooltip-duration-in: 0;
-    --paper-tooltip-duration-out: 0;
-    --paper-tooltip-background: var(--tooltip-background-color);
-    --paper-tooltip-opacity: 1;
-    --paper-tooltip-text-color: var(--tooltip-text-color);
+    /* md-filled-card (colours originate from paper-card) */
+    --card-surface-container-highest: #f8f9fa;
+
+    /* md-checkbox (colours from paper-checkbox but adapted using material/web theme selector) */
+    --checkbox-primary: #273a9f;
+    --checkbox-on-primary: #ffffff;
+    --checkbox-on-surface: #1a1b22;
+    --checkbox-on-surface-variant: #3f4948;
+
+    /* md-radio (similar to md-checkbox above) */
+    --radio-primary: #273a9f;
+    --radio-on-primary: #ffffff;
+    --radio-on-surface: #1a1b22;
+    --radio-on-surface-variant: #3f4948;
+
+    /* md-filled-select/md-outlined-select (colour originates from paper-listbox but generated by material-web using the hex */
+    --select-surface-container: #f8f9fa;
+    --select-surface-container-highest: #f8f9fa;
+    --select-on-surface: #1c1b1b;
+    --select-on-surface-variant: #444749;
+    --select-primary: #5c5f60;
+    --select-secondary-container: #e5e3e3;
+    --select-on-secondary-container: #484949;
+
+    /* md-switch (colour originates from paper-toggle-button but generated by material-web using the hex */
+    --switch-color-surface-container-highest: #e0e2ea;
+    --switch-color-on-surface: #181c22;
+    --switch-color-on-surface-variant: #404752;
+    --switch-color-outline: #707783;
+    --switch-color-primary: #005290;
+    --switch-color-on-primary: #ffffff;
+    --switch-color-primary-container: #d7e2ff;
+    --switch-color-on-primary-container: #ffffff;
+    --switch-disabled-track-color: #000;
   }
   @media screen and (max-width: 50em) {
     html {
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 635d246..371b4b0 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -133,10 +133,10 @@
     --selection-background-color: rgba(161, 194, 250, 0.1);
     --tooltip-background-color: var(--gray-200);
     --section-header-background-color: var(--blue-tonal);
+    --gr-autogrow-textarea-disabled-color: #6c6c6c;
 
     /* comment background colors */
     --comment-background-color: #3c3f43;
-    --robot-comment-background-color: #1e3a5f;
     --unresolved-comment-background-color: #614a19;
 
     /* Suggest edits */
@@ -231,8 +231,6 @@
 
     --diff-moved-in-background: #1d4042;
     --diff-moved-in-label-color: var(--cyan-50);
-    --diff-moved-in-changed-background: #1d4042;
-    --diff-moved-in-changed-label-color: var(--cyan-50);
     --diff-moved-out-background: #230e34;
     --diff-moved-out-label-color: var(--purple-50);
 
@@ -288,6 +286,47 @@
 
     /* rules applied to html */
     background-color: var(--view-background-color);
+
+    /* md-filled-card (colours originate from paper-card) */
+    --card-surface-container-highest: #2f3034;
+
+    /* md-checkbox (colours from paper-checkbox but adapted using material/web theme selector) */
+    --checkbox-primary: #bac3ff;
+    --checkbox-on-primary: #08218a;
+    --checkbox-on-surface: #e3e1ea;
+    --checkbox-on-surface-variant: #c5c5d4;
+
+  /* These colours come from paper-checkbox */
+    --radio-primary: #bac3ff;
+    --radio-on-primary: #08218a;
+    --radio-on-surface: #e3e1ea;
+    --radio-on-surface-variant: #c5c5d4;
+
+    /* md-filled-select/md-outlined-select (colour originates from paper-listbox but generated by material-web using the hex */
+    --select-surface-container: #201f20;
+    --select-surface-container-highest: #2f3034;
+    --select-on-surface: #e5e2e1;
+    --select-on-surface-variant: #c7c6cb;
+    --select-primary: #c7c6cb;
+    --select-secondary-container: #3d3d3f;
+    --select-on-secondary-container: #d2d0d2;
+    --switch-disabled-track-color: #000;
+
+    /* md-menu/md-menu-itme/md-focus-ring */
+    --gr-dropdown-focus-ring-color: #c8c6c7;
+
+    /* md-switch (colour originates from paper-toggle-button but generated by material-web using the hex */
+    --switch-color-surface-container-highest: #000000;
+    --switch-color-on-surface: #e0e2ea;
+    --switch-color-on-surface-variant: #c0c7d4;
+    --switch-color-outline: #8a919e;
+    --switch-color-primary: #a2c9ff;
+    --switch-color-on-primary: #00315b;
+    --switch-color-primary-container: #0077ce;
+    --switch-color-on-primary-container: #ffffff;
+
+    /* md-tabs */
+    --tabs-color-on-surface: #e1e2e6;
   }
 `;
 
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 82c1bb9..81ad5bf 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -30,8 +30,8 @@
 import {_testOnly_allTasks} from '../utils/async-util';
 import {cleanUpStorage} from '../services/storage/gr-storage_mock';
 import {
-  DependencyRequestEvent,
   DependencyError,
+  DependencyRequestEvent,
   DependencyToken,
   Provider,
 } from '../models/dependency';
@@ -45,7 +45,6 @@
   interface Window {
     sinon: typeof sinon;
   }
-  let sinon: typeof sinon;
 }
 
 window.sinon = sinon;
@@ -60,6 +59,7 @@
 });
 
 let testSetupTimestampMs = 0;
+let currentTestName = '';
 
 const injectedDependencies: Map<
   DependencyToken<unknown>,
@@ -94,8 +94,9 @@
   evt.callback(() => testResolver(evt.dependency));
 }
 
-setup(() => {
+setup(function () {
   testSetupTimestampMs = new Date().getTime();
+  currentTestName = this.currentTest?.title || 'unknown test';
 
   // If the following asserts fails - then window.stub is
   // overwritten by some other code.
@@ -122,6 +123,7 @@
   // `awaitPluginsLoaded` will rely on that to kick off,
   // in testing, we want to kick start this earlier.
   testResolver(pluginLoaderToken).loadPlugins([]);
+  window.emojis = [];
   testOnlyResetGrRestApiSharedObjects(getAppContext().authService);
 });
 
@@ -132,7 +134,7 @@
 // Very simple function to catch unexpected elements in documents body.
 // It can't catch everything, but in most cases it is enough.
 function checkChildAllowed(element: Element) {
-  const allowedTags = ['SCRIPT', 'IRON-A11Y-ANNOUNCER', 'LINK'];
+  const allowedTags = ['SCRIPT', 'GR-A11Y-ANNOUNCER', 'LINK'];
   if (allowedTags.includes(element.tagName)) {
     return;
   }
@@ -180,6 +182,9 @@
   const testTeardownTimestampMs = new Date().getTime();
   const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
   if (elapsedMs > 1000) {
-    console.warn(`ATTENTION! Test took longer than 1 second: ${elapsedMs} ms`);
+    console.warn(
+      `ATTENTION! Test "${currentTestName}" took longer than 1 second: ${elapsedMs} ms`
+    );
   }
+  currentTestName = '';
 });
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 30bac0d..818dbef 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -1,65 +1,69 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
 /**
  * @license
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {
+  RestApiService,
+  SubmittabilityInfo,
+} from '../../services/gr-rest-api/gr-rest-api';
+import {FlowInfo} from '../../api/rest-api';
+import {
+  AccountCapabilityInfo,
   AccountDetailInfo,
   AccountExternalIdInfo,
   AccountInfo,
   AccountStateInfo,
-  ValidationOptionsInfo,
-  ServerInfo,
-  ProjectInfo,
-  AccountCapabilityInfo,
-  SuggestedReviewerInfo,
-  GroupNameToGroupInfoMap,
-  EditPreferencesInfo,
-  SshKeyInfo,
-  RepoName,
-  GpgKeyInfo,
-  PreferencesInfo,
-  EmailInfo,
-  ProjectAccessInfo,
+  ActionNameToActionInfoMap,
+  AuthTokenInfo,
+  Base64FileContent,
+  BlameInfo,
+  BranchInfo,
   CapabilityInfoMap,
   ChangeInfo,
-  ProjectInfoWithName,
-  GroupInfo,
-  BranchInfo,
-  ConfigInfo,
-  EditInfo,
-  DashboardInfo,
-  RepoAccessInfoMap,
-  IncludedInInfo,
   CommentInfo,
-  PluginInfo,
-  DocResult,
-  ContributorAgreementInfo,
-  Password,
-  ProjectWatchInfo,
-  NameToProjectInfoMap,
-  GroupAuditEventInfo,
-  Base64FileContent,
-  TagInfo,
-  RelatedChangesInfo,
-  SubmittedTogetherInfo,
-  FilePathToDiffInfoMap,
-  BlameInfo,
-  ImagesForDiff,
-  ActionNameToActionInfoMap,
-  Hashtag,
-  FileNameToFileInfoMap,
-  TopMenuEntryInfo,
-  MergeableInfo,
   CommitInfo,
-  GroupId,
-  GroupName,
-  UrlEncodedRepoName,
-  NumericChangeId,
-  PreferencesInput,
+  ConfigInfo,
+  ContributorAgreementInfo,
+  DashboardInfo,
+  DocResult,
   DraftInfo,
+  EditInfo,
+  EditPreferencesInfo,
+  EmailInfo,
+  FileNameToFileInfoMap,
+  FilePathToDiffInfoMap,
+  GpgKeyInfo,
+  GroupAuditEventInfo,
+  GroupId,
+  GroupInfo,
+  GroupName,
+  GroupNameToGroupInfoMap,
+  Hashtag,
+  ImagesForDiff,
+  IncludedInInfo,
+  MergeableInfo,
+  NameToProjectInfoMap,
+  NumericChangeId,
+  PluginInfo,
+  PreferencesInfo,
+  PreferencesInput,
+  ProjectAccessInfo,
+  ProjectInfo,
+  ProjectInfoWithName,
+  ProjectWatchInfo,
+  RelatedChangesInfo,
+  RepoAccessInfoMap,
+  RepoName,
+  ServerInfo,
+  SshKeyInfo,
+  SubmitRequirementInfo,
+  SubmittedTogetherInfo,
+  SuggestedReviewerInfo,
+  TagInfo,
+  TopMenuEntryInfo,
+  UrlEncodedRepoName,
+  ValidationOptionsInfo,
 } from '../../types/common';
 import {DiffInfo, DiffPreferencesInfo} from '../../types/diff';
 import {
@@ -78,6 +82,7 @@
 } from '../../constants/constants';
 import {ParsedChangeInfo} from '../../types/types';
 import {ErrorCallback} from '../../api/rest';
+import {IsFlowsEnabledInfo, LabelDefinitionInfo} from '../../api/rest-api';
 
 export const grRestApiMock: RestApiService = {
   addAccountEmail(): Promise<Response> {
@@ -95,9 +100,6 @@
   applyFixSuggestion(): Promise<Response> {
     return Promise.resolve(new Response());
   },
-  applyRobotFixSuggestion(): Promise<Response> {
-    return Promise.resolve(new Response());
-  },
   awaitPendingDiffDrafts(): Promise<void> {
     return Promise.resolve();
   },
@@ -169,8 +171,16 @@
     return Promise.resolve(new Response());
   },
   finalize(): void {},
-  generateAccountHttpPassword(): Promise<Password | undefined> {
-    return Promise.resolve('asdf');
+  getAccountAuthTokens(): Promise<AuthTokenInfo[] | undefined> {
+    return Promise.resolve([{id: 'tokenId', token: 'asdf'}]);
+  },
+  deleteAccountAuthToken(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  generateAccountAuthToken(
+    tokenId: string
+  ): Promise<AuthTokenInfo | undefined> {
+    return Promise.resolve({id: tokenId, token: 'asdf'});
   },
   getAccount(): Promise<AccountDetailInfo | undefined> {
     return Promise.resolve(createAccountDetailWithId(1));
@@ -205,12 +215,18 @@
   getAccountStatus(): Promise<string | undefined> {
     return Promise.resolve('');
   },
+  getAllRevisionFiles() {
+    return Promise.resolve(undefined);
+  },
   getAvatarChangeUrl(): Promise<string | undefined> {
     return Promise.resolve('');
   },
   getBlame(): Promise<BlameInfo[] | undefined> {
     return Promise.resolve([]);
   },
+  getPatchContent(): Promise<string | undefined> {
+    return Promise.resolve(undefined);
+  },
   getCapabilities(): Promise<CapabilityInfoMap | undefined> {
     return Promise.resolve({});
   },
@@ -235,6 +251,9 @@
     if (changeNum === undefined) return Promise.resolve(undefined);
     return Promise.resolve(createChange() as ParsedChangeInfo);
   },
+  getSubmittabilityInfo(): Promise<SubmittabilityInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
   getChangeEdit(): Promise<EditInfo | undefined> {
     return Promise.resolve(undefined);
   },
@@ -314,11 +333,6 @@
   getDiffPreferences(): Promise<DiffPreferencesInfo | undefined> {
     return Promise.resolve(createDefaultDiffPrefs());
   },
-  getDiffRobotComments() {
-    // NOTE: This method can not be typed properly due to overloads.
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    return Promise.resolve({}) as any;
-  },
   getDocumentationSearches(): Promise<DocResult[] | undefined> {
     return Promise.resolve([]);
   },
@@ -331,6 +345,9 @@
   getFileContent(): Promise<Response | Base64FileContent | undefined> {
     return Promise.resolve(new Response());
   },
+  getIfFlowsIsEnabled(): Promise<IsFlowsEnabledInfo | undefined> {
+    return Promise.resolve({enabled: true});
+  },
   getRepoName(): Promise<RepoName> {
     throw new Error('getRepoName() not implemented by RestApiMock.');
   },
@@ -393,6 +410,39 @@
   getRepoAccess(): Promise<RepoAccessInfoMap | undefined> {
     return Promise.resolve({});
   },
+  getRepoSubmitRequirements(): Promise<SubmitRequirementInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  createSubmitRequirement(): Promise<SubmitRequirementInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  updateSubmitRequirement(): Promise<SubmitRequirementInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  deleteSubmitRequirement(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveRepoSubmitRequirementsForReview(): Promise<ChangeInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  getRepoLabels(): Promise<LabelDefinitionInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getRepoLabel(): Promise<LabelDefinitionInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  createRepoLabel(): Promise<LabelDefinitionInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  updateRepoLabel(): Promise<LabelDefinitionInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  saveRepoLabelsForReview(): Promise<ChangeInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  deleteRepoLabel(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
   getRepoAccessRights(): Promise<ProjectAccessInfo | undefined> {
     return Promise.resolve(undefined);
   },
@@ -414,9 +464,6 @@
   getFixPreview(): Promise<FilePathToDiffInfoMap | undefined> {
     return Promise.resolve({});
   },
-  getRobotCommentFixPreview(): Promise<FilePathToDiffInfoMap | undefined> {
-    return Promise.resolve({});
-  },
   queryAccounts(): Promise<AccountInfo[] | undefined> {
     return Promise.resolve([]);
   },
@@ -512,6 +559,9 @@
   saveGroupOptions(): Promise<Response> {
     return Promise.resolve(new Response());
   },
+  deleteGroup(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
   saveGroupOwner(): Promise<Response> {
     return Promise.resolve(new Response());
   },
@@ -577,4 +627,16 @@
   updateIdentityInChangeEdit(): Promise<Response | undefined> {
     return Promise.resolve(new Response());
   },
+  getFlow(): Promise<FlowInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  listFlows(): Promise<FlowInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  createFlow(): Promise<FlowInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  deleteFlow(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
 };
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index df064d3..ad28150 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -20,8 +20,8 @@
 import {storageServiceToken} from '../services/storage/gr-storage_impl';
 import {highlightServiceToken} from '../services/highlight/highlight-service';
 import {
-  diffModelToken,
   DiffModel,
+  diffModelToken,
 } from '../embed/diff/gr-diff-model/gr-diff-model';
 import {Finalizable} from '../types/types';
 import {suggestionsServiceToken} from '../services/suggestions/suggestions-service';
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 5f40cad..a670967 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -17,6 +17,7 @@
   ChangeId,
   ChangeInfo,
   ChangeInfoId,
+  ChangeMessage,
   ChangeMessageId,
   ChangeMessageInfo,
   ChangeViewChangeInfo,
@@ -24,12 +25,14 @@
   CommentLinkInfo,
   CommentLinks,
   CommentRange,
+  CommentThread,
   CommitId,
   CommitInfo,
   ConfigInfo,
   DownloadInfo,
-  EditInfo,
+  DraftInfo,
   EDIT,
+  EditInfo,
   EmailAddress,
   FixId,
   FixSuggestionInfo,
@@ -40,6 +43,7 @@
   GroupAuditEventType,
   GroupId,
   GroupInfo,
+  GroupsConfigInfo,
   InheritedBooleanInfo,
   LabelInfo,
   MaxObjectSizeLimitInfo,
@@ -57,9 +61,7 @@
   Reviewers,
   RevisionInfo,
   RevisionPatchSetNum,
-  RobotCommentInfo,
-  RobotId,
-  RobotRunId,
+  SavingState,
   SchemesInfoMap,
   ServerInfo,
   SubmittedTogetherInfo,
@@ -68,10 +70,6 @@
   Timestamp,
   UrlEncodedCommentId,
   UserConfigInfo,
-  CommentThread,
-  DraftInfo,
-  ChangeMessage,
-  SavingState,
 } from '../types/common';
 import {
   AccountsVisibility,
@@ -435,6 +433,19 @@
   };
 }
 
+export function createChangeWithStatus(
+  status: ChangeStatus,
+  mergeable: boolean | undefined = undefined
+): ChangeInfo {
+  return {
+    ...createChange(),
+    revisions: createRevisions(1),
+    current_revision: 'rev1' as CommitId,
+    status,
+    mergeable,
+  };
+}
+
 export function createChangeViewChange(): ChangeViewChangeInfo {
   return {
     ...createChange(),
@@ -495,6 +506,10 @@
   };
 }
 
+export function createGroupsInfo(): GroupsConfigInfo {
+  return {};
+}
+
 export function createPluginConfig(): PluginConfigInfo {
   return {
     has_avatars: false,
@@ -521,6 +536,7 @@
     change: createChangeConfig(),
     download: createDownloadInfo(),
     gerrit: createGerritInfo(),
+    groups: createGroupsInfo(),
     plugin: createPluginConfig(),
     suggest: createSuggestInfo(),
     user: createUserConfig(),
@@ -893,6 +909,7 @@
 export function createDraft(extra: Partial<CommentInfo> = {}): DraftInfo {
   return {
     ...createComment(),
+    id: 'draft-12345' as UrlEncodedCommentId,
     savingState: SavingState.OK,
     ...extra,
   };
@@ -906,36 +923,6 @@
   };
 }
 
-export function createRobotComment(
-  extra: Partial<CommentInfo> = {}
-): RobotCommentInfo {
-  return {
-    ...createComment(),
-    robot_id: 'robot-id-123' as RobotId,
-    robot_run_id: 'robot-run-id-456' as RobotRunId,
-    properties: {},
-    fix_suggestions: [
-      {
-        fix_id: 'robot-run-id-456-fix' as FixId,
-        description: 'Robot suggestion',
-        replacements: [
-          {
-            path: 'abc.txt'!,
-            range: {
-              start_line: 0,
-              start_character: 0,
-              end_line: 1,
-              end_character: 10,
-            },
-            replacement: 'replacement',
-          },
-        ],
-      },
-    ],
-    ...extra,
-  };
-}
-
 export function createChangeComments(): ChangeComments {
   const comments = {
     '/COMMIT_MSG': [
@@ -1036,7 +1023,7 @@
       },
     ],
   };
-  return new ChangeComments(comments, {}, drafts, {}, {});
+  return new ChangeComments(comments, drafts, {}, {});
 }
 
 export function createThread(
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 571edf0..a62c5d7 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -8,7 +8,7 @@
 import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
 import {SinonSpy, SinonStub} from 'sinon';
 import {ReportingService} from '../services/gr-reporting/gr-reporting';
-import {queryAndAssert, query} from '../utils/common-util';
+import {query, queryAndAssert} from '../utils/common-util';
 import {FlagsService} from '../services/flags/flags';
 import {Key, Modifier, whenVisible} from '../utils/dom-util';
 import {Observable} from 'rxjs';
@@ -18,6 +18,14 @@
 import {PageContext} from '../elements/core/gr-router/gr-page';
 import {waitUntil} from '../utils/async-util';
 import {JSON_PREFIX} from '../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {
+  applyTheme as applyDarkTheme,
+  removeTheme as removeDarkTheme,
+} from '../styles/themes/dark-theme';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {LitElement} from 'lit';
 export {query, queryAll, queryAndAssert} from '../utils/common-util';
 export {mockPromise, waitUntil} from '../utils/async-util';
 export type {MockPromise} from '../utils/async-util';
@@ -316,3 +324,14 @@
 export function makePrefixedJSON(obj: any) {
   return JSON_PREFIX + JSON.stringify(obj);
 }
+
+export async function visualDiffDarkTheme(
+  element: LitElement | HTMLElement,
+  name: string
+) {
+  applyDarkTheme();
+  document.documentElement.classList.add('darkTheme');
+  await visualDiff(element, `${name}-dark`);
+  removeDarkTheme();
+  document.documentElement.classList.remove('darkTheme');
+}
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 98aaf0f..eab8759 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -1,8 +1,8 @@
 {
   "compilerOptions": {
     /* Basic Options */
-    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021' or 'ESNEXT'. */
+    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
     "allowJs": true, /* Allow javascript files to be compiled. */
     "checkJs": false, /* Report errors in .js files. */
     "declaration": false, /* Temporary disabled - generates corresponding '.d.ts' file. */
@@ -62,9 +62,9 @@
         "name": "ts-lit-plugin",
         "strict": true,
         "rules": {
-          "no-unknown-tag-name": "error",
+          "no-unknown-tag-name": "off",
           "no-unclosed-tag": "error",
-          "no-unknown-property": "error",
+          "no-unknown-property": "off",
           "no-unintended-mixed-binding": "error",
           "no-invalid-boolean-binding": "error",
           "no-expressionless-property-binding": "error",
@@ -72,12 +72,14 @@
           "no-boolean-in-attribute-binding": "error",
           "no-complex-attribute-binding": "error",
           "no-nullable-attribute-binding": "error",
-          "no-incompatible-type-binding": "error",
+          "no-incompatible-type-binding": "off",
           "no-invalid-directive-binding": "error",
-          "no-incompatible-property-type": "error",
+          "no-incompatible-property-type": "off",
           "no-unknown-property-converter": "error",
           "no-invalid-attribute-name": "error",
-          "no-invalid-tag-name": "error"
+          "no-invalid-tag-name": "error",
+          "no-property-visibility-mismatch": "off",
+          "no-unknown-attribute": "off",
         }
       }
     ]
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index c88c07b..22c1907 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -4,26 +4,26 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {
-  ChangeStatus,
-  RepoState,
-  SubmitType,
-  InheritedBooleanInfoConfiguredValue,
-  PermissionAction,
   AppTheme,
+  ChangeStatus,
   DateFormat,
-  TimeFormat,
-  EmailStrategy,
   DefaultBase,
-  UserPriority,
   DiffViewMode,
   DraftsAction,
-  NotifyType,
   EmailFormat,
+  EmailStrategy,
+  InheritedBooleanInfoConfiguredValue,
   MergeStrategy,
+  NotifyType,
+  PermissionAction,
+  RepoState,
+  SubmitType,
+  TimeFormat,
+  UserPriority,
 } from '../constants/constants';
 import {
-  AccountId,
   AccountDetailInfo,
+  AccountId,
   AccountInfo,
   AccountsConfigInfo,
   ActionInfo,
@@ -31,6 +31,7 @@
   ApprovalInfo,
   AuthInfo,
   AvatarInfo,
+  Base64FileContent,
   BasePatchSetNum,
   BranchName,
   BrandType,
@@ -44,6 +45,7 @@
   CommentInfo,
   CommentLinkInfo,
   CommentLinks,
+  CommentRange,
   CommentSide,
   CommitId,
   CommitInfo,
@@ -63,6 +65,9 @@
   EmailInfo,
   FetchInfo,
   FileInfo,
+  FixId,
+  FixReplacementInfo,
+  FixSuggestionInfo,
   GerritInfo,
   GitPersonInfo,
   GitRef,
@@ -72,8 +77,11 @@
   GroupInfo,
   GroupName,
   GroupOptionsInfo,
+  GroupsConfigInfo,
   Hashtag,
   InheritedBooleanInfo,
+  isDetailedLabelInfo,
+  isQuickLabelInfo,
   LabelInfo,
   LabelNameToInfoMap,
   LabelNameToLabelTypeInfoMap,
@@ -84,8 +92,8 @@
   MaxObjectSizeLimitInfo,
   MetadataInfo,
   NumericChangeId,
-  ParentCommitInfo,
   PARENT,
+  ParentCommitInfo,
   PatchSetNum,
   PatchSetNumber,
   PluginConfigInfo,
@@ -98,15 +106,14 @@
   RepoName,
   Requirement,
   RequirementType,
-  ReviewInputTag,
+  Reviewers,
   ReviewerState,
   ReviewerUpdateInfo,
-  Reviewers,
+  ReviewInputTag,
   RevisionInfo,
   RevisionPatchSetNum,
   SchemesInfoMap,
   ServerInfo,
-  ValidationOptionsInfo,
   SubmitTypeInfo,
   SuggestInfo,
   Timestamp,
@@ -114,18 +121,12 @@
   UrlEncodedCommentId,
   UrlEncodedRepoName,
   UserConfigInfo,
+  ValidationOptionsInfo,
   VotingRangeInfo,
   WebLinkInfo,
-  isDetailedLabelInfo,
-  isQuickLabelInfo,
-  Base64FileContent,
-  CommentRange,
-  FixReplacementInfo,
-  FixSuggestionInfo,
-  FixId,
 } from '../api/rest-api';
 import {DiffInfo, IgnoreWhitespaceType} from './diff';
-import {PatchRange, LineNumber} from '../api/diff';
+import {LineNumber, PatchRange} from '../api/diff';
 
 export type {
   AccountId,
@@ -169,8 +170,10 @@
   EmailInfo,
   FileInfo,
   FixId,
+  FixReplacementInfo,
   FixSuggestionInfo,
   GerritInfo,
+  GroupsConfigInfo,
   GitPersonInfo,
   GitRef,
   GpgKeyId,
@@ -238,10 +241,6 @@
  */
 export type ParsedJSON = BrandType<unknown, '_parsedJSON'>;
 
-export type RobotId = BrandType<string, '_robotId'>;
-
-export type RobotRunId = BrandType<string, '_robotRunId'>;
-
 // RevisionId '0' is the same as 'current'. However, we want to avoid '0'
 // in our code, so it is not added here as a possible value.
 export type RevisionId = 'current' | CommitId | PatchSetNum;
@@ -254,9 +253,6 @@
 
 export type LabelName = BrandType<string, '_labelName'>;
 
-// The Encoded UUID of the group
-export type EncodedGroupId = BrandType<string, '_encodedGroupId'>;
-
 export type UserId = AccountId | GroupId | EmailAddress;
 
 export type DiffPageSidebar = 'NONE' | `plugin-${string}`;
@@ -458,7 +454,7 @@
   name?: GroupName;
   uuid?: string;
   description?: string;
-  visible_to_all?: string;
+  visible_to_all?: boolean;
   owner_id?: string;
   members?: string[];
 }
@@ -467,7 +463,7 @@
   url?: string;
   name?: string;
   external?: boolean;
-  target?: string | null;
+  target?: '_blank' | '_parent' | '_self' | '_top' | null;
   download?: boolean;
   id?: string;
   tooltip?: string;
@@ -522,7 +518,7 @@
 export interface TopMenuItemInfo {
   url: string;
   name: string;
-  target: string;
+  target?: '_blank' | '_parent' | '_self' | '_top' | null;
   id?: string;
 }
 
@@ -592,22 +588,16 @@
 }
 
 /**
- * This is what human, robot and draft comments can agree upon.
+ * This is what published and draft comments can agree upon.
  *
  * Note that `id` and `updated` must be considered optional, because we might
  * be dealing with unsaved draft comments.
  */
-export type Comment = DraftInfo | CommentInfo | RobotCommentInfo;
+export type Comment = DraftInfo | CommentInfo;
 
 // TODO: Replace the CommentMap type with just an array of paths.
 export type CommentMap = {[path: string]: boolean};
 
-export function isRobot<T extends Comment>(
-  x: T | RobotCommentInfo | undefined
-): x is RobotCommentInfo {
-  return !!x && !!(x as RobotCommentInfo).robot_id;
-}
-
 export function isDraft<T extends Comment>(
   x: T | DraftInfo | undefined
 ): x is DraftInfo {
@@ -715,15 +705,16 @@
 }
 
 /**
- * Images are retrieved by using the file content API and the body is just the
- * HTML response.
- * TODO(TS): where is the source of this type ? I don't find it in doc
+ * The SubmitRequirementInfo entity describes a submit requirement.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-requirement-info
  */
-export interface ImageInfo {
-  body: string;
-  type: string;
-  _name?: string;
-  _expectedType?: string;
+export interface SubmitRequirementInfo {
+  name: string;
+  description?: string;
+  applicability_expression?: string;
+  submittability_expression: string;
+  override_expression?: string;
+  allow_override_in_child_projects: boolean;
 }
 
 /**
@@ -1026,6 +1017,7 @@
   createAccount?: boolean;
   createGroup?: boolean;
   createProject?: boolean;
+  deleteGroup?: boolean;
   emailReviewers?: boolean;
   flushCaches?: boolean;
   killTask?: boolean;
@@ -1100,8 +1092,8 @@
  * There is no RestAPI interface for it
  */
 export interface Base64ImageFile extends Base64File {
-  _expectedType: string;
-  _name: string;
+  _expectedType?: string;
+  _name?: string;
 }
 
 /**
@@ -1113,7 +1105,6 @@
   tag?: ReviewInputTag;
   labels?: LabelNameToValueMap;
   comments?: PathToCommentsInputMap;
-  robot_comments?: PathToRobotCommentsMap;
   drafts?: DraftsAction;
   notify?: NotifyType;
   notify_details?: RecipientTypeToNotifyInfoMap;
@@ -1156,30 +1147,11 @@
 
 export type LabelNameToValueMap = {[labelName: string]: number};
 export type PathToCommentsInputMap = {[path: string]: CommentInput[]};
-export type PathToRobotCommentsMap = {[path: string]: RobotCommentInput[]};
 export type RecipientTypeToNotifyInfoMap = {
   [recepientType: string]: NotifyInfo;
 };
 
 /**
- * The RobotCommentInput entity contains information for creating an inline robot comment
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#robot-comment-input
- */
-export type RobotCommentInput = RobotCommentInfo;
-
-/**
- * The RobotCommentInfo entity contains information about a robot inline comment
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#robot-comment-info
- */
-export interface RobotCommentInfo extends CommentInfo {
-  robot_id: RobotId;
-  robot_run_id: RobotRunId;
-  url?: string;
-  properties: {[propertyName: string]: string};
-}
-export type PathToRobotCommentsInfoMap = {[path: string]: RobotCommentInfo[]};
-
-/**
  * The ApplyProvidedFixInput entity contains information for applying fixes, provided in the
  * request body, to a revision.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#apply-provided-fix
@@ -1289,7 +1261,17 @@
 
 export type RequestPayload = string | object;
 
-export type Password = string;
+export interface AuthTokenInput {
+  id?: string;
+  token?: string;
+  lifetime?: string;
+}
+
+export interface AuthTokenInfo {
+  id: string;
+  token?: string;
+  expiration?: Timestamp;
+}
 
 /**
  * The BranchInput entity contains information for the creation of a new branch
@@ -1390,6 +1372,7 @@
   // we can bring this feature back. There is a frontend code and CSS styles for
   // it and this property is added here to keep related frontend code unchanged.
   submittable?: boolean;
+  work_in_progress?: boolean;
 }
 
 /**
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 0d1592e..47a96e4 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -16,9 +16,9 @@
   DiffFileMetaInfo as DiffFileMetaInfoApi,
   DiffInfo as DiffInfoApi,
   DiffIntralineInfo,
+  DiffPreferencesInfo as DiffPreferenceInfoApi,
   DiffRangesToFocus,
   DiffResponsiveMode,
-  DiffPreferencesInfo as DiffPreferenceInfoApi,
   IgnoreWhitespaceType,
   MarkLength,
   MoveDetails,
diff --git a/polygerrit-ui/app/utils/account-util.ts b/polygerrit-ui/app/utils/account-util.ts
index 51df47f..47e12b3 100644
--- a/polygerrit-ui/app/utils/account-util.ts
+++ b/polygerrit-ui/app/utils/account-util.ts
@@ -13,14 +13,14 @@
   isAccount,
   isDetailedLabelInfo,
   isGroup,
-  ReviewerInput,
-  ServerInfo,
-  UserId,
-  Suggestion,
   isReviewerAccountSuggestion,
   isReviewerGroupSuggestion,
+  ReviewerInput,
+  ServerInfo,
   SuggestedReviewerAccountInfo,
   SuggestedReviewerGroupInfo,
+  Suggestion,
+  UserId,
 } from '../types/common';
 import {AccountTag, ReviewerState} from '../constants/constants';
 import {assertNever, hasOwnProperty} from './common-util';
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 32a6867..599ecc1 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -229,19 +229,15 @@
   if (existingPromise) existingPromise.delegate(promise);
   return promise;
 }
-const THROTTLE_INTERVAL_MS = 500;
 
 /**
  * Ensure only one call is made within THROTTLE_INTERVAL_MS and any call within
  * this interval is ignored
  */
-export function throttleWrap<T>(fn: (e: T) => void) {
+export function throttleWrap<T>(fn: (e?: T) => void, throttleInterval = 500) {
   let lastCall: number | undefined;
-  return (e: T) => {
-    if (
-      lastCall !== undefined &&
-      Date.now() - lastCall < THROTTLE_INTERVAL_MS
-    ) {
+  return (e?: T) => {
+    if (lastCall !== undefined && Date.now() - lastCall < throttleInterval) {
       return;
     }
     lastCall = Date.now();
diff --git a/polygerrit-ui/app/utils/autocomplete-cache.ts b/polygerrit-ui/app/utils/autocomplete-cache.ts
index b880ccd..11d9acf 100644
--- a/polygerrit-ui/app/utils/autocomplete-cache.ts
+++ b/polygerrit-ui/app/utils/autocomplete-cache.ts
@@ -3,6 +3,9 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+
+import {ResponseCode} from '../api/suggestions';
+
 export interface AutocompletionContext {
   draftContent: string;
   draftContentLength?: number;
@@ -27,6 +30,7 @@
   savedDraftLength?: number;
 
   hasDraftChanged?: boolean;
+  responseCode?: ResponseCode;
 }
 
 /**
diff --git a/polygerrit-ui/app/utils/autocomplete-util.ts b/polygerrit-ui/app/utils/autocomplete-util.ts
new file mode 100644
index 0000000..8b8fc80
--- /dev/null
+++ b/polygerrit-ui/app/utils/autocomplete-util.ts
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {AccountInfo, ServerInfo} from '../api/rest-api';
+import {getUserName} from './display-name-util';
+
+export interface AutocompleteSuggestion<T = string> {
+  name?: string;
+  label?: string;
+  value?: T;
+  text?: string;
+}
+
+const SELF_EXPRESSION = 'self';
+const ME_EXPRESSION = 'me';
+
+/**
+ * Maps account information to a list of suggestions for autocompletion.
+ *
+ * @param accounts An array of account information.
+ * @param predicate The search predicate (e.g., "owner", "reviewer"). This is used to
+ *   prefix the suggested account/group in the autocomplete dropdown,
+ *   forming a query like "predicate:value".
+ * @param serverConfig The server configuration, used to determine the display name.
+ * @return An array of autocomplete suggestions.
+ */
+function convertToSuggestion(
+  accounts: AccountInfo[],
+  predicate: string,
+  serverConfig?: ServerInfo
+): AutocompleteSuggestion[] {
+  return accounts.map(account => {
+    const userName = getUserName(serverConfig, account);
+    return {
+      label: account.name || '',
+      text: account.email
+        ? `${predicate}:${account.email}`
+        : `${predicate}:"${userName}"`,
+    };
+  });
+}
+
+/**
+ * Fetches account suggestions based on the provided predicate and expression.
+ * It also includes "self" and "me" as suggestions if they contain expression as prefix.
+ *
+ * @param accountFetcher A function that fetches a list of accounts that match expression.
+ * @param predicate The search predicate (e.g., "owner", "reviewer"). This is used to
+ *   prefix the suggested account/group in the autocomplete dropdown,
+ *   forming a query like "predicate:value".
+ * @param accountPrefix The prefix of the account identifier.
+ * @param serverConfig The server configuration.
+ * @return A promise that resolves to an array of autocomplete suggestions.
+ */
+export function fetchAccountSuggestions(
+  accountFetcher: (accountPrefix: string) => Promise<AccountInfo[] | undefined>,
+  predicate: string,
+  accountPrefix: string,
+  serverConfig?: ServerInfo
+): Promise<AutocompleteSuggestion[]> {
+  if (accountPrefix.length === 0) {
+    return Promise.resolve([]);
+  }
+  return accountFetcher(accountPrefix)
+    .then(accounts => {
+      if (!accounts) {
+        return [];
+      }
+      return convertToSuggestion(accounts, predicate, serverConfig);
+    })
+    .then(accounts => {
+      // When the expression supplied is a beginning substring of 'self',
+      // add it as an autocomplete option.
+      if (SELF_EXPRESSION.startsWith(accountPrefix)) {
+        return accounts.concat([{text: predicate + ':' + SELF_EXPRESSION}]);
+      } else if (ME_EXPRESSION.startsWith(accountPrefix)) {
+        return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]);
+      } else {
+        return accounts;
+      }
+    });
+}
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index a78254d..1a6f8d2 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -6,12 +6,12 @@
 import {getBaseUrl} from './url-util';
 import {ChangeStatus} from '../constants/constants';
 import {
+  AccountInfo,
+  ChangeInfo,
+  ChangeStates,
   NumericChangeId,
   PatchSetNum,
-  ChangeInfo,
-  AccountInfo,
   RelatedChangeAndCommitInfo,
-  ChangeStates,
 } from '../types/common';
 import {ParsedChangeInfo} from '../types/types';
 import {getUserId, isServiceUser} from './account-util';
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
index 51e935e..b17cc1c 100644
--- a/polygerrit-ui/app/utils/change-util_test.ts
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -9,6 +9,7 @@
 import {
   createAccountWithId,
   createChange,
+  createChangeWithStatus,
   createRevisions,
   createServiceUserWithId,
 } from '../test/test-data-generators';
@@ -22,14 +23,14 @@
 } from '../types/common';
 import {
   changeBaseURL,
-  changeIsOpen,
-  changeIsMerged,
   changeIsAbandoned,
+  changeIsMerged,
+  changeIsOpen,
   changePath,
   changeStatuses,
+  hasHumanReviewer,
   isRemovableReviewer,
   listChangesOptionsToHex,
-  hasHumanReviewer,
 } from './change-util';
 
 suite('change-util tests', () => {
@@ -56,12 +57,7 @@
   });
 
   test('Open status', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      mergeable: true,
-    };
+    const change = createChangeWithStatus(ChangeStatus.NEW, true);
     let statuses = changeStatuses(change);
     assert.deepEqual(statuses, []);
 
@@ -88,35 +84,19 @@
   });
 
   test('Merge conflict', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.NEW,
-      mergeable: false,
-    };
+    const change = createChangeWithStatus(ChangeStatus.NEW, false);
     const statuses = changeStatuses(change);
     assert.deepEqual(statuses, [ChangeStates.MERGE_CONFLICT]);
   });
 
   test('mergeable prop undefined', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.NEW,
-    };
+    const change = createChangeWithStatus(ChangeStatus.NEW);
     const statuses = changeStatuses(change);
     assert.deepEqual(statuses, []);
   });
 
   test('Merged status', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.MERGED,
-    };
+    const change = createChangeWithStatus(ChangeStatus.MERGED);
     assert.deepEqual(changeStatuses(change), [ChangeStates.MERGED]);
     change.is_private = true;
     assert.deepEqual(changeStatuses(change), [ChangeStates.MERGED]);
@@ -149,13 +129,7 @@
   });
 
   test('Abandoned status', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.ABANDONED,
-      mergeable: false,
-    };
+    const change = createChangeWithStatus(ChangeStatus.ABANDONED, false);
     assert.deepEqual(changeStatuses(change), [ChangeStates.ABANDONED]);
     change.is_private = true;
     assert.deepEqual(changeStatuses(change), [ChangeStates.ABANDONED]);
@@ -267,39 +241,21 @@
   });
 
   test('changeIsOpen', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.NEW,
-      mergeable: false,
-    };
+    const change = createChangeWithStatus(ChangeStatus.NEW, false);
     assert.isTrue(changeIsOpen(change));
     change.status = ChangeStatus.MERGED;
     assert.isFalse(changeIsOpen(change));
   });
 
   test('changeIsMerged', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.MERGED,
-      mergeable: false,
-    };
+    const change = createChangeWithStatus(ChangeStatus.MERGED, false);
     assert.isTrue(changeIsMerged(change));
     change.status = ChangeStatus.NEW;
     assert.isFalse(changeIsMerged(change));
   });
 
   test('changeIsAbandoned', () => {
-    const change = {
-      ...createChange(),
-      revisions: createRevisions(1),
-      current_revision: 'rev1' as CommitId,
-      status: ChangeStatus.ABANDONED,
-      mergeable: false,
-    };
+    const change = createChangeWithStatus(ChangeStatus.ABANDONED, false);
     assert.isTrue(changeIsAbandoned(change));
     change.status = ChangeStatus.NEW;
     assert.isFalse(changeIsAbandoned(change));
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index ecc3c99..c618e28 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -4,37 +4,37 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {
-  CommentInfo,
-  PatchSetNum,
-  UrlEncodedCommentId,
-  PatchRange,
-  PARENT,
-  ContextLine,
-  BasePatchSetNum,
-  RevisionPatchSetNum,
-  AccountInfo,
   AccountDetailInfo,
-  VotingRangeInfo,
-  FixSuggestionInfo,
-  FixId,
-  PatchSetNumber,
-  CommentThread,
-  DraftInfo,
+  AccountInfo,
+  BasePatchSetNum,
   ChangeMessage,
-  isRobot,
-  isDraft,
   Comment,
   CommentIdToCommentThreadMap,
-  SavingState,
-  NewDraftInfo,
-  isNew,
+  CommentInfo,
   CommentInput,
   CommentRange,
+  CommentThread,
+  ContextLine,
+  DraftInfo,
+  FixId,
+  FixReplacementInfo,
+  FixSuggestionInfo,
+  isDraft,
+  isNew,
+  NewDraftInfo,
+  PARENT,
+  PatchRange,
+  PatchSetNum,
+  PatchSetNumber,
+  RevisionPatchSetNum,
+  SavingState,
+  UrlEncodedCommentId,
+  VotingRangeInfo,
 } from '../types/common';
 import {CommentSide, SpecialFilePath} from '../constants/constants';
 import {parseDate} from './date-util';
 import {specialFilePathCompare} from './path-list-util';
-import {isMergeParent, getParentIndex} from './patch-set-util';
+import {getParentIndex, isMergeParent} from './patch-set-util';
 import {DiffInfo} from '../types/diff';
 import {FormattedReviewerUpdateInfo} from '../types/types';
 import {extractMentionedUsers} from './account-util';
@@ -218,7 +218,33 @@
   };
 }
 
+/**
+ * Ensures that all comments that are replies have the same range as their
+ * parent.
+ * sortComments requires the ranges of all comments in a thread to match.
+ * It's possible to create a reply via REST API without the range being set
+ * hence ensure the ranges are properly derived before forming threads.
+ */
+export function sanitiseRanges(comments: Comment[]) {
+  const idToComment = new Map<string, Comment>();
+  for (const comment of comments) {
+    if (comment.id) {
+      idToComment.set(comment.id, comment);
+    }
+  }
+
+  for (const comment of comments) {
+    if (comment.in_reply_to) {
+      const parent = idToComment.get(comment.in_reply_to);
+      if (parent?.range && !comment.range) {
+        comment.range = {...parent.range};
+      }
+    }
+  }
+}
+
 export function createCommentThreads(comments: Comment[]) {
+  sanitiseRanges(comments);
   const sortedComments = sortComments(comments);
   const threads: CommentThread[] = [];
   const idThreadMap: CommentIdToCommentThreadMap = {};
@@ -337,10 +363,6 @@
     .includes(account.email);
 }
 
-export function isRobotThread(thread: CommentThread): boolean {
-  return isRobot(getFirstComment(thread));
-}
-
 export function hasSuggestion(thread: CommentThread): boolean {
   const firstComment = getFirstComment(thread);
   if (!firstComment) return false;
@@ -350,10 +372,6 @@
   );
 }
 
-export function hasHumanReply(thread: CommentThread): boolean {
-  return countComments(thread) > 1 && !isRobot(getLastComment(thread));
-}
-
 export function lastUpdated(thread: CommentThread): Date | undefined {
   // We don't want to re-sort comments when you save a draft reply, so
   // we stick to the timestampe of the last *published* comment.
@@ -557,9 +575,7 @@
 
 // This can either mean a user or a checks provided fix.
 // "Provided" means that the fix is sent along with the request
-// when previewing and applying the fix. This is in contrast to
-// robot comment fixes, which are stored in the backend, and they
-// are referenced by a unique `FixId`;
+// when previewing and applying the fix.
 export const PROVIDED_FIX_ID = 'provided_fix' as FixId;
 
 export function hasUserSuggestion(comment: Comment) {
@@ -696,3 +712,74 @@
 export function isFileLevelComment(comment: Comment) {
   return !comment.line && !comment.range;
 }
+
+const START_REPLACEMENT_DELIMITER = '--- START REPLACEMENT';
+const END_REPLACEMENT_DELIMITER = '--- END REPLACEMENT ---';
+
+export function replacementsToString(
+  replacements: FixReplacementInfo[]
+): string {
+  return replacements
+    .map(r => {
+      const range = `${r.range.start_line}-${r.range.end_line}`;
+      return `${START_REPLACEMENT_DELIMITER} path=${r.path} range=${range}
+${r.replacement}
+${END_REPLACEMENT_DELIMITER}`;
+    })
+    .join('\n\n');
+}
+
+/**
+ * Parses a string containing one or more replacement blocks into an array of
+ * `FixReplacementInfo` objects.
+ *
+ * Each replacement block in the string is expected to be in the format:
+ * --- START REPLACEMENT path=[file_path] range=[start_line]-[end_line]
+ * [replacement_code]
+ * --- END REPLACEMENT ---
+ *
+ * This function assumes `start_character` and `end_character` for the range
+ * are always 0.
+ *
+ * @param text The raw string containing one or more replacement blocks.
+ * @return An array of `FixReplacementInfo` objects.
+ * @throws An error if the input string cannot be parsed into at least one
+ *     replacement.
+ *
+ * @example
+ * const text = `
+ * --- START REPLACEMENT path=src/main.js range=10-12
+ *   console.log("Hello, world!");
+ * --- END REPLACEMENT ---
+ * `;
+ * const replacements = stringToReplacements(text);
+ * // replacements would be:
+ * // [
+ * //   {
+ * //     path: 'src/main.js',
+ * //     range: { start_line: 10, end_line: 12, start_character: 0, end_character: 0 },
+ * //     replacement: '  console.log("Hello, world!");'
+ * //   }
+ * // ]
+ */
+export function stringToReplacements(text: string): FixReplacementInfo[] {
+  const regex =
+    /--- START REPLACEMENT path=(.*?) range=(\d+)-(\d+)\n([\s\S]*?)\n--- END REPLACEMENT ---/g;
+  const replacements: FixReplacementInfo[] = [];
+  let match;
+  while ((match = regex.exec(text)) !== null) {
+    const path = match[1].trim();
+    const start_line = Number(match[2]);
+    const end_line = Number(match[3]);
+    const replacement = match[4];
+    replacements.push({
+      path,
+      range: {start_line, end_line, start_character: 0, end_character: 0},
+      replacement,
+    });
+  }
+  if (replacements.length === 0) {
+    throw new Error('Could not parse replacements from text');
+  }
+  return replacements;
+}
diff --git a/polygerrit-ui/app/utils/comment-util_test.ts b/polygerrit-ui/app/utils/comment-util_test.ts
index 031a738..be99553 100644
--- a/polygerrit-ui/app/utils/comment-util_test.ts
+++ b/polygerrit-ui/app/utils/comment-util_test.ts
@@ -5,20 +5,23 @@
  */
 import '../test/common-test-setup';
 import {
-  isUnresolved,
-  getPatchRangeForCommentUrl,
   createCommentThreads,
-  sortComments,
-  USER_SUGGESTION_START_PATTERN,
-  hasUserSuggestion,
-  getUserSuggestion,
-  getContentInCommentRange,
-  createUserFixSuggestion,
-  PROVIDED_FIX_ID,
-  getMentionedThreads,
-  isNewThread,
   createNew,
+  createUserFixSuggestion,
+  getContentInCommentRange,
+  getMentionedThreads,
+  getPatchRangeForCommentUrl,
+  getUserSuggestion,
   getUserSuggestionFromString,
+  hasUserSuggestion,
+  isNewThread,
+  isUnresolved,
+  PROVIDED_FIX_ID,
+  replacementsToString,
+  sanitiseRanges,
+  sortComments,
+  stringToReplacements,
+  USER_SUGGESTION_START_PATTERN,
 } from './comment-util';
 import {
   createAccountWithEmail,
@@ -28,9 +31,10 @@
 import {CommentSide, SpecialFilePath} from '../constants/constants';
 import {
   Comment,
-  SavingState,
+  FixReplacementInfo,
   PARENT,
   RevisionPatchSetNum,
+  SavingState,
   Timestamp,
   UrlEncodedCommentId,
 } from '../types/common';
@@ -599,4 +603,88 @@
       );
     });
   });
+  suite('stringToReplacements and replacementsToString', () => {
+    const REPLACEMENT_1: FixReplacementInfo = {
+      path: 'a/b/c.js',
+      range: {start_line: 1, end_line: 2, start_character: 0, end_character: 0},
+      replacement: 'line 1\nline 2',
+    };
+    const REPLACEMENT_2: FixReplacementInfo = {
+      path: 'd/e/f.js',
+      range: {start_line: 3, end_line: 4, start_character: 0, end_character: 0},
+      replacement: 'line 3',
+    };
+    const TEXT_1 =
+      '--- START REPLACEMENT path=a/b/c.js range=1-2\nline 1\nline 2\n--- END REPLACEMENT ---';
+    const TEXT_2 =
+      '--- START REPLACEMENT path=d/e/f.js range=3-4\nline 3\n--- END REPLACEMENT ---';
+
+    suite('stringToReplacements', () => {
+      test('single replacement', () => {
+        assert.deepEqual(stringToReplacements(TEXT_1), [REPLACEMENT_1]);
+      });
+
+      test('multiple replacements', () => {
+        const text = `${TEXT_1}\n\n${TEXT_2}`;
+        assert.deepEqual(stringToReplacements(text), [
+          REPLACEMENT_1,
+          REPLACEMENT_2,
+        ]);
+      });
+
+      test('throws error for no replacements', () => {
+        assert.throws(() => stringToReplacements('no replacements here'));
+      });
+    });
+
+    suite('replacementsToString', () => {
+      test('single replacement', () => {
+        assert.equal(replacementsToString([REPLACEMENT_1]), TEXT_1);
+      });
+
+      test('multiple replacements', () => {
+        const expectedText = `${TEXT_1}\n\n${TEXT_2}`;
+        assert.equal(
+          replacementsToString([REPLACEMENT_1, REPLACEMENT_2]),
+          expectedText
+        );
+      });
+
+      test('empty array', () => {
+        assert.equal(replacementsToString([]), '');
+      });
+    });
+  });
+
+  suite('sanitiseRanges', () => {
+    test('copies range from parent to child', () => {
+      const comments: Comment[] = [
+        {
+          id: 'parent_comment' as UrlEncodedCommentId,
+          message: 'i am a parent',
+          updated: '2015-12-23 15:00:20.396000000' as Timestamp,
+          line: 1,
+          patch_set: 1 as RevisionPatchSetNum,
+          path: 'some/path',
+          range: {
+            start_line: 1,
+            start_character: 0,
+            end_line: 2,
+            end_character: 0,
+          },
+        },
+        {
+          id: 'child_comment' as UrlEncodedCommentId,
+          message: 'i am a child',
+          updated: '2015-12-24 15:01:20.396000000' as Timestamp,
+          line: 1,
+          in_reply_to: 'parent_comment' as UrlEncodedCommentId,
+          patch_set: 1 as RevisionPatchSetNum,
+          path: 'some/path',
+        },
+      ];
+      sanitiseRanges(comments);
+      assert.deepEqual(comments[1].range, comments[0].range);
+    });
+  });
 });
diff --git a/polygerrit-ui/app/utils/commit-message-formatter-util.ts b/polygerrit-ui/app/utils/commit-message-formatter-util.ts
index e1b3e27..6d80a39 100644
--- a/polygerrit-ui/app/utils/commit-message-formatter-util.ts
+++ b/polygerrit-ui/app/utils/commit-message-formatter-util.ts
@@ -32,9 +32,45 @@
 const MAX_LINE_LENGTH = 72;
 const INDENTATION_THRESHOLD = 4;
 const BULLET_POINT_REGEX = /^\s*[-+*#]\s/;
+const FOOTER_REGEX = /^([\w-]+):[ \t]+(.+)$/;
+
+/*
+ * Check if last line of "Body" follows the "footer" format and if yes, then transfer it to the "footer section"
+ * So if the commit message is
+ * Foo
+ *
+ * Footer1: val1
+ *
+ * Footer2: val2
+ *
+ * Current message.footer will only contain val2 and the rest will be considered in the body
+ * Note: This does not support multi-line footers.
+ */
+
+function portFootersFromBody(message: CommitMessage): CommitMessage {
+  const footersToPortOver = [];
+  for (let i = message.body.length - 1; i >= 0; i--) {
+    const line = message.body[i];
+    const match = line.match(FOOTER_REGEX);
+    // If it's a footer line or an empty blank line, then we remove it from the body
+    if (match || line.trim() === '') {
+      if (line.trim() !== '') {
+        footersToPortOver.push(line);
+      }
+      message.body.pop();
+    } else {
+      break;
+    }
+  }
+  if (footersToPortOver.length > 0) {
+    message.footer.unshift(...footersToPortOver.reverse());
+  }
+  return message;
+}
 
 function formatCommitMessage(message: CommitMessage): CommitMessage {
   const formattedSubject = formatSubject(message.subject);
+  message = portFootersFromBody(message);
   const formattedBody = formatBody(message.body);
   const formattedFooter = formatFooter(message.footer);
 
@@ -319,7 +355,7 @@
   return errors;
 }
 
-function parseCommitMessageString(messageString: string): CommitMessage {
+export function parseCommitMessageString(messageString: string): CommitMessage {
   const lines = messageString.split('\n');
   // Remove leading blank lines
   while (lines.length > 0 && lines[0].trim() === '') {
@@ -356,7 +392,7 @@
 
   // Extract body lines, removing all leading/trailing blank lines
   body = lines.slice(
-    firstNonEmptyLineIndex(lines, 2, /* direction */ 1),
+    firstNonEmptyLineIndex(lines, 1, /* direction */ 1),
     firstNonEmptyLineIndex(lines, footerStartIndex - 1, /* direction */ -1) + 1
   );
 
@@ -364,7 +400,7 @@
   // If not, move footer lines to body and make footer empty
   // This is typically the case when creating a new commit message and the footer is not yet formatted
   const hasFormattedFooterLine = footer.some(line =>
-    /^[^:]+:.+/.test(line.trim())
+    FOOTER_REGEX.test(line.trim())
   );
   if (!hasFormattedFooterLine && footer.length > 0) {
     // If body is not empty, add a blank line before appending footer
diff --git a/polygerrit-ui/app/utils/commit-message-formatter-util_test.ts b/polygerrit-ui/app/utils/commit-message-formatter-util_test.ts
index f14480c..b4ba84b 100644
--- a/polygerrit-ui/app/utils/commit-message-formatter-util_test.ts
+++ b/polygerrit-ui/app/utils/commit-message-formatter-util_test.ts
@@ -6,11 +6,11 @@
 import {assert} from '@open-wc/testing';
 import '../test/common-test-setup';
 import {
-  formatCommitMessageString,
+  CommitMessage,
   detectFormattingErrorsInString,
   ErrorType,
+  formatCommitMessageString,
   FormattingError,
-  CommitMessage,
   TEST_ONLY,
 } from './commit-message-formatter-util';
 
@@ -185,6 +185,23 @@
           'Change-Id: abcdefg\n'
       );
     });
+
+    test('reproduction of bug: multiple paragraphs wrapping', () => {
+      const message =
+        'Generic subject that is long enough\n\n' +
+        'This is the first paragraph of the body which is quite long and should be wrapped correctly by the formatter without any issues.\n\n' +
+        'This is the second paragraph which triggered the bug because it looks like a footer due to a colon: happening after some words. This should be wrapped.';
+
+      const expected =
+        'Generic subject that is long enough\n\n' +
+        'This is the first paragraph of the body which is quite long and should\n' +
+        'be wrapped correctly by the formatter without any issues.\n\n' +
+        'This is the second paragraph which triggered the bug because it looks\n' +
+        'like a footer due to a colon: happening after some words. This should be\n' +
+        'wrapped.';
+
+      assert.equal(formatCommitMessageString(message), expected);
+    });
   });
 
   suite('detectFormattingErrorsInString', () => {
@@ -433,6 +450,28 @@
       );
     });
 
+    test('without extra blank line after subject', () => {
+      assertParseResult(
+        'Fix the thing\nThis is the body.\n\nFixes: #123\nChange-Id: abcdefg\n',
+        {
+          subject: 'Fix the thing',
+          body: ['This is the body.'],
+          footer: ['Fixes: #123', 'Change-Id: abcdefg'],
+          hasTrailingBlankLine: true,
+        },
+        'with trailing blank line'
+      );
+    });
+
+    test('footers separated by a blank line', () => {
+      assert.equal(
+        formatCommitMessageString(
+          'Fix the thing\n\nThis is the body.\n\nTest: 123\n\nChange-Id: abcdefg\n'
+        ),
+        'Fix the thing\n\nThis is the body.\n\nTest: 123\nChange-Id: abcdefg\n'
+      );
+    });
+
     test('footer without proper format is moved to body', () => {
       assertParseResult(
         'Subject\n\nBody line\n\nThis is not a proper footer line\nNeither is this one',
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index c89bdff..b14c856 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -171,7 +171,7 @@
   return a.filter(aVal => !b.some(bVal => compareBy(aVal, bVal)));
 }
 
-export async function copyToClipbard(text: string, copyTargetName?: string) {
+export async function copyToClipboard(text: string, copyTargetName?: string) {
   await navigator.clipboard.writeText(text);
   fireAlert(document, `${copyTargetName ?? text} was copied to clipboard`);
 }
diff --git a/polygerrit-ui/app/utils/common-util_test.ts b/polygerrit-ui/app/utils/common-util_test.ts
index 76c8a6c..282fe4d 100644
--- a/polygerrit-ui/app/utils/common-util_test.ts
+++ b/polygerrit-ui/app/utils/common-util_test.ts
@@ -6,11 +6,11 @@
 import {assert} from '@open-wc/testing';
 import '../test/common-test-setup';
 import {
-  hasOwnProperty,
   areSetsEqual,
   containsAll,
-  intersection,
   difference,
+  hasOwnProperty,
+  intersection,
   toggle,
 } from './common-util';
 
diff --git a/polygerrit-ui/app/utils/dashboard-util.ts b/polygerrit-ui/app/utils/dashboard-util.ts
index 6087e2c..dd25ff14 100644
--- a/polygerrit-ui/app/utils/dashboard-util.ts
+++ b/polygerrit-ui/app/utils/dashboard-util.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {ChangeConfigInfo, ChangeInfo} from '../api/rest-api';
+import {UserId} from '../types/common';
 
 export interface DashboardSection {
   name: string;
@@ -100,7 +101,7 @@
 ];
 
 export function getUserDashboard(
-  user = 'self',
+  user: UserId | 'self' = 'self',
   sections = DEFAULT_SECTIONS,
   title = ''
 ): UserDashboard {
@@ -110,7 +111,8 @@
       return {
         ...section,
         name: section.name,
-        query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
+        // user is usually account_id which is type number
+        query: section.query.replace(USER_PLACEHOLDER_PATTERN, String(user)),
       };
     });
   return {title, sections};
diff --git a/polygerrit-ui/app/utils/dashboard-util_test.ts b/polygerrit-ui/app/utils/dashboard-util_test.ts
index 40e4ad9..6f53eb2 100644
--- a/polygerrit-ui/app/utils/dashboard-util_test.ts
+++ b/polygerrit-ui/app/utils/dashboard-util_test.ts
@@ -6,6 +6,7 @@
 import {assert} from '@open-wc/testing';
 import '../test/common-test-setup';
 import {getUserDashboard} from './dashboard-util';
+import {EmailAddress} from '../api/rest-api';
 
 suite('gr-navigation tests', () => {
   suite('_getUserDashboard', () => {
@@ -38,12 +39,16 @@
     });
 
     test('dashboard for other user', () => {
-      const dashboard = getUserDashboard('user', sections, 'title');
+      const dashboard = getUserDashboard(
+        'user@email.com' as EmailAddress,
+        sections,
+        'title'
+      );
       assert.deepEqual(dashboard, {
         title: 'title',
         sections: [
           {name: 'section 1', query: 'query 1'},
-          {name: 'section 2', query: 'query 2 for user'},
+          {name: 'section 2', query: 'query 2 for user@email.com'},
           {
             name: 'section 4',
             query: 'query 4',
diff --git a/polygerrit-ui/app/utils/date-util.ts b/polygerrit-ui/app/utils/date-util.ts
index d95d24a..a078b1d 100644
--- a/polygerrit-ui/app/utils/date-util.ts
+++ b/polygerrit-ui/app/utils/date-util.ts
@@ -25,43 +25,64 @@
 }
 
 // similar to fromNow from moment.js
-export function fromNow(date: Date, noAgo = false) {
-  return durationString(date, new Date(), noAgo);
+export function relative(date: Date, affix = false) {
+  const now = new Date();
+  if (now > date) {
+    return fromNow(date, affix);
+  } else {
+    return toNow(date, affix);
+  }
 }
 
 // similar to fromNow from moment.js
-export function durationString(from: Date, to: Date, noAgo = false) {
-  const ago = noAgo ? '' : ' ago';
-  const secondsAgo = Math.floor((to.valueOf() - from.valueOf()) / 1000);
-  if (secondsAgo <= 59) {
-    if (noAgo) return `${secondsAgo} seconds`;
+export function fromNow(date: Date, noAgo = false) {
+  return durationString(date, new Date(), '', noAgo ? '' : 'ago');
+}
+
+// similar to toNow from moment.js
+export function toNow(date: Date, noIn = false) {
+  return durationString(new Date(), date, noIn ? '' : 'in', '');
+}
+
+// similar to fromNow from moment.js
+export function durationString(
+  from: Date,
+  to: Date,
+  prefix: string = '',
+  suffix: string = ''
+) {
+  prefix = prefix ? `${prefix.trim()} ` : '';
+  suffix = suffix ? ` ${suffix.trim()}` : '';
+  const seconds = Math.floor((to.valueOf() - from.valueOf()) / 1000);
+  if (seconds <= 59) {
+    if (!prefix && !suffix) return `${seconds} seconds`;
     return 'just now';
   }
-  if (secondsAgo <= 119) return `1 minute${ago}`;
-  const minutesAgo = Math.floor(secondsAgo / 60);
-  if (minutesAgo <= 59) return `${minutesAgo} minutes${ago}`;
-  if (minutesAgo === 60) return `1 hour${ago}`;
-  if (minutesAgo <= 119) return `1 hour ${minutesAgo - 60} min${ago}`;
-  const hoursAgo = Math.floor(minutesAgo / 60);
-  if (hoursAgo <= 23) return `${hoursAgo} hours${ago}`;
-  if (hoursAgo === 24) return `1 day${ago}`;
-  if (hoursAgo <= 47) return `1 day ${hoursAgo - 24} hr${ago}`;
-  const daysAgo = Math.floor(hoursAgo / 24);
-  if (daysAgo <= 30) return `${daysAgo} days${ago}`;
-  if (daysAgo <= 60) return `1 month${ago}`;
-  const monthsAgo = Math.floor(daysAgo / 30);
-  if (monthsAgo <= 11) return `${monthsAgo} months${ago}`;
-  if (monthsAgo === 12) return `1 year${ago}`;
-  if (monthsAgo <= 24) return `1 year ${monthsAgo - 12} m${ago}`;
-  const yearsAgo = Math.floor(daysAgo / 365);
-  return `${yearsAgo} years${ago}`;
+  if (seconds <= 119) return `${prefix}1 minute${suffix}`;
+  const minutes = Math.floor(seconds / 60);
+  if (minutes <= 59) return `${prefix}${minutes} minutes${suffix}`;
+  if (minutes === 60) return `${prefix}1 hour${suffix}`;
+  if (minutes <= 119) return `${prefix}1 hour ${minutes - 60} min${suffix}`;
+  const hours = Math.floor(minutes / 60);
+  if (hours <= 23) return `${prefix}${hours} hours${suffix}`;
+  if (hours === 24) return `${prefix}1 day${suffix}`;
+  if (hours <= 47) return `${prefix}1 day ${hours - 24} hr${suffix}`;
+  const days = Math.floor(hours / 24);
+  if (days <= 30) return `${prefix}${days} days${suffix}`;
+  if (days <= 60) return `${prefix}1 month${suffix}`;
+  const months = Math.floor(days / 30);
+  if (months <= 11) return `${prefix}${months} months${suffix}`;
+  if (months === 12) return `${prefix}1 year${suffix}`;
+  if (months <= 24) return `${prefix}1 year ${months - 12} m${suffix}`;
+  const years = Math.floor(days / 365);
+  return `${prefix}${years} years${suffix}`;
 }
 
 /**
  * Return true if date is within 24 hours and on the same day.
  */
 export function isWithinDay(now: Date, date: Date) {
-  const diff = now.valueOf() - date.valueOf();
+  const diff = Math.abs(now.valueOf() - date.valueOf());
   return diff < Duration.DAY && date.getDay() === now.getDay();
 }
 
@@ -79,7 +100,7 @@
  * Returns true if date is from one to six months.
  */
 export function isWithinHalfYear(now: Date, date: Date) {
-  const diff = now.valueOf() - date.valueOf();
+  const diff = Math.abs(now.valueOf() - date.valueOf());
   return diff < 180 * Duration.DAY;
 }
 
diff --git a/polygerrit-ui/app/utils/date-util_test.ts b/polygerrit-ui/app/utils/date-util_test.ts
index 45cdc25..90164f9 100644
--- a/polygerrit-ui/app/utils/date-util_test.ts
+++ b/polygerrit-ui/app/utils/date-util_test.ts
@@ -7,12 +7,13 @@
 import {Timestamp} from '../types/common';
 import '../test/common-test-setup';
 import {
-  isValidDate,
-  parseDate,
+  formatDate,
   fromNow,
+  isValidDate,
   isWithinDay,
   isWithinHalfYear,
-  formatDate,
+  parseDate,
+  toNow,
   wasYesterday,
 } from './date-util';
 import {assert} from '@open-wc/testing';
@@ -62,6 +63,31 @@
     });
   });
 
+  suite('toNow', () => {
+    test('test all variants', () => {
+      const fakeNow = new Date('May 08 2020 12:00:00');
+      sinon.useFakeTimers(fakeNow.getTime());
+      assert.equal('just now', toNow(new Date('May 08 2020 12:00:30')));
+      assert.equal('in 1 minute', toNow(new Date('May 08 2020 12:01:00')));
+      assert.equal('in 5 minutes', toNow(new Date('May 08 2020 12:05:00')));
+      assert.equal('in 1 hour', toNow(new Date('May 08 2020 13:00:00')));
+      assert.equal('in 1 hour 5 min', toNow(new Date('May 08 2020 13:05:00')));
+      assert.equal('in 3 hours', toNow(new Date('May 08 2020 15:00:00')));
+      assert.equal('in 1 day', toNow(new Date('May 09 2020 12:00:00')));
+      assert.equal('in 1 day 2 hr', toNow(new Date('May 09 2020 14:00:00')));
+      assert.equal('in 3 days', toNow(new Date('May 11 2020 12:00:00')));
+      assert.equal('in 1 month', toNow(new Date('Jun 08 2020 12:00:00')));
+      assert.equal('in 2 months', toNow(new Date('Jul 08 2020 12:00:00')));
+      assert.equal('in 1 year', toNow(new Date('May 08 2021 12:00:00')));
+      assert.equal('in 10 years', toNow(new Date('May 08 2030 12:00:00')));
+    });
+    test('rounding error', () => {
+      const fakeNow = new Date('May 08 2020 12:00:00');
+      sinon.useFakeTimers(fakeNow.getTime());
+      assert.equal('in 2 hours', toNow(new Date('May 08 2020 14:30:00')));
+    });
+  });
+
   suite('isWithinDay', () => {
     test('basics works', () => {
       assert.isTrue(
diff --git a/polygerrit-ui/app/utils/diff-util.ts b/polygerrit-ui/app/utils/diff-util.ts
index 3e46120..f6b00d4 100644
--- a/polygerrit-ui/app/utils/diff-util.ts
+++ b/polygerrit-ui/app/utils/diff-util.ts
@@ -18,6 +18,32 @@
   }, 0);
 }
 
+export function isLineUnchanged(
+  diff?: DiffInfo,
+  side?: Side,
+  line?: number
+): boolean {
+  if (!diff?.content || !side || !line) return false;
+  let currentLine = 0;
+  for (const chunk of diff.content) {
+    if (chunk.skip) {
+      currentLine += chunk.skip;
+      if (currentLine >= line) return false;
+    } else if (chunk.ab) {
+      currentLine += chunk.ab.length;
+      if (currentLine >= line) return true;
+    } else {
+      const chunkLength =
+        (side === Side.LEFT ? chunk.a?.length : chunk.b?.length) ?? 0;
+      currentLine += chunkLength;
+      if (currentLine >= line) {
+        return chunk.common ?? false;
+      }
+    }
+  }
+  return false;
+}
+
 /**
  * Get the lines of the diff for a given side.
  */
diff --git a/polygerrit-ui/app/utils/diff-util_test.ts b/polygerrit-ui/app/utils/diff-util_test.ts
index 2829209..838fab3 100644
--- a/polygerrit-ui/app/utils/diff-util_test.ts
+++ b/polygerrit-ui/app/utils/diff-util_test.ts
@@ -7,7 +7,11 @@
 import {DiffInfo, Side} from '../api/diff';
 import '../test/common-test-setup';
 import {createDiff} from '../test/test-data-generators';
-import {getContentFromDiff, isFileUnchanged} from './diff-util';
+import {
+  getContentFromDiff,
+  isFileUnchanged,
+  isLineUnchanged,
+} from './diff-util';
 
 suite('diff-util tests', () => {
   test('isFileUnchanged', () => {
@@ -42,6 +46,83 @@
     assert.equal(isFileUnchanged(diff), true);
   });
 
+  suite('isLineUnchanged()', () => {
+    test('all lines changed', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [{a: ['abcd']}, {b: ['wxyz']}],
+      };
+
+      assert.isFalse(isLineUnchanged(diff, Side.LEFT, 0));
+      assert.isFalse(isLineUnchanged(diff, Side.LEFT, 1));
+      assert.isFalse(isLineUnchanged(diff, Side.LEFT, 2));
+      assert.isFalse(isLineUnchanged(diff, Side.LEFT, 3));
+
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 0));
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 1));
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 2));
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 3));
+    });
+
+    test('line 2 unchanged', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {a: ['abcd']},
+          {b: ['wxyz']},
+          {ab: ['qwer']},
+          {a: ['abcd']},
+          {b: ['wxyz']},
+        ],
+      };
+
+      assert.isFalse(isLineUnchanged(diff, Side.LEFT, 1));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 2));
+      assert.isFalse(isLineUnchanged(diff, Side.LEFT, 3));
+
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 1));
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 2));
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 3));
+    });
+
+    test('common chunk', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+        content: [
+          {ab: ['qwer']},
+          {a: ['  qwer'], b: ['qwer  '], common: true},
+          {ab: ['qwer']},
+        ],
+      };
+
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 1));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 2));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 3));
+
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 1));
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 2));
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 3));
+    });
+
+    test('4 lines unchanged, then 1 line on the RIGHT changed', () => {
+      const diff: DiffInfo = {
+        ...createDiff(),
+      };
+
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 1));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 2));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 3));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 4));
+      assert.isTrue(isLineUnchanged(diff, Side.LEFT, 5));
+
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 1));
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 2));
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 3));
+      assert.isTrue(isLineUnchanged(diff, Side.RIGHT, 4));
+      assert.isFalse(isLineUnchanged(diff, Side.RIGHT, 5));
+    });
+  });
+
   suite('getContentFromDiff', () => {
     test('one changed line', () => {
       const diff: DiffInfo = {
diff --git a/polygerrit-ui/app/utils/display-name-util.ts b/polygerrit-ui/app/utils/display-name-util.ts
index 5f56e51..6c2a690 100644
--- a/polygerrit-ui/app/utils/display-name-util.ts
+++ b/polygerrit-ui/app/utils/display-name-util.ts
@@ -18,6 +18,8 @@
     return account.username;
   } else if (account?.email) {
     return account.email;
+  } else if (account?.deleted) {
+    return 'Deleted User';
   } else if (
     config &&
     config.user &&
diff --git a/polygerrit-ui/app/utils/display-name-util_test.ts b/polygerrit-ui/app/utils/display-name-util_test.ts
index 2f938a5..f3fa89f 100644
--- a/polygerrit-ui/app/utils/display-name-util_test.ts
+++ b/polygerrit-ui/app/utils/display-name-util_test.ts
@@ -12,11 +12,11 @@
 } from '../api/rest-api';
 import '../test/common-test-setup';
 import {
-  getDisplayName,
-  getUserName,
-  getGroupDisplayName,
-  getAccountDisplayName,
   _testOnly_accountEmail,
+  getAccountDisplayName,
+  getDisplayName,
+  getGroupDisplayName,
+  getUserName,
 } from './display-name-util';
 import {
   createAccountsConfig,
@@ -133,6 +133,13 @@
     assert.deepEqual(getUserName(config, account), 'test-user@test-url.com');
   });
 
+  test('getUserName deleted account', () => {
+    const account: AccountInfo = {
+      deleted: true,
+    };
+    assert.deepEqual(getUserName(config, account), 'Deleted User');
+  });
+
   test('getUserName returns not Anonymous Coward as the anon name', () => {
     assert.deepEqual(getUserName(config, undefined), 'Anonymous');
   });
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 6035e1d..fc527df 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -391,9 +391,11 @@
 }
 
 /**
- * @deprecated
- *
- * For LitElement use the shortcut-controller.
+ * Nearly deprecated: Within <gr-app> please use the shortcut-controller.
+ * The only exception to use this utility directly is for components that are
+ * potentially used outside of <gr-app>. An example of this would be
+ * <gr-button> being used in plugins that add their widgets outside of
+ * <gr-app>.
  */
 export function addShortcut(
   element: HTMLElement,
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 7deae3d..4d455a6 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -16,8 +16,8 @@
   strToClassName,
 } from './dom-util';
 import {mockPromise, pressKey, queryAndAssert} from '../test/test-utils';
-import {fixture, assert} from '@open-wc/testing';
-import {LitElement, html} from 'lit';
+import {assert, fixture} from '@open-wc/testing';
+import {html, LitElement} from 'lit';
 import {customElement} from 'lit/decorators.js';
 
 /**
diff --git a/polygerrit-ui/app/utils/event-util_test.ts b/polygerrit-ui/app/utils/event-util_test.ts
new file mode 100644
index 0000000..b720f5e
--- /dev/null
+++ b/polygerrit-ui/app/utils/event-util_test.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../test/common-test-setup';
+import {assert} from '@open-wc/testing';
+import {
+  fire,
+  fireAlert,
+  fireError,
+  fireNoBubble,
+  fireNoBubbleNoCompose,
+  waitForEventOnce,
+} from './event-util';
+
+declare global {
+  interface HTMLElementEventMap {
+    'custom-event': CustomEvent<{message: string}>;
+  }
+}
+
+suite('event-util tests', () => {
+  let target: HTMLElement;
+
+  setup(() => {
+    target = document.createElement('div');
+    document.body.appendChild(target);
+  });
+
+  teardown(() => {
+    document.body.removeChild(target);
+  });
+
+  test('fire dispatches custom event with correct properties', () => {
+    const detail = {message: 'test'};
+    let receivedEvent: CustomEvent | undefined;
+
+    target.addEventListener('custom-event', (e: Event) => {
+      receivedEvent = e as CustomEvent;
+    });
+
+    fire(target, 'custom-event', detail);
+
+    assert.isTrue(receivedEvent?.bubbles);
+    assert.isTrue(receivedEvent?.composed);
+    assert.deepEqual(receivedEvent?.detail, detail);
+  });
+
+  test('fireNoBubble dispatches non-bubbling event', () => {
+    const detail = {message: 'test'};
+    let receivedEvent: CustomEvent | undefined;
+
+    target.addEventListener('custom-event', (e: Event) => {
+      receivedEvent = e as CustomEvent;
+    });
+
+    fireNoBubble(target, 'custom-event', detail);
+
+    assert.isFalse(receivedEvent?.bubbles);
+    assert.isTrue(receivedEvent?.composed);
+    assert.deepEqual(receivedEvent?.detail, detail);
+  });
+
+  test('fireNoBubbleNoCompose dispatches non-bubbling and non-composed event', () => {
+    const detail = {message: 'test'};
+    let receivedEvent: CustomEvent | undefined;
+
+    target.addEventListener('custom-event', (e: Event) => {
+      receivedEvent = e as CustomEvent;
+    });
+
+    fireNoBubbleNoCompose(target, 'custom-event', detail);
+
+    assert.isFalse(receivedEvent?.bubbles);
+    assert.isFalse(receivedEvent?.composed);
+    assert.deepEqual(receivedEvent?.detail, detail);
+  });
+
+  test('fireAlert dispatches show-alert event with correct detail', () => {
+    const message = 'test alert';
+    let receivedEvent: CustomEvent | undefined;
+
+    target.addEventListener('show-alert', (e: Event) => {
+      receivedEvent = e as CustomEvent;
+    });
+
+    fireAlert(target, message);
+
+    assert.deepEqual(receivedEvent?.detail, {
+      message,
+      showDismiss: true,
+    });
+  });
+
+  test('fireError dispatches show-error event with correct detail', () => {
+    const message = 'test error';
+    let receivedEvent: CustomEvent | undefined;
+
+    target.addEventListener('show-error', (e: Event) => {
+      receivedEvent = e as CustomEvent;
+    });
+
+    fireError(target, message);
+
+    assert.deepEqual(receivedEvent?.detail, {message});
+  });
+
+  test('waitForEventOnce resolves with event when fired', async () => {
+    const detail = {message: 'test'};
+
+    const eventPromise = waitForEventOnce(target, 'custom-event');
+    fire(target, 'custom-event', detail);
+
+    const receivedEvent = await eventPromise;
+    assert.deepEqual(receivedEvent.detail, detail);
+  });
+});
diff --git a/polygerrit-ui/app/utils/focusable.ts b/polygerrit-ui/app/utils/focusable.ts
index d5bed09..acb993c 100644
--- a/polygerrit-ui/app/utils/focusable.ts
+++ b/polygerrit-ui/app/utils/focusable.ts
@@ -32,7 +32,7 @@
   for (const node of children.filter(
     node => node instanceof HTMLElement || node instanceof SVGElement
   )) {
-    yield* getFocusableElements(node as HTMLElement | SVGElement);
+    yield* getFocusableElements(node);
   }
 }
 
@@ -58,7 +58,7 @@
   for (const node of children
     .filter(node => node instanceof HTMLElement || node instanceof SVGElement)
     .reverse()) {
-    yield* getFocusableElementsReverse(node as HTMLElement | SVGElement);
+    yield* getFocusableElementsReverse(node);
   }
 
   if (el.matches(FOCUSABLE_QUERY)) {
diff --git a/polygerrit-ui/app/utils/focusable_test.ts b/polygerrit-ui/app/utils/focusable_test.ts
index 6ae40ec..ac900e1 100644
--- a/polygerrit-ui/app/utils/focusable_test.ts
+++ b/polygerrit-ui/app/utils/focusable_test.ts
@@ -6,7 +6,7 @@
 import '../test/common-test-setup';
 import {getFocusableElements, getFocusableElementsReverse} from './focusable';
 import {html, render} from 'lit';
-import {fixture, assert} from '@open-wc/testing';
+import {assert, fixture} from '@open-wc/testing';
 
 async function createDom() {
   const container = await fixture<HTMLDivElement>(html`<div></div>`);
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index dd1c894..ee7aa3a 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -6,10 +6,10 @@
 import {
   ChangeInfo,
   isQuickLabelInfo,
-  SubmitRequirementResultInfo,
-  SubmitRequirementStatus,
   LabelNameToValuesMap,
   LabelValueToDescriptionMap,
+  SubmitRequirementResultInfo,
+  SubmitRequirementStatus,
 } from '../api/rest-api';
 import {
   AccountInfo,
@@ -21,7 +21,7 @@
   VotingRangeInfo,
 } from '../types/common';
 import {ParsedChangeInfo} from '../types/types';
-import {assertNever, unique, hasOwnProperty} from './common-util';
+import {assertNever, hasOwnProperty, unique} from './common-util';
 
 export interface Label {
   name: string;
@@ -277,23 +277,39 @@
   return labels;
 }
 
+export interface ExtractLabelsOptions {
+  extractFromSubmittability: boolean;
+  extractFromOverride: boolean | 'onlyIfOverridden';
+}
+
 export function extractAssociatedLabels(
   requirement: SubmitRequirementResultInfo,
-  type: 'all' | 'onlyOverride' | 'onlySubmittability' = 'all'
+  options: ExtractLabelsOptions = {
+    extractFromSubmittability: true,
+    extractFromOverride: true,
+  }
 ): string[] {
   let labels: string[] = [];
-  if (requirement.submittability_expression_result && type !== 'onlyOverride') {
+  if (
+    requirement.submittability_expression_result &&
+    options.extractFromSubmittability
+  ) {
     labels = labels.concat(
       extractLabelsFrom(requirement.submittability_expression_result.expression)
     );
   }
-  if (requirement.override_expression_result && type !== 'onlySubmittability') {
+  const overridden = !!requirement.override_expression_result?.fulfilled;
+  const extractFromOverride =
+    options.extractFromOverride === true ||
+    (overridden && options.extractFromOverride === 'onlyIfOverridden');
+  if (requirement.override_expression_result && extractFromOverride) {
     labels = labels.concat(
       extractLabelsFrom(requirement.override_expression_result.expression)
     );
   }
   return labels.filter(unique);
 }
+
 export interface SubmitRequirementsIcon {
   // The material icon name.
   icon: string;
@@ -335,7 +351,16 @@
  * Show only applicable.
  */
 export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
-  return (change?.submit_requirements ?? []).filter(
+  return getApplicableRequirements(change?.submit_requirements);
+}
+
+/**
+ * Get applicable requirements.
+ */
+export function getApplicableRequirements(
+  requirements?: SubmitRequirementResultInfo[]
+) {
+  return (requirements ?? []).filter(
     req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
   );
 }
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index a0b6c50..b73bf7c 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -5,28 +5,36 @@
  */
 import '../test/common-test-setup';
 import {
+  canReviewerVote,
+  computeLabels,
+  computeOrderedLabelValues,
   extractAssociatedLabels,
+  extractLabelsWithCountFrom,
+  getApplicableLabels,
   getApprovalInfo,
+  getCodeReviewLabel,
+  getDefaultValue,
   getLabelStatus,
   getMaxAccounts,
   getRepresentativeValue,
-  getVotingRange,
-  getVotingRangeOrDefault,
   getRequirements,
   getTriggerVotes,
+  getVotingRange,
+  getVotingRangeOrDefault,
+  hasApprovedVote,
   hasNeutralStatus,
+  hasRejectedVote,
+  hasVoted,
+  hasVotes,
+  isBlockingCondition,
   labelCompare,
   LabelStatus,
-  computeLabels,
-  mergeLabelMaps,
-  computeOrderedLabelValues,
   mergeLabelInfoMaps,
-  getApplicableLabels,
-  isBlockingCondition,
+  mergeLabelMaps,
+  orderSubmitRequirementNames,
+  orderSubmitRequirements,
+  StandardLabels,
   valueString,
-  hasVotes,
-  hasVoted,
-  extractLabelsWithCountFrom,
 } from './label-util';
 import {
   AccountId,
@@ -38,20 +46,21 @@
 } from '../types/common';
 import {
   createAccountWithEmail,
+  createAccountWithId,
+  createApproval,
   createChange,
+  createDetailedLabelInfo,
+  createNonApplicableSubmitRequirementResultInfo,
+  createQuickLabelInfo,
   createSubmitRequirementExpressionInfo,
   createSubmitRequirementResultInfo,
-  createNonApplicableSubmitRequirementResultInfo,
-  createDetailedLabelInfo,
-  createAccountWithId,
-  createQuickLabelInfo,
-  createApproval,
 } from '../test/test-data-generators';
 import {
+  LabelNameToInfoMap,
+  LabelValueToDescriptionMap,
+  SubmitRequirementExpressionInfoStatus,
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
-  LabelNameToInfoMap,
-  SubmitRequirementExpressionInfoStatus,
 } from '../api/rest-api';
 import {assert} from '@open-wc/testing';
 
@@ -73,6 +82,21 @@
   '+2': 'perfect',
 };
 
+function createDetailedLabelInfoWithValues(
+  values: LabelValueToDescriptionMap = VALUES_2
+) {
+  return {
+    ...createDetailedLabelInfo(),
+    values,
+  };
+}
+
+function createApprovalWithValue(value: number, account?: AccountInfo) {
+  const approval = createApproval(account);
+  approval.value = value;
+  return approval;
+}
+
 suite('label-util', () => {
   test('getVotingRange -1 to +1', () => {
     const label = {values: VALUES_1};
@@ -480,6 +504,14 @@
       const labels = extractAssociatedLabels(submitRequirement);
       assert.deepEqual(labels, ['Verified']);
     });
+    test('1 label, extractFromSubmittability:false', () => {
+      const submitRequirement = createSubmitRequirementResultInfo();
+      const labels = extractAssociatedLabels(submitRequirement, {
+        extractFromSubmittability: false,
+        extractFromOverride: true,
+      });
+      assert.deepEqual(labels, []);
+    });
     test('label with number', () => {
       const submitRequirement = createSubmitRequirementResultInfo(
         'label2:verified=MAX'
@@ -504,6 +536,46 @@
       const labels = extractAssociatedLabels(submitRequirement);
       assert.deepEqual(labels, ['Verified', 'Build-cop-override']);
     });
+    test('overridden label, extractFromOverride:false', () => {
+      const submitRequirement = {
+        ...createSubmitRequirementResultInfo(),
+        override_expression_result: createSubmitRequirementExpressionInfo(
+          'label:Build-cop-override'
+        ),
+      };
+      const labels = extractAssociatedLabels(submitRequirement, {
+        extractFromSubmittability: true,
+        extractFromOverride: false,
+      });
+      assert.deepEqual(labels, ['Verified']);
+    });
+    test('overridden label, overridden, extractFromOverride:onlyIfOverridden', () => {
+      const submitRequirement = {
+        ...createSubmitRequirementResultInfo(),
+        override_expression_result: createSubmitRequirementExpressionInfo(
+          'label:Build-cop-override'
+        ),
+      };
+      const labels = extractAssociatedLabels(submitRequirement, {
+        extractFromSubmittability: true,
+        extractFromOverride: 'onlyIfOverridden',
+      });
+      assert.deepEqual(labels, ['Verified', 'Build-cop-override']);
+    });
+    test('overridden label, not overridden, extractFromOverride:onlyIfOverridden', () => {
+      const submitRequirement = {
+        ...createSubmitRequirementResultInfo(),
+        override_expression_result: {
+          ...createSubmitRequirementExpressionInfo('label:Build-cop-override'),
+          fulfilled: false,
+        },
+      };
+      const labels = extractAssociatedLabels(submitRequirement, {
+        extractFromSubmittability: true,
+        extractFromOverride: 'onlyIfOverridden',
+      });
+      assert.deepEqual(labels, ['Verified']);
+    });
   });
 
   suite('extractLabelsWithCountFrom', () => {
@@ -901,4 +973,150 @@
       assert.isTrue(hasVoted(quickLabelInfo, account));
     });
   });
+
+  suite('orderSubmitRequirements', () => {
+    test('orders priority requirements first', () => {
+      const codeReview = {
+        ...createSubmitRequirementResultInfo(),
+        name: StandardLabels.CODE_REVIEW,
+      };
+      const codeOwners = {
+        ...createSubmitRequirementResultInfo(),
+        name: StandardLabels.CODE_OWNERS,
+      };
+      const presubmitVerified = {
+        ...createSubmitRequirementResultInfo(),
+        name: StandardLabels.PRESUBMIT_VERIFIED,
+      };
+      const customLabel = createSubmitRequirementResultInfo('Custom-Label');
+
+      const requirements = [
+        customLabel,
+        codeReview,
+        presubmitVerified,
+        codeOwners,
+      ];
+      const ordered = orderSubmitRequirements(requirements);
+
+      assert.deepEqual(ordered, [
+        codeReview,
+        codeOwners,
+        presubmitVerified,
+        customLabel,
+      ]);
+    });
+
+    test('preserves order of non-priority requirements', () => {
+      const customLabel1 = {
+        ...createSubmitRequirementResultInfo(),
+        name: 'Custom-Label-1',
+      };
+      const customLabel2 = {
+        ...createSubmitRequirementResultInfo(),
+        name: 'Custom-Label-2',
+      };
+      const customLabel3 = {
+        ...createSubmitRequirementResultInfo(),
+        name: 'Custom-Label-3',
+      };
+
+      const requirements = [customLabel2, customLabel1, customLabel3];
+      const ordered = orderSubmitRequirements(requirements);
+
+      assert.deepEqual(ordered, requirements);
+    });
+  });
+
+  suite('getDefaultValue', () => {
+    test('returns default value when label exists and has default_value', () => {
+      const defaultValue = 1;
+      const labels = {
+        'Code-Review': {
+          ...createDetailedLabelInfo(),
+          default_value: defaultValue,
+        },
+      };
+      assert.equal(getDefaultValue(labels, 'Code-Review'), defaultValue);
+    });
+  });
+
+  suite('hasApprovedVote', () => {
+    test('returns true for quick label info with approved status', () => {
+      const quickLabelInfo = createQuickLabelInfo();
+      assert.isFalse(hasApprovedVote(quickLabelInfo));
+      quickLabelInfo.approved = createAccountWithId();
+      assert.isTrue(hasApprovedVote(quickLabelInfo));
+    });
+
+    test('returns true for detailed label info with approved vote', () => {
+      const detailedLabelInfo = createDetailedLabelInfoWithValues();
+      assert.isFalse(hasApprovedVote(detailedLabelInfo));
+      const approval = createApprovalWithValue(2);
+      detailedLabelInfo.all = [approval];
+      assert.isTrue(hasApprovedVote(detailedLabelInfo));
+    });
+  });
+
+  suite('hasRejectedVote', () => {
+    test('returns true for quick label info with rejected status', () => {
+      const quickLabelInfo = createQuickLabelInfo();
+      assert.isFalse(hasRejectedVote(quickLabelInfo));
+      quickLabelInfo.rejected = createAccountWithId();
+      assert.isTrue(hasRejectedVote(quickLabelInfo));
+    });
+
+    test('returns true for detailed label info with rejected vote', () => {
+      const detailedLabelInfo = createDetailedLabelInfoWithValues();
+      assert.isFalse(hasRejectedVote(detailedLabelInfo));
+      const approval = createApprovalWithValue(-2);
+      detailedLabelInfo.all = [approval];
+      assert.isTrue(hasRejectedVote(detailedLabelInfo));
+    });
+  });
+
+  suite('canReviewerVote', () => {
+    test('returns true when reviewer has permitted voting range with max > 0', () => {
+      const detailedLabelInfo = createDetailedLabelInfo();
+      const account = createAccountWithId();
+      const approval = createApproval(account);
+      assert.isFalse(canReviewerVote(detailedLabelInfo, account));
+      approval.permitted_voting_range = {min: -1, max: 1};
+      detailedLabelInfo.all = [approval];
+      assert.isTrue(canReviewerVote(detailedLabelInfo, account));
+    });
+  });
+
+  suite('getCodeReviewLabel', () => {
+    test('returns Code-Review label from label map', () => {
+      const codeReviewLabel = createDetailedLabelInfoWithValues(VALUES_2);
+      const labels = {
+        Verified: createDetailedLabelInfoWithValues(VALUES_1),
+        [StandardLabels.CODE_REVIEW]: codeReviewLabel,
+        'Custom-Label': createDetailedLabelInfoWithValues(VALUES_0),
+      };
+      assert.deepEqual(getCodeReviewLabel(labels), codeReviewLabel);
+    });
+  });
+
+  suite('orderSubmitRequirementNames', () => {
+    test('orders priority requirements first, then alphabetically', () => {
+      const names = [
+        'Custom-Label-2',
+        StandardLabels.PRESUBMIT_VERIFIED,
+        'Custom-Label-1',
+        StandardLabels.CODE_REVIEW,
+        'Custom-Label-3',
+        StandardLabels.CODE_OWNERS,
+      ];
+      const expected = [
+        StandardLabels.CODE_REVIEW,
+        StandardLabels.CODE_OWNERS,
+        StandardLabels.PRESUBMIT_VERIFIED,
+        'Custom-Label-1',
+        'Custom-Label-2',
+        'Custom-Label-3',
+      ];
+      assert.deepEqual(orderSubmitRequirementNames(names), expected);
+    });
+  });
 });
diff --git a/polygerrit-ui/app/utils/location-util.ts b/polygerrit-ui/app/utils/location-util.ts
index 98c9ff9..4f3ec9d 100644
--- a/polygerrit-ui/app/utils/location-util.ts
+++ b/polygerrit-ui/app/utils/location-util.ts
@@ -7,7 +7,7 @@
 // This file adds some simple checks to match internal Google rules.
 // Internally at Google it has different a implementation.
 
-import {setLocationHref, locationReplace, locationAssign} from 'safevalues/dom';
+import {locationAssign, locationReplace, setLocationHref} from 'safevalues/dom';
 
 export function setHref(loc: Location, url: string) {
   setLocationHref(loc, url);
diff --git a/polygerrit-ui/app/utils/message-util.ts b/polygerrit-ui/app/utils/message-util.ts
index fffe612..50c9640 100644
--- a/polygerrit-ui/app/utils/message-util.ts
+++ b/polygerrit-ui/app/utils/message-util.ts
@@ -4,7 +4,27 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {MessageTag} from '../constants/constants';
-import {ChangeId, ChangeMessageInfo} from '../types/common';
+import {
+  AccountInfo,
+  ChangeId,
+  ChangeInfo,
+  ChangeMessage,
+  ChangeMessageInfo,
+  PatchSetNum,
+} from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
+import {LabelExtreme, PATCH_SET_PREFIX_PATTERN} from './comment-util';
+import {hasOwnProperty} from './common-util';
+import {getVotingRange, StandardLabels} from './label-util';
+
+export const VOTE_RESET_TEXT = '0 (vote reset)';
+export const LABEL_TITLE_SCORE_PATTERN =
+  /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.:]?$/;
+
+export interface Score {
+  label?: string;
+  value?: string;
+}
 
 function getRevertChangeIdFromMessage(msg: ChangeMessageInfo): ChangeId {
   const REVERT_REGEX =
@@ -19,3 +39,76 @@
     .filter(m => m.tag === MessageTag.TAG_REVERT)
     .map(m => getRevertChangeIdFromMessage(m));
 }
+
+export function getScores(
+  message?: ChangeMessage,
+  labelExtremes?: LabelExtreme
+): Score[] {
+  if (!message || !message.message || !labelExtremes) {
+    return [];
+  }
+  const line = message.message.split('\n', 1)[0];
+  const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+  if (!line.match(patchSetPrefix)) {
+    return [];
+  }
+  const scoresRaw = line.split(patchSetPrefix)[1];
+  if (!scoresRaw) {
+    return [];
+  }
+  return scoresRaw
+    .split(' ')
+    .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+    .filter(ms => ms && ms.length === 4 && hasOwnProperty(labelExtremes, ms[2]))
+    .map(ms => {
+      const label = ms?.[2];
+      const value = ms?.[1] === '-' ? VOTE_RESET_TEXT : ms?.[3];
+      return {label, value};
+    });
+}
+
+/**
+ * Extracts Code-Review votes from change messages, specifically those posted
+ * by the provided `account`.
+ * @param change The change info.
+ * @param account The account for which to extract the votes.
+ * @return A map where keys are patch set numbers and values are objects
+ *   containing the label ('Code-Review') and the numeric vote value.
+ */
+export function getCodeReviewVotesFromMessage(
+  change?: ChangeInfo | ParsedChangeInfo,
+  account?: AccountInfo
+): Map<PatchSetNum, Score> {
+  const codeReviewVotes = new Map<PatchSetNum, Score>();
+  if (!change?.messages || !change?.labels || !account) {
+    return codeReviewVotes;
+  }
+
+  const labelExtremes: LabelExtreme = {};
+  for (const labelName of Object.keys(change.labels)) {
+    const labelInfo = change.labels[labelName];
+    const range = getVotingRange(labelInfo);
+    if (range) {
+      labelExtremes[labelName] = range;
+    }
+  }
+
+  for (const message of change.messages) {
+    if (message.author?._account_id !== account._account_id) {
+      continue;
+    }
+    if (!message._revision_number) continue;
+
+    const scores = getScores(message as ChangeMessage, labelExtremes);
+    for (const score of scores) {
+      if (score.label === StandardLabels.CODE_REVIEW && score.value) {
+        const value = score.value === VOTE_RESET_TEXT ? '0' : score.value;
+        codeReviewVotes.set(message._revision_number, {
+          label: score.label,
+          value,
+        });
+      }
+    }
+  }
+  return codeReviewVotes;
+}
diff --git a/polygerrit-ui/app/utils/message-util_test.ts b/polygerrit-ui/app/utils/message-util_test.ts
index 64d765a..bed9376 100644
--- a/polygerrit-ui/app/utils/message-util_test.ts
+++ b/polygerrit-ui/app/utils/message-util_test.ts
@@ -3,50 +3,191 @@
  * Copyright 2023 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {getRevertCreatedChangeIds} from './message-util';
+import {
+  getCodeReviewVotesFromMessage,
+  getRevertCreatedChangeIds,
+} from './message-util';
 import {assert} from '@open-wc/testing';
 import {MessageTag} from '../constants/constants';
-import {ChangeId, ReviewInputTag} from '../api/rest-api';
-import {createChangeMessage} from '../test/test-data-generators';
+import {
+  AccountInfo,
+  ChangeId,
+  ChangeInfo,
+  ChangeMessageInfo,
+  LabelNameToInfoMap,
+  PatchSetNum,
+  ReviewInputTag,
+} from '../api/rest-api';
+import {
+  createAccountWithId,
+  createChange,
+  createChangeMessage,
+  createDetailedLabelInfo,
+} from '../test/test-data-generators';
 
 suite('message-util tests', () => {
-  test('getRevertCreatedChangeIds', () => {
-    const messages = [
-      {
-        ...createChangeMessage(),
-        message:
-          'Created a revert of this change as If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
-        tag: MessageTag.TAG_REVERT as ReviewInputTag,
-      },
-      {
-        ...createChangeMessage(),
-        message: 'Created a revert of this change as abc',
-        tag: undefined,
-      },
-    ];
+  suite('getRevertCreatedChangeIds', () => {
+    test('getRevertCreatedChangeIds', () => {
+      const messages = [
+        {
+          ...createChangeMessage(),
+          message:
+            'Created a revert of this change as If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
+          tag: MessageTag.TAG_REVERT as ReviewInputTag,
+        },
+        {
+          ...createChangeMessage(),
+          message: 'Created a revert of this change as abc',
+          tag: undefined,
+        },
+      ];
 
-    assert.deepEqual(getRevertCreatedChangeIds(messages), [
-      'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
-    ]);
+      assert.deepEqual(getRevertCreatedChangeIds(messages), [
+        'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
+      ]);
+    });
+
+    test('getRevertCreatedChangeIds with extra spam', () => {
+      const messages = [
+        {
+          ...createChangeMessage(),
+          message:
+            'Created a revert of this change as IIf02ca1cd494579d6bb92a157bf1819e3689cd6b1',
+          tag: MessageTag.TAG_REVERT as ReviewInputTag,
+        },
+        {
+          ...createChangeMessage(),
+          message: 'Created a revert of this change as abc',
+          tag: undefined,
+        },
+      ];
+
+      assert.deepEqual(getRevertCreatedChangeIds(messages), [
+        'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
+      ]);
+    });
   });
 
-  test('getRevertCreatedChangeIds with extra spam', () => {
-    const messages = [
-      {
-        ...createChangeMessage(),
-        message:
-          'Created a revert of this change as IIf02ca1cd494579d6bb92a157bf1819e3689cd6b1',
-        tag: MessageTag.TAG_REVERT as ReviewInputTag,
-      },
-      {
-        ...createChangeMessage(),
-        message: 'Created a revert of this change as abc',
-        tag: undefined,
-      },
-    ];
+  suite('getCodeReviewVotesFromMessage', () => {
+    const account1: AccountInfo = {
+      ...createAccountWithId(1),
+    };
+    const account2: AccountInfo = {
+      ...createAccountWithId(2),
+    };
 
-    assert.deepEqual(getRevertCreatedChangeIds(messages), [
-      'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
-    ]);
+    const labels: LabelNameToInfoMap = {
+      'Code-Review': createDetailedLabelInfo(),
+    };
+
+    function createMessage(
+      author: AccountInfo,
+      message: string,
+      ps: number
+    ): ChangeMessageInfo {
+      return {
+        ...createChangeMessage(),
+        author,
+        message,
+        _revision_number: ps as PatchSetNum,
+      };
+    }
+
+    test('no messages', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.equal(actual.size, 0);
+    });
+
+    test('no messages from account', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [createMessage(account2, 'Patch Set 1: Code-Review+1', 1)],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.equal(actual.size, 0);
+    });
+
+    test('one message with code review vote', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [createMessage(account1, 'Patch Set 1: Code-Review+1', 1)],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.deepEqual(
+        actual,
+        new Map([[1 as PatchSetNum, {label: 'Code-Review', value: '+1'}]])
+      );
+    });
+
+    test('vote reset', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [createMessage(account1, 'Patch Set 1: Code-Review-1', 1)],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.deepEqual(
+        actual,
+        new Map([[1 as PatchSetNum, {label: 'Code-Review', value: '-1'}]])
+      );
+    });
+
+    test('latest message wins for same patchset', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [
+          createMessage(account1, 'Patch Set 1: Code-Review-1', 1),
+          createMessage(account1, 'Patch Set 1: Code-Review+1', 1),
+        ],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.deepEqual(
+        actual,
+        new Map([[1 as PatchSetNum, {label: 'Code-Review', value: '+1'}]])
+      );
+    });
+
+    test('messages from different users', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [
+          createMessage(account1, 'Patch Set 1: Code-Review+1', 1),
+          createMessage(account2, 'Patch Set 1: Code-Review-1', 1),
+        ],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.deepEqual(
+        actual,
+        new Map([[1 as PatchSetNum, {label: 'Code-Review', value: '+1'}]])
+      );
+    });
+
+    test('messages for different patchsets', () => {
+      const change: ChangeInfo = {
+        ...createChange(),
+        messages: [
+          createMessage(account1, 'Patch Set 1: Code-Review-1', 1),
+          createMessage(account1, 'Patch Set 2: Code-Review+1', 2),
+        ],
+        labels,
+      };
+      const actual = getCodeReviewVotesFromMessage(change, account1);
+      assert.deepEqual(
+        actual,
+        new Map([
+          [1 as PatchSetNum, {label: 'Code-Review', value: '-1'}],
+          [2 as PatchSetNum, {label: 'Code-Review', value: '+1'}],
+        ])
+      );
+    });
   });
 });
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index dc7adc1..f31ff9f 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -1,14 +1,14 @@
 import {
-  RevisionInfo,
+  BasePatchSetNum,
+  BranchName,
   ChangeInfo,
-  PatchSetNum,
+  CommitId,
   EDIT,
   PARENT,
+  PatchSetNum,
   PatchSetNumber,
-  BasePatchSetNum,
+  RevisionInfo,
   RevisionPatchSetNum,
-  BranchName,
-  CommitId,
 } from '../types/common';
 import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
 import {assert} from './common-util';
diff --git a/polygerrit-ui/app/utils/patch-set-util_test.ts b/polygerrit-ui/app/utils/patch-set-util_test.ts
index 31803a4..8060db8 100644
--- a/polygerrit-ui/app/utils/patch-set-util_test.ts
+++ b/polygerrit-ui/app/utils/patch-set-util_test.ts
@@ -13,13 +13,13 @@
 import {
   BasePatchSetNum,
   ChangeInfo,
+  CommitId,
   EDIT,
+  PARENT,
   PatchSetNum,
   PatchSetNumber,
   ReviewInputTag,
-  PARENT,
   RevisionInfo,
-  CommitId,
 } from '../types/common';
 import {
   _testOnly_computeWipForPatchSets,
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index b007d47..5c523b3 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -3,7 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {SpecialFilePath, FileInfoStatus} from '../constants/constants';
+import {FileInfoStatus, SpecialFilePath} from '../constants/constants';
 import {FileInfo, FileNameToFileInfoMap} from '../types/common';
 import {hasOwnProperty} from './common-util';
 
diff --git a/polygerrit-ui/app/utils/safari-selection-util.ts b/polygerrit-ui/app/utils/safari-selection-util.ts
index a24ac3e..6ede081 100644
--- a/polygerrit-ui/app/utils/safari-selection-util.ts
+++ b/polygerrit-ui/app/utils/safari-selection-util.ts
@@ -3,7 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {isSafari, findActiveElement} from './dom-util';
+import {findActiveElement, isSafari} from './dom-util';
 
 const SUPPORTS_SHADOW_SELECTION =
   typeof window.ShadowRoot.prototype.getSelection === 'function';
diff --git a/polygerrit-ui/app/utils/safe-types-util_test.ts b/polygerrit-ui/app/utils/safe-types-util_test.ts
index dbeaa4f..2c8bb70 100644
--- a/polygerrit-ui/app/utils/safe-types-util_test.ts
+++ b/polygerrit-ui/app/utils/safe-types-util_test.ts
@@ -5,7 +5,7 @@
  */
 import {assert} from '@open-wc/testing';
 import '../test/common-test-setup';
-import {safeTypesBridge, _testOnly_SafeUrl} from './safe-types-util';
+import {_testOnly_SafeUrl, safeTypesBridge} from './safe-types-util';
 
 suite('safe-types-util tests', () => {
   test('SafeUrl accepts valid urls', () => {
diff --git a/polygerrit-ui/app/utils/string-util_test.ts b/polygerrit-ui/app/utils/string-util_test.ts
index 6ba3354..c71de91 100644
--- a/polygerrit-ui/app/utils/string-util_test.ts
+++ b/polygerrit-ui/app/utils/string-util_test.ts
@@ -6,11 +6,16 @@
 import {assert} from '@open-wc/testing';
 import '../test/common-test-setup';
 import {
-  pluralize,
-  ordinal,
-  listForSentence,
+  capitalizeFirstLetter,
+  charsOnly,
   diffFilePaths,
   escapeAndWrapSearchOperatorValue,
+  isCharacterLetter,
+  isUpperCase,
+  levenshteinDistance,
+  listForSentence,
+  ordinal,
+  pluralize,
   trimWithEllipsis,
 } from './string-util';
 
@@ -101,4 +106,47 @@
       '"\\"value of \\\\: \\\\\\"something\\""'
     );
   });
+
+  test('charsOnly', () => {
+    assert.equal(charsOnly('Hello123'), 'Hello');
+    assert.equal(charsOnly('123Hello'), 'Hello');
+    assert.equal(charsOnly('Hello World!'), 'HelloWorld');
+    assert.equal(charsOnly('!@#$%^&*()'), '');
+    assert.equal(charsOnly(''), '');
+  });
+
+  test('isCharacterLetter', () => {
+    assert.isTrue(isCharacterLetter('a'));
+    assert.isTrue(isCharacterLetter('Z'));
+    assert.isFalse(isCharacterLetter('1'));
+    assert.isFalse(isCharacterLetter('!'));
+    assert.isFalse(isCharacterLetter(''));
+    assert.isFalse(isCharacterLetter('ab'));
+  });
+
+  test('isUpperCase', () => {
+    assert.isTrue(isUpperCase('A'));
+    assert.isTrue(isUpperCase('Z'));
+    assert.isFalse(isUpperCase('a'));
+    assert.isFalse(isUpperCase('z'));
+    assert.isTrue(isUpperCase('AB'));
+    assert.isTrue(isUpperCase('1'));
+  });
+
+  test('capitalizeFirstLetter', () => {
+    assert.equal(capitalizeFirstLetter('hello'), 'Hello');
+    assert.equal(capitalizeFirstLetter('Hello'), 'Hello');
+    assert.equal(capitalizeFirstLetter('HELLO'), 'HELLO');
+    assert.equal(capitalizeFirstLetter(''), '');
+    assert.equal(capitalizeFirstLetter('123hello'), '123hello');
+  });
+
+  test('levenshteinDistance', () => {
+    // kitten -> sitten -> sittin -> sitting
+    assert.equal(levenshteinDistance('kitten', 'sitting'), 3);
+    assert.equal(levenshteinDistance('', ''), 0);
+    assert.equal(levenshteinDistance('', 'abc'), 3);
+    assert.equal(levenshteinDistance('abc', ''), 3);
+    assert.equal(levenshteinDistance('same', 'same'), 0);
+  });
 });
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index 265becf..366fdfc 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -8,15 +8,15 @@
 import {
   encodeURL,
   getBaseUrl,
+  getDocUrl,
   getPatchRangeExpression,
   loginUrl,
   PatchRangeParams,
+  sameOrigin,
   singleDecodeURL,
   toPath,
   toPathname,
   toSearchParams,
-  sameOrigin,
-  getDocUrl,
 } from './url-util';
 import {assert} from '@open-wc/testing';
 import {createAuth} from '../test/test-data-generators';
diff --git a/polygerrit-ui/app/utils/weblink-util_test.ts b/polygerrit-ui/app/utils/weblink-util_test.ts
index a8c3668..3d5a7e9 100644
--- a/polygerrit-ui/app/utils/weblink-util_test.ts
+++ b/polygerrit-ui/app/utils/weblink-util_test.ts
@@ -5,11 +5,11 @@
  */
 import {assert} from '@open-wc/testing';
 import '../test/common-test-setup';
-import {createServerInfo, createGerritInfo} from '../test/test-data-generators';
+import {createGerritInfo, createServerInfo} from '../test/test-data-generators';
 import {
-  getCodeBrowserWeblink,
   computeMainCodeBrowserWeblink,
   getChangeWeblinks,
+  getCodeBrowserWeblink,
 } from './weblink-util';
 
 suite('weblink util tests', () => {
diff --git a/polygerrit-ui/app/workers/service-worker-class.ts b/polygerrit-ui/app/workers/service-worker-class.ts
index ff9efb7..67f5b31 100644
--- a/polygerrit-ui/app/workers/service-worker-class.ts
+++ b/polygerrit-ui/app/workers/service-worker-class.ts
@@ -16,7 +16,7 @@
   getServiceWorkerState,
   putServiceWorkerState,
 } from './service-worker-indexdb';
-import {DashboardType, createDashboardUrl} from '../models/views/dashboard';
+import {createDashboardUrl, DashboardType} from '../models/views/dashboard';
 import {createChangeUrl} from '../models/views/change';
 import {noAwait} from '../utils/async-util';
 
diff --git a/polygerrit-ui/app/workers/syntax-worker.ts b/polygerrit-ui/app/workers/syntax-worker.ts
index 1ddb9c6..e3f40f3 100644
--- a/polygerrit-ui/app/workers/syntax-worker.ts
+++ b/polygerrit-ui/app/workers/syntax-worker.ts
@@ -5,10 +5,10 @@
  */
 import {HighlightJS} from '../types/types';
 import {
+  isInit,
+  isRequest,
   SyntaxWorkerMessage,
   SyntaxWorkerResult,
-  isRequest,
-  isInit,
 } from '../types/syntax-worker-api';
 import {highlightedStringToRanges} from '../utils/syntax-util';
 import {importScript} from '../utils/worker-util';
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 088d613..96881d0 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,17 +2,17 @@
 # yarn lockfile v1
 
 
-"@lit-labs/ssr-dom-shim@^1.2.0":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz#a28799c463177d1a0b0e5cefdc173da5ac859eb4"
-  integrity sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==
+"@lit-labs/ssr-dom-shim@^1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz#55eb80ab5ef6e188f7e541c1e2bea1ef582413b8"
+  integrity sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==
 
-"@lit/reactive-element@^2.0.4":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b"
-  integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==
+"@lit/reactive-element@^2.1.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.1.1.tgz#0662ac4a43d4898974aef9a6c5cd47b9e331919a"
+  integrity sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==
   dependencies:
-    "@lit-labs/ssr-dom-shim" "^1.2.0"
+    "@lit-labs/ssr-dom-shim" "^1.4.0"
 
 "@mapbox/node-pre-gyp@^1.0.0":
   version "1.0.11"
@@ -29,6 +29,14 @@
     semver "^7.3.5"
     tar "^6.1.11"
 
+"@material/web@^2.4.0":
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/@material/web/-/web-2.4.0.tgz#4158df5afa2a36f55db5a5b2d77146cf6bdc84a6"
+  integrity sha512-2jYiPIYOuP2UcWXal4VdKRlkpBX02U3rgXMWp0yFQbJaOmK8MOIA3BsyuooM3VOYI+VCO70BeH1x43y5cBLvZQ==
+  dependencies:
+    lit "^2.8.0 || ^3.0.0"
+    tslib "^2.4.0"
+
 "@polymer/decorators@^3.0.0":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@polymer/decorators/-/decorators-3.0.0.tgz#e4212ac976d9abd1210f560b6e1be4165c1c0183"
@@ -46,13 +54,6 @@
   resolved "https://registry.yarnpkg.com/@polymer/font-roboto/-/font-roboto-3.0.2.tgz#80cdaa7225db2359130dfb2c6d9a3be1820020c3"
   integrity sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==
 
-"@polymer/iron-a11y-announcer@^3.0.0-pre.26", "@polymer/iron-a11y-announcer@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.2.0.tgz#d04b1301413d473336cc5797dfd97b3b36dd0cd7"
-  integrity sha512-We+hyaFHcg7Ke8ovsoxUpYEXFIJLHxMCDaLehTB4dELS+C+K0zMnGSiqQvb/YzGS+nSYpAfkQIyg1msOCdHMtA==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/iron-a11y-keys-behavior@^3.0.0-pre.26":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz#2868ea72912d2007ffab4734684a91f5aac49b84"
@@ -60,16 +61,6 @@
   dependencies:
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-autogrow-textarea@^3.0.0-pre.26", "@polymer/iron-autogrow-textarea@^3.0.3":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz#b75dbebc23ce47d428a26156709d4a8a4c05823e"
-  integrity sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==
-  dependencies:
-    "@polymer/iron-behaviors" "^3.0.0-pre.26"
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/iron-validatable-behavior" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/iron-behaviors@^3.0.0-pre.26":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz#a3b6f876779a7f0a91a15e4423890968b6525901"
@@ -87,23 +78,6 @@
     "@polymer/iron-validatable-behavior" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-dropdown@^3.0.0-pre.26", "@polymer/iron-dropdown@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-dropdown/-/iron-dropdown-3.0.1.tgz#c573faa1a08c179d201ae877c1c726018314bff3"
-  integrity sha512-22yLhepfcKjuQMfFmRHi/9MPKTqkzgRrmWWW0P5uqK++xle53k2QBO5VYUAYiCN3ZcxIi9lEhZ9YWGeQj2JBig==
-  dependencies:
-    "@polymer/iron-behaviors" "^3.0.0-pre.26"
-    "@polymer/iron-overlay-behavior" "^3.0.0-pre.27"
-    "@polymer/neon-animation" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-fit-behavior@^3.0.0-pre.26", "@polymer/iron-fit-behavior@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.1.0.tgz#8197a61e0ce2234e9bf8fbd7a1ae53d1a06026b0"
-  integrity sha512-ABcgIYqrjhmUT8tiuolqeGttF/8pd3sEymUDrO1vXbZu4FWIvoLNndrMDFvs++AGd12Mjf5pYy84NJc6dB8Vig==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/iron-flex-layout@^3.0.0-pre.26":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz#36f9e1a8eb792d279b2bc75d362628721ad37f0c"
@@ -118,49 +92,6 @@
   dependencies:
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-icon@^3.0.0-pre.26", "@polymer/iron-icon@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-icon/-/iron-icon-3.0.1.tgz#93211c39d8825fe4965a68419566036c1df291eb"
-  integrity sha512-QLPwirk+UPZNaLnMew9VludXA4CWUCenRewgEcGYwdzVgDPCDbXxy6vRJjmweZobMQv/oVLppT2JZtJFnPxX6g==
-  dependencies:
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/iron-meta" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-iconset-svg@^3.0.0-pre.26", "@polymer/iron-iconset-svg@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz#568d6e7dbc120299dae63be3600aeba0d30ddbea"
-  integrity sha512-XNwURbNHRw6u2fJe05O5fMYye6GSgDlDqCO+q6K1zAnKIrpgZwf2vTkBd5uCcZwsN0FyCB3mvNZx4jkh85dRDw==
-  dependencies:
-    "@polymer/iron-meta" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-image@^3.0.0-pre.26":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-image/-/iron-image-3.0.2.tgz#425ee6269634e024dbea726a91a61724ae4402b6"
-  integrity sha512-VyYtnewGozDb5sUeoLR1OvKzlt5WAL6b8Od7fPpio5oYL+9t061/nTV8+ZMrpMgF2WgB0zqM/3K53o3pbK5v8Q==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-input@^3.0.0-pre.26", "@polymer/iron-input@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-input/-/iron-input-3.0.1.tgz#dc866a25107f9b38d9ca4512dd9a3e51b78b4915"
-  integrity sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==
-  dependencies:
-    "@polymer/iron-a11y-announcer" "^3.0.0-pre.26"
-    "@polymer/iron-validatable-behavior" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-menu-behavior@^3.0.0-pre.26":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-menu-behavior/-/iron-menu-behavior-3.0.2.tgz#f8fa2d59af472a4cb4fb0359c704b808bc2c105d"
-  integrity sha512-8dpASkFNBIkxAJWsFLWIO1M7tKM0+wKs3PqdeF/dDdBciwoaaFgC2K1XCZFZnbe2t9/nJgemXxVugGZAWpYCGg==
-  dependencies:
-    "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/iron-selector" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/iron-meta@^3.0.0-pre.26":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/iron-meta/-/iron-meta-3.0.1.tgz#7f140628d127b0a284f882f1bb323a261bc125f5"
@@ -168,30 +99,6 @@
   dependencies:
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/iron-overlay-behavior@^3.0.0-pre.27":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz#29c198e19e05bb2bcf7d86d3c11848cb93301d00"
-  integrity sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==
-  dependencies:
-    "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-fit-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-resizable-behavior" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-resizable-behavior@^3.0.0-pre.26":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz#e284348ed7c1c7e263f7039297532fa954025ea2"
-  integrity sha512-FyHxRxFspVoRaeZSWpT3y0C9awomb4tXXolIJcZ7RvXhMP632V5lez+ch5G5SwK0LpnAPkg35eB0LPMFv+YMMQ==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/iron-selector@^3.0.0-pre.26", "@polymer/iron-selector@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/iron-selector/-/iron-selector-3.0.1.tgz#e845bec58489c96b4e7609525532437869ad5a88"
-  integrity sha512-sBVk2uas6prW0glUe2xEJJYlvxmYzM40Au9OKbfDK2Qekou/fLKcBRyIYI39kuI8zWRaip8f3CI8qXcUHnKb1A==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/iron-validatable-behavior@^3.0.0-pre.26":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz#73538f005a07741c31b6fc1e981168c3d3e0d92b"
@@ -200,23 +107,6 @@
     "@polymer/iron-meta" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/marked-element@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/marked-element/-/marked-element-3.0.1.tgz#56add62080404dea142c055977807ae9ca773a89"
-  integrity sha512-WJQzQetxdStVGQbyTBUBgd+hSI0Rl39uJg7b2zL3r6EfMnibzmA/YNT06M8jVZdxPF+B4SumrFWRtasVtGQRUQ==
-  dependencies:
-    "@polymer/polymer" "^3.0.0"
-    marked "~0.3.9"
-
-"@polymer/neon-animation@^3.0.0-pre.26":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/neon-animation/-/neon-animation-3.0.1.tgz#6658e4b524abc057477772a7473292493d366c24"
-  integrity sha512-cDDc0llpVCe0ATbDS3clDthI54Bc8YwZIeTGGmBJleKOvbRTUC5+ssJmRL+VwVh+VM5FlnQlx760ppftY3uprg==
-  dependencies:
-    "@polymer/iron-resizable-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-selector" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/paper-behaviors@^3.0.0-pre.27":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/paper-behaviors/-/paper-behaviors-3.0.1.tgz#83f1cd06489f484c1b108a2967fb01952df722ad"
@@ -237,108 +127,6 @@
     "@polymer/paper-styles" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/paper-card@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-card/-/paper-card-3.0.1.tgz#fb5960b3e55fab56d20b7c1c3dee08f0d052ff2a"
-  integrity sha512-ZYzfA4kzP9niRO22wSOBL2RS+URZNUP5XmUCwN91fYPIGO0Qbimh7d1O2HpJD4cRCZhvGYn2CJMDMVmDm35vIg==
-  dependencies:
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/iron-image" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-checkbox@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-checkbox/-/paper-checkbox-3.1.0.tgz#66b903ae5814db237d027deb4a3f3430f48d905b"
-  integrity sha512-kXm6yDG1tT8if0XuJ2cc9NF+g8Ev4wG+rnf0a+Sx+O7J6fn1jcnBlYn72FlrfjVjDQZDBFmT6nynhD5PvFw8iQ==
-  dependencies:
-    "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-checked-element-behavior" "^3.0.0-pre.26"
-    "@polymer/paper-behaviors" "^3.0.0-pre.27"
-    "@polymer/paper-ripple" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-dialog-behavior@^3.0.0-pre.26", "@polymer/paper-dialog-behavior@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-dialog-behavior/-/paper-dialog-behavior-3.0.1.tgz#819b2fbb9444c1c318ddf55f02819bb29a85657b"
-  integrity sha512-wbI4kCK8le/9MHT+IXzvHjoatxf3kd3Yn0tgozAiAwfSZ7N4Ubpi5MHrK0m9S9PeIxKokAgBYdTUrezSE5378A==
-  dependencies:
-    "@polymer/iron-overlay-behavior" "^3.0.0-pre.27"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-dialog-scrollable@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-dialog-scrollable/-/paper-dialog-scrollable-3.0.1.tgz#42fd30380320e6dd6d4d68b2ac4e45ee9e5e024f"
-  integrity sha512-1E8B9kNdL58jUrJ/BwqJeOoNVcxNrB559z//d1V0rVHWT5bWCCZegwS3G06iFK5MjxWFbIKzleVTLrT0opiZkA==
-  dependencies:
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/paper-dialog-behavior" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-dialog@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-dialog/-/paper-dialog-3.0.1.tgz#728ebdbfc4d35ec1485e543434cef5dba476f15e"
-  integrity sha512-KvglYbEq7AWJvui2j6WKLnOvgVMeGjovAydGrPRj7kVzCiD49Eq/hpYFJTRV5iDcalWH+mORUpw+jrFnG9+Kgw==
-  dependencies:
-    "@polymer/iron-overlay-behavior" "^3.0.0-pre.27"
-    "@polymer/neon-animation" "^3.0.0-pre.26"
-    "@polymer/paper-dialog-behavior" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-dropdown-menu@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-dropdown-menu/-/paper-dropdown-menu-3.2.0.tgz#17f507ef864dc18365a92e256bde628c2d352956"
-  integrity sha512-2ohwSHF+RLSK6kA0UkkMiMQF6EZcaEYWAA25kfisI6DWie7yozKrpQNsqvwfOEHU6DdDMIotrOtH1TM88YS8Zg==
-  dependencies:
-    "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-form-element-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-icon" "^3.0.0-pre.26"
-    "@polymer/iron-iconset-svg" "^3.0.0-pre.26"
-    "@polymer/iron-validatable-behavior" "^3.0.0-pre.26"
-    "@polymer/paper-behaviors" "^3.0.0-pre.27"
-    "@polymer/paper-input" "^3.1.0"
-    "@polymer/paper-menu-button" "^3.1.0"
-    "@polymer/paper-ripple" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.3.1"
-
-"@polymer/paper-fab@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-fab/-/paper-fab-3.0.1.tgz#2636359e7fb70dd5a549ed92ba9b3bdb9ff86bf8"
-  integrity sha512-LO8ckgd72MnAtC1WiPd5CFR27WC/dEuY/lOIQuHYdEjwI62+iiV7Bmr7uoQ9wvvV71qMFdMIOyq/03KklsuAzw==
-  dependencies:
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/iron-icon" "^3.0.0-pre.26"
-    "@polymer/paper-behaviors" "^3.0.0-pre.27"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-icon-button@^3.0.0-pre.26":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-icon-button/-/paper-icon-button-3.0.2.tgz#a1254faadc2c8dd135ce1ae33bcc161a94c31f65"
-  integrity sha512-kOdxQgnKL097bggFF6PWvsBYuWg+MCcoHoTHX6bh/MuZoWFZNjrFntFqwuB4oEbpjCpfm4moA33muPJFj7CihQ==
-  dependencies:
-    "@polymer/iron-icon" "^3.0.0-pre.26"
-    "@polymer/paper-behaviors" "^3.0.0-pre.27"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-input@^3.1.0", "@polymer/paper-input@^3.2.1":
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-input/-/paper-input-3.2.1.tgz#0fd0d30de3b43ba7d2c8d5d76f870d257b667ebf"
-  integrity sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==
-  dependencies:
-    "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-autogrow-textarea" "^3.0.0-pre.26"
-    "@polymer/iron-behaviors" "^3.0.0-pre.26"
-    "@polymer/iron-form-element-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-input" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/paper-item@^3.0.1":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polymer/paper-item/-/paper-item-3.0.1.tgz#05b3543483e556cd5532431cd1751a84343989b5"
@@ -349,29 +137,6 @@
     "@polymer/paper-styles" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/paper-listbox@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-listbox/-/paper-listbox-3.0.1.tgz#fe05094781b359e4afbc5bec89a07758a303a957"
-  integrity sha512-vMLWFpYcggAPmEDBmK+96fFefacOG3GLB1EguTn8+ZkqI+328hNfw1MzHjH68rgCIIUtjmm+9qgB1Sy/MN0a/A==
-  dependencies:
-    "@polymer/iron-behaviors" "^3.0.0-pre.26"
-    "@polymer/iron-menu-behavior" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-menu-button@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-menu-button/-/paper-menu-button-3.1.0.tgz#3eb2fb88ba3cdaa4c39be6ae3530efa40b1eccf3"
-  integrity sha512-q0G0/rvYD/FFmIBMGCQWjfXzRqwFw9+WHSYV4uOQzM1Ln8LMXSAd+2CENsbVwtMh6fmBePj15ZlU8SM2dt1WDQ==
-  dependencies:
-    "@polymer/iron-a11y-keys-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-behaviors" "^3.0.0-pre.26"
-    "@polymer/iron-dropdown" "^3.0.0-pre.26"
-    "@polymer/iron-fit-behavior" "^3.1.0"
-    "@polymer/neon-animation" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
 "@polymer/paper-ripple@^3.0.0-pre.26":
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/@polymer/paper-ripple/-/paper-ripple-3.0.2.tgz#52566f5ee367942022ceaa991368105d21403de5"
@@ -389,44 +154,10 @@
     "@polymer/iron-flex-layout" "^3.0.0-pre.26"
     "@polymer/polymer" "^3.0.0"
 
-"@polymer/paper-tabs@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-tabs/-/paper-tabs-3.1.0.tgz#a173839d20703fdd5fca97a9d878f7b0e6257150"
-  integrity sha512-t8G+3CiyI0R+wA077UNQXR/oG9GlsqRRO1KMsFHHjBSsYqWXghNsqxUG827wEj+PafI5u9tZ3vVt1S++Lg4B2g==
-  dependencies:
-    "@polymer/iron-behaviors" "^3.0.0-pre.26"
-    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
-    "@polymer/iron-icon" "^3.0.0-pre.26"
-    "@polymer/iron-iconset-svg" "^3.0.0-pre.26"
-    "@polymer/iron-menu-behavior" "^3.0.0-pre.26"
-    "@polymer/iron-resizable-behavior" "^3.0.0-pre.26"
-    "@polymer/paper-behaviors" "^3.0.0-pre.27"
-    "@polymer/paper-icon-button" "^3.0.0-pre.26"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-toggle-button@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-toggle-button/-/paper-toggle-button-3.0.1.tgz#7d855420f0df96e7f812a9f1bdcfbc5ab082e819"
-  integrity sha512-jadZB60fycT7YnSAH0H23LYo6/2HYmMZTtNr9LpdSIRFPLX6mqqxewex92cFz019bMKaRJgORn308hRlJo2u6A==
-  dependencies:
-    "@polymer/iron-checked-element-behavior" "^3.0.0-pre.26"
-    "@polymer/paper-behaviors" "^3.0.0-pre.27"
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/paper-tooltip@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@polymer/paper-tooltip/-/paper-tooltip-3.0.1.tgz#cdbb06442737513f081437c6302842170ce714dc"
-  integrity sha512-yiUk09opTEnE1lK+tb501ENb+yQBi4p++Ep0eGJAHesVYKVMPNgPphVKkIizkDaU+n0SE+zXfTsRbYyOMDYXSg==
-  dependencies:
-    "@polymer/paper-styles" "^3.0.0-pre.26"
-    "@polymer/polymer" "^3.0.0"
-
-"@polymer/polymer@3.5.1", "@polymer/polymer@^3.0.0", "@polymer/polymer@^3.0.2", "@polymer/polymer@^3.0.5", "@polymer/polymer@^3.3.1":
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.5.1.tgz#4b5234e43b8876441022bcb91313ab3c4a29f0c8"
-  integrity sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==
+"@polymer/polymer@3.5.2", "@polymer/polymer@^3.0.0", "@polymer/polymer@^3.0.2", "@polymer/polymer@^3.0.5":
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.5.2.tgz#af0e7e13976df53ace6728e841121c36aca351de"
+  integrity sha512-fWwImY/UH4bb2534DVSaX+Azs2yKg8slkMBHOyGeU2kKx7Xmxp6Lee0jP8p6B3d7c1gFUPB2Z976dTUtX81pQA==
   dependencies:
     "@webcomponents/shadycss" "^1.9.1"
 
@@ -450,12 +181,7 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.2.tgz#7539b0ad29598aa2eafee8b341059e20ac9e1006"
   integrity sha512-vRq+GniJAYSBmTRnhCYPAPq6THYqovJ/gzGThWbgEZUQaBccndGTi1hdiUP15HzEco0I6t4RCtXyX0rsSmwgPw==
 
-"@webcomponents/webcomponentsjs@^1.3.3":
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
-  integrity sha512-eLH04VBMpuZGzBIhOnUjECcQPEPcmfhWEijW9u1B5I+2PPYdWf3vWUExdDxu4Y3GljRSTCOlWnGtS9tpzmXMyQ==
-
-"@webcomponents/webcomponentsjs@^2.0.3":
+"@webcomponents/webcomponentsjs@^2.0.3", "@webcomponents/webcomponentsjs@^2.8.0":
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz#ab21f027594fa827c1889e8b646da7be27c7908a"
   integrity sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==
@@ -478,9 +204,9 @@
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
 "aproba@^1.0.3 || ^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
-  integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.1.0.tgz#75500a190313d95c64e871e7e4284c6ac219f0b1"
+  integrity sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==
 
 are-we-there-yet@^2.0.0:
   version "2.0.0"
@@ -496,9 +222,9 @@
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
 brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
+  integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
   dependencies:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
@@ -533,9 +259,9 @@
   integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
 
 debug@4:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
-  integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+  integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
   dependencies:
     ms "^2.1.3"
 
@@ -552,9 +278,9 @@
   integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
 
 detect-libc@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
-  integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
+  integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
 
 emoji-regex@^8.0.0:
   version "8.0.0"
@@ -668,30 +394,30 @@
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
-lit-element@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.1.1.tgz#07905992815076e388cf6f1faffc7d6866c82007"
-  integrity sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==
+lit-element@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.2.1.tgz#0a3782f36eaa545862fe07f84abcb14b2903a042"
+  integrity sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==
   dependencies:
-    "@lit-labs/ssr-dom-shim" "^1.2.0"
-    "@lit/reactive-element" "^2.0.4"
-    lit-html "^3.2.0"
+    "@lit-labs/ssr-dom-shim" "^1.4.0"
+    "@lit/reactive-element" "^2.1.0"
+    lit-html "^3.3.0"
 
-lit-html@^3.2.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.2.1.tgz#8fc49e3531ee5947e4d93e8a5aa642ab1649833b"
-  integrity sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==
+lit-html@^3.3.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.3.1.tgz#f0a7e4b9ea0a1d034eb28a4bf2d1b0a0096253e3"
+  integrity sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==
   dependencies:
     "@types/trusted-types" "^2.0.2"
 
-lit@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/lit/-/lit-3.2.1.tgz#d6dd15eac20db3a098e81e2c85f70a751ff55592"
-  integrity sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==
+"lit@^2.8.0 || ^3.0.0", lit@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lit/-/lit-3.3.1.tgz#9dc79be626bc9a3b824de98b107dd662cabdeda6"
+  integrity sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==
   dependencies:
-    "@lit/reactive-element" "^2.0.4"
-    lit-element "^4.1.0"
-    lit-html "^3.2.0"
+    "@lit/reactive-element" "^2.1.0"
+    lit-element "^4.2.0"
+    lit-html "^3.3.0"
 
 make-dir@^3.1.0:
   version "3.1.0"
@@ -700,10 +426,10 @@
   dependencies:
     semver "^6.0.0"
 
-marked@~0.3.9:
-  version "0.3.19"
-  resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
-  integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
+marked@^0.5.0:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-0.5.2.tgz#3efdb27b1fd0ecec4f5aba362bddcd18120e5ba9"
+  integrity sha512-fdZvBa7/vSQIZCi4uuwo2N3q+7jJURpMVCcbaX0S1Mg65WZ5ilXvC67MviJAsdjqqgD+CEq4RKo5AYGgINkVAA==
 
 mimic-response@^2.0.0:
   version "2.1.0"
@@ -748,9 +474,9 @@
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
 nan@^2.17.0:
-  version "2.22.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3"
-  integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==
+  version "2.23.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.23.0.tgz#24aa4ddffcc37613a2d2935b97683c1ec96093c6"
+  integrity sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==
 
 node-fetch@^2.6.7:
   version "2.7.0"
@@ -850,9 +576,9 @@
   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
 
 semver@^7.3.5:
-  version "7.7.1"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
-  integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+  version "7.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+  integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
 
 set-blocking@^2.0.0:
   version "2.0.0"
@@ -923,6 +649,11 @@
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
+tslib@^2.4.0:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
 util-deprecate@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
diff --git a/polygerrit-ui/grep-patch-karma.js b/polygerrit-ui/grep-patch-karma.js
deleted file mode 100644
index ae0ff7f..0000000
--- a/polygerrit-ui/grep-patch-karma.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-// The IntelliJ (and probably other IDEs) passes test names as a regexp in
-// the format:
-// --grep=/some regexp.../
-// But mochajs doesn't expect the '/' characters before and after the regexp.
-// The code below patches input args and removes '/' if they exists.
-function installPatch(karma) {
-  const originalKarmaStart = karma.start;
-
-  karma.start = function (config, ...args) {
-    const regexpGrepPrefix = "--grep=/";
-    const regexpGrepSuffix = "/";
-    if (config && config.args) {
-      for (let i = 0; i < config.args.length; i++) {
-        const arg = config.args[i];
-        if (
-          arg.startsWith(regexpGrepPrefix) &&
-          arg.endsWith(regexpGrepSuffix)
-        ) {
-          const regexpText = arg.slice(
-            regexpGrepPrefix.length,
-            -regexpGrepPrefix.length
-          );
-          config.args[i] = "--grep=" + regexpText;
-        }
-      }
-    }
-    originalKarmaStart.apply(this, [config, ...args]);
-  };
-}
-
-const karma = window.__karma__;
-if (karma && karma.start && !karma.__grep_patch_installed__) {
-  karma.__grep_patch_installed__ = true;
-  installPatch(karma);
-}
diff --git a/polygerrit-ui/karma.conf.js b/polygerrit-ui/karma.conf.js
deleted file mode 100644
index b32c71c..0000000
--- a/polygerrit-ui/karma.conf.js
+++ /dev/null
@@ -1,250 +0,0 @@
-
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-const runUnderBazel = !!process.env["RUNFILES_DIR"];
-const path = require("path");
-
-function getModulesDir() {
-  if (runUnderBazel) {
-    // Run under bazel
-    return [
-      `external/plugins_npm/node_modules`,
-      `external/ui_npm/node_modules`,
-      `external/ui_dev_npm/node_modules`,
-    ];
-  }
-
-  // Run from intellij or npm run test:kdebug
-  return [
-    path.join(__dirname, "app/node_modules"),
-    path.join(__dirname, "node_modules"),
-  ];
-}
-
-function getUiDevNpmFilePath(importPath) {
-  if (runUnderBazel) {
-    return `external/ui_dev_npm/node_modules/${importPath}`;
-  } else {
-    return `polygerrit-ui/node_modules/${importPath}`;
-  }
-}
-
-function runInIde() {
-  // A simple detection of IDE.
-  // Default browserNoActivityTimeout is 30 seconds. An IDE usually
-  // runs karma in background and send commands when a user wants to
-  // execute test. If interval between user executed tests is bigger than
-  // browserNoActivityTimeout, the IDE reports error and doesn't restart
-  // server.
-  // We want to increase browserNoActivityTimeout when tests run in IDE.
-  // Wd don't want to increase it in other cases, oterhise hanging tests
-  // can slow down CI.
-  return (
-    !runUnderBazel &&
-    process.argv.some((arg) => arg.toLowerCase().contains("intellij"))
-  );
-}
-
-module.exports = function (config) {
-  let root = config.root;
-  if (!root) {
-    console.warn(`--root argument not set. Falling back to __dirname.`);
-    root = path.resolve(__dirname) + "/";
-  }
-  // Use --test-files to specify pattern for a test files.
-  // It can be just a file name, without a path:
-  // --test-files async-foreach-behavior_test.js
-  // If you specify --test-files without pattern, it gets true value
-  // In this case we will run all tests (usefull for package.json "debugtest"
-  // script)
-  // We will convert a .ts argument to .js and fill in .js if no extension is
-  // given.
-  let filePattern;
-  if (typeof config.testFiles === "string") {
-    if (config.testFiles.endsWith(".ts")) {
-      filePattern =
-        config.testFiles.substr(0, config.testFiles.lastIndexOf(".")) + ".js";
-    } else if (config.testFiles.endsWith(".js")) {
-      filePattern = config.testFiles;
-    } else {
-      filePattern = config.testFiles + ".js";
-    }
-  } else {
-    filePattern = "*_test.js";
-  }
-  const testFilesPattern = root + "**/" + filePattern;
-
-  console.info(`Karma test file pattern: ${testFilesPattern}`);
-  // Special patch for grep parameters (see details in the grep-patch-karam.js)
-  const additionalFiles = runUnderBazel
-    ? []
-    : ["polygerrit-ui/grep-patch-karma.js"];
-  config.set({
-    browserNoActivityTimeout: runInIde ? 60 * 60 * 1000 : 30 * 1000,
-    // base path that will be used to resolve all patterns (eg. files, exclude)
-    basePath: "../",
-    plugins: [
-      // Do not use karma-* to load all installed plugin
-      // This can lead to unexpected behavior under bazel
-      // if you forget to add a plugin in a bazel rule.
-      require.resolve("@open-wc/karma-esm"),
-      "karma-mocha",
-      "karma-chrome-launcher",
-      "karma-mocha-reporter",
-    ],
-    // frameworks to use
-    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
-    frameworks: ["mocha", "esm"],
-
-    // list of files / patterns to load in the browser
-    files: [
-      ...additionalFiles,
-      getUiDevNpmFilePath("source-map-support/browser-source-map-support.js"),
-      getUiDevNpmFilePath(
-        "accessibility-developer-tools/dist/js/axs_testing.js"
-      ),
-      {
-        pattern: getUiDevNpmFilePath("@open-wc/semantic-dom-diff/index.js"),
-        type: "module",
-      },
-      {
-        pattern: getUiDevNpmFilePath("@open-wc/testing-helpers/index.js"),
-        type: "module",
-      },
-      getUiDevNpmFilePath("sinon/pkg/sinon.js"),
-      { pattern: testFilesPattern, type: "module" },
-    ],
-    esm: {
-      nodeResolve: {
-        // By default, it tries to use page.mjs file instead of page.js
-        // when importing 'page/page', so we shouldn't use .mjs extension
-        // in node resolve.
-        // The .ts extension is required to display source code in browser
-        // (otherwise esm plugin crashes)
-        extensions: [".js", ".ts"],
-      },
-      moduleDirs: getModulesDir(),
-      // Bazel and yarn uses symlinks for files.
-      // preserveSymlinks is necessary for correct modules paths resolving
-      preserveSymlinks: true,
-      // By default, esm-dev-server uses 'auto' compatibility mode.
-      // In the 'auto' mode it incorrectly applies polyfills and
-      // breaks tests in some browser versions
-      // (for example, Chrome 69 on gerrit-ci).
-      compatibility: "none",
-      plugins: [
-        {
-          resolveImport(importSpecifier) {
-            // esm-dev-server interprets .ts files as .js files and
-            // tries to replace all module imports with relative/absolute
-            // paths. In most cases this works correctly. However if
-            // a ts file imports type from .d.ts and there is no
-            // associated .js file then the esm-dev-server responds with
-            // 500 error.
-            // For example the following import .ts file causes problem
-            // import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-            // To avoid problems, we don't resolve imports in .ts files
-            // and instead always return original path
-            if (importSpecifier.context.originalUrl.endsWith(".ts")) {
-              return importSpecifier.source;
-            }
-            return undefined;
-          },
-        },
-        {
-          transform(context) {
-            if (context.path.endsWith("/node_modules/page/page.js")) {
-              const orignalBody = context.body;
-              // Can't import page.js directly, because this is undefined.
-              // Replace it with window
-              // The same replace exists in server.go
-              // Rollup makes this replacement automatically
-              const transformedBody = orignalBody.replace(
-                "}(this, (function () { 'use strict';",
-                "}(window, (function () { 'use strict';"
-              );
-              if (orignalBody.length === transformedBody.length) {
-                console.error(
-                  "The page.js was updated. Please update transform accordingly"
-                );
-                process.exit(1);
-              }
-              return { body: transformedBody };
-            }
-          },
-        },
-      ],
-    },
-    // test results reporter to use
-    // possible values: 'dots', 'progress'
-    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
-    reporters: ["mocha"],
-
-    mochaReporter: {
-      showDiff: true,
-    },
-
-    // Listen on localhost so it either listens to ipv4
-    // or ipv6. Some OS's default to ipv6 for localhost
-    // and others ipv4.
-    // Nodejs 17 changed the behaviour from prefering ipv4 to
-    // using the OS settings.
-    // The default is 127.0.0.1 thus if localhost is on ipv6 only
-    // it'll fail to connect to the karma server.
-    // See https://github.com/karma-runner/karma/blob/e17698f950af83bf2b3edc540d2a3e1fb73cba59/lib/utils/dns-utils.js#L3
-    listenAddress: "localhost",
-
-    // web server port
-    port: 9876,
-
-    // enable / disable colors in the output (reporters and logs)
-    colors: true,
-
-    // level of logging
-    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
-    logLevel: config.LOG_INFO,
-
-    // enable / disable watching file and executing tests whenever any file changes
-    autoWatch: false,
-
-    // start these browsers
-    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
-    browsers: ["CustomChromeHeadless"],
-    browserForDebugging: "CustomChromeHeadlessWithDebugPort",
-
-    // Continuous Integration mode
-    // if true, Karma captures browsers, runs the tests and exits
-    singleRun: true,
-
-    // Concurrency level
-    // how many browser should be started simultaneous
-    concurrency: Infinity,
-
-    client: {
-      mocha: {
-        ui: "tdd",
-        timeout: 5000,
-      },
-    },
-
-    customLaunchers: {
-      // Based on https://developers.google.com/web/updates/2017/06/headless-karma-mocha-chai
-      CustomChromeHeadless: {
-        base: "ChromeHeadless",
-        flags: ["--disable-translate", "--disable-extensions"],
-      },
-      ChromeDev: {
-        base: "Chrome",
-        flags: ["--disable-extensions", " --auto-open-devtools-for-tabs"],
-      },
-      CustomChromeHeadlessWithDebugPort: {
-        base: "CustomChromeHeadless",
-        flags: ["--remote-debugging-port=9222"],
-      },
-    },
-  });
-};
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 519947e..1707aa3 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -3,35 +3,33 @@
   "description": "Gerrit Code Review - Polygerrit dev dependencies",
   "browser": true,
   "dependencies": {
-    "@types/sinon": "^10.0.16"
+    "@types/sinon": "^17.0.4"
   },
   "devDependencies": {
-    "@open-wc/karma-esm": "^3.0.9",
-    "@open-wc/semantic-dom-diff": "^0.19.9",
-    "@open-wc/testing": "^3.2.0",
-    "@web/dev-server-esbuild": "^0.3.6",
-    "@web/test-runner": "^0.14.0",
-    "@web/test-runner-playwright": "^0.9.0",
-    "@web/test-runner-visual-regression": "^0.7.1",
+    "@types/mocha": "^10.0.10",
+    "@open-wc/semantic-dom-diff": "^0.20.1",
+    "@open-wc/testing": "^4.0.0",
+    "@web/dev-server-esbuild": "^1.0.4",
+    "@web/test-runner": "^0.20.2",
+    "@web/test-runner-playwright": "^0.11.1",
+    "@web/test-runner-visual-regression": "^0.10.0",
     "accessibility-developer-tools": "^2.12.0",
-    "karma": "^6.4.4",
-    "karma-chrome-launcher": "^3.2.0",
-    "karma-mocha": "^2.0.1",
-    "karma-mocha-reporter": "^2.2.5",
-    "mocha": "9.2.2",
-    "sinon": "^13.0.2",
+    "mocha": "^10.2.0",
+    "sinon": "^20.0.0",
     "source-map-support": "^0.5.21"
   },
   "scripts": {
+    "postinstall": "playwright install",
     "test": "web-test-runner",
     "test:screenshot": "web-test-runner --run-screenshots",
-    "test:screenshot-update": "web-test-runner --update-screenshots --files",
+    "test:screenshot-update": "web-test-runner --run-screenshots --update-screenshots --files",
     "test:browsers": "web-test-runner --playwright --browsers webkit firefox chromium",
     "test:coverage": "web-test-runner --coverage",
     "test:watch": "web-test-runner --watch",
     "test:single": "web-test-runner --watch --group default --files",
+    "test:single:nowatch": "web-test-runner --group default --files",
     "test:single:coverage": "web-test-runner --watch --coverage --files"
   },
   "license": "Apache-2.0",
   "private": true
-}
+}
\ No newline at end of file
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-account-info-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-account-info-dark.png
new file mode 100644
index 0000000..9fa51fc
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-account-info-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-account-info.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-account-info.png
new file mode 100644
index 0000000..d1227fd
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-account-info.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-admin-group-list-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-admin-group-list-dark.png
new file mode 100644
index 0000000..cab7aca
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-admin-group-list-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-admin-group-list.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-admin-group-list.png
new file mode 100644
index 0000000..508e19b
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-admin-group-list.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
new file mode 100644
index 0000000..01c139a
--- /dev/null
+++ 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.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list.png
new file mode 100644
index 0000000..cf6fc7a
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-list.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png
new file mode 100644
index 0000000..d85c67c
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png
new file mode 100644
index 0000000..7406562
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-checks-results-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-checks-results-dark.png
new file mode 100644
index 0000000..2c81cf5
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-checks-results-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-checks-results.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-checks-results.png
new file mode 100644
index 0000000..4af5816
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-checks-results.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-resolved-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-resolved-dark.png
new file mode 100644
index 0000000..d6b63bf
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-resolved-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-resolved.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-resolved.png
new file mode 100644
index 0000000..e9cfdd2
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-resolved.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-unresolved-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-unresolved-dark.png
new file mode 100644
index 0000000..cdfebe6
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-unresolved-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-unresolved.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-unresolved.png
new file mode 100644
index 0000000..a3c2cda
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-unresolved.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-with-draft-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-with-draft-dark.png
new file mode 100644
index 0000000..f1db32b
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-with-draft-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-with-draft.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-with-draft.png
new file mode 100644
index 0000000..c8c9517
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-comment-thread-with-draft.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-copy-links.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-copy-links.png
new file mode 100644
index 0000000..e41749f
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-copy-links.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-collapsed-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-collapsed-dark.png
new file mode 100644
index 0000000..5b7b70f
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-collapsed-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-collapsed.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-collapsed.png
new file mode 100644
index 0000000..96bea54
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-collapsed.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-expanded-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-expanded-dark.png
new file mode 100644
index 0000000..5553c10
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-expanded-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-expanded.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-expanded.png
new file mode 100644
index 0000000..d42627f
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-diff-check-result-expanded.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-file-list-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-file-list-dark.png
new file mode 100644
index 0000000..76b57e8
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-file-list-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-file-list.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-file-list.png
new file mode 100644
index 0000000..4b1184a
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-file-list.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-label-score-row-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-label-score-row-dark.png
new file mode 100644
index 0000000..49cbcf6
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-label-score-row-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-label-score-row.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-label-score-row.png
new file mode 100644
index 0000000..4fa6f74
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-label-score-row.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-rule-editor-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-rule-editor-dark.png
new file mode 100644
index 0000000..58fffae
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-rule-editor-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-rule-editor.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-rule-editor.png
new file mode 100644
index 0000000..94b2f6b5
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-rule-editor.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-user-suggestion-fix-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-user-suggestion-fix-dark.png
new file mode 100644
index 0000000..34fca7b
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-user-suggestion-fix-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-user-suggestion-fix.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-user-suggestion-fix.png
new file mode 100644
index 0000000..e308091
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-user-suggestion-fix.png
Binary files differ
diff --git a/polygerrit-ui/web-test-runner.config.mjs b/polygerrit-ui/web-test-runner.config.mjs
index 52b9073..24cfaf7 100644
--- a/polygerrit-ui/web-test-runner.config.mjs
+++ b/polygerrit-ui/web-test-runner.config.mjs
@@ -2,6 +2,7 @@
 import { esbuildPlugin } from '@web/dev-server-esbuild';
 import { defaultReporter, summaryReporter } from '@web/test-runner';
 import { visualRegressionPlugin } from '@web/test-runner-visual-regression/plugin';
+import { playwrightLauncher } from '@web/test-runner-playwright';
 
 function testRunnerHtmlFactory() {
   return (testFramework) => `
@@ -48,37 +49,65 @@
 }
 
 const pathPrefix = runUnderBazel ? 'polygerrit-ui/' : '';
-const testFiles = getArgValue('--test-files') ?? `${pathPrefix}app/**/*_test.{ts,js}`;
+const testFiles = getArgValue('--test-files');
+const runScreenshots = process.argv.includes('--run-screenshots');
 const rootDir = getArgValue('--root-dir') ?? `${path.resolve(process.cwd())}/`;
 const tsConfig = getArgValue('--ts-config') ?? `${pathPrefix}app/tsconfig.json`;
 
 /** @type {import('@web/test-runner').TestRunnerConfig} */
 const config = {
-  // TODO: https://g-issues.gerritcodereview.com/issues/365565157 - undo the
-  // change once the underlying issue is fixed.
-  concurrency: 1,
-
-  files: [
-    testFiles,
-    `!${pathPrefix}**/node_modules/**/*`,
-    ...(process.argv.includes('--run-screenshots')
-      ? []
-      : [`!${pathPrefix}app/**/*_screenshot_test.{ts,js}`]),
+  // 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: {
+        args: [
+          '--disable-background-timer-throttling',
+          '--disable-backgrounding-occluded-windows',
+          '--disable-renderer-backgrounding',
+        ],
+      },
+    }),
   ],
 
+  files: runScreenshots
+      ? [
+          // If --run-screenshots is set, ONLY run screenshot tests.
+          testFiles ?? `${pathPrefix}app/**/*_screenshot_test.{ts,js}`,
+          `!${pathPrefix}**/node_modules/**/*`,
+        ]
+      : [
+          // Otherwise, run all tests EXCEPT screenshot tests
+          testFiles ?? `${pathPrefix}app/**/*_test.{ts,js}`,
+          `!${pathPrefix}**/node_modules/**/*`,
+          `!${pathPrefix}app/**/*_screenshot_test.{ts,js}`,
+        ],
+
   port: 9876,
 
   nodeResolve: {
-    moduleDirectories: getModulesDir(),
+    modulePaths: getModulesDir(),
   },
 
   testFramework: {
     config: {
       ui: 'tdd',
-      timeout: 5000,
+      timeout: 2000,
     },
   },
 
+  coverageConfig: {
+    report: true,
+    reportDir: 'coverage',
+    reporters: ['lcov', 'text'],
+  },
+
   plugins: [
     esbuildPlugin({
       ts: true,
@@ -86,14 +115,19 @@
       tsconfig: tsConfig,
     }),
     visualRegressionPlugin({
-      diffOptions: { threshold: 0.8 },
+      // TODO(milutin): Tweak these values - diffOptions threshold is for color change
+      // and failureThreshold is for pixel change. We need to find a balance to allow
+      // CI to pass, but also catch regressions.
+      diffOptions: { threshold: 0.6 },
+      failureThreshold: 1,
+      failureThresholdType: 'percent',
       update: process.argv.includes('--update-screenshots'),
     }),
   ],
 
   // serve from gerrit root directory so that we can serve fonts from
-  // /lib/fonts/, see middleware.
-  rootDir,
+  // /lib/fonts/ for screenshots tests, see middleware.
+  rootDir: runScreenshots ? '..' : rootDir,
 
   reporters: [defaultReporter(), summaryReporter()],
 
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index c73413f..540168f 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -2,1094 +2,149 @@
 # yarn lockfile v1
 
 
-"@75lb/deep-merge@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@75lb/deep-merge/-/deep-merge-1.1.1.tgz#3b06155b90d34f5f8cc2107d796f1853ba02fd6d"
-  integrity sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==
+"@babel/code-frame@^7.12.11":
+  version "7.27.1"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
+  integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
   dependencies:
-    lodash.assignwith "^4.2.0"
-    typical "^7.1.1"
-
-"@ampproject/remapping@^2.2.0":
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
-  integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
-  dependencies:
-    "@jridgewell/gen-mapping" "^0.3.0"
-    "@jridgewell/trace-mapping" "^0.3.9"
-
-"@babel/code-frame@^7.12.11", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5":
-  version "7.23.5"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
-  integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
-  dependencies:
-    "@babel/highlight" "^7.23.4"
-    chalk "^2.4.2"
-
-"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5":
-  version "7.23.5"
-  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
-  integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==
-
-"@babel/core@^7.11.1":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.7.tgz#4d8016e06a14b5f92530a13ed0561730b5c6483f"
-  integrity sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==
-  dependencies:
-    "@ampproject/remapping" "^2.2.0"
-    "@babel/code-frame" "^7.23.5"
-    "@babel/generator" "^7.23.6"
-    "@babel/helper-compilation-targets" "^7.23.6"
-    "@babel/helper-module-transforms" "^7.23.3"
-    "@babel/helpers" "^7.23.7"
-    "@babel/parser" "^7.23.6"
-    "@babel/template" "^7.22.15"
-    "@babel/traverse" "^7.23.7"
-    "@babel/types" "^7.23.6"
-    convert-source-map "^2.0.0"
-    debug "^4.1.0"
-    gensync "^1.0.0-beta.2"
-    json5 "^2.2.3"
-    semver "^6.3.1"
-
-"@babel/generator@^7.23.6", "@babel/generator@^7.4.0":
-  version "7.23.6"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e"
-  integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==
-  dependencies:
-    "@babel/types" "^7.23.6"
-    "@jridgewell/gen-mapping" "^0.3.2"
-    "@jridgewell/trace-mapping" "^0.3.17"
-    jsesc "^2.5.1"
-
-"@babel/helper-annotate-as-pure@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
-  integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
-  dependencies:
-    "@babel/types" "^7.22.5"
-
-"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15":
-  version "7.22.15"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956"
-  integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==
-  dependencies:
-    "@babel/types" "^7.22.15"
-
-"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6":
-  version "7.23.6"
-  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991"
-  integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==
-  dependencies:
-    "@babel/compat-data" "^7.23.5"
-    "@babel/helper-validator-option" "^7.23.5"
-    browserslist "^4.22.2"
-    lru-cache "^5.1.1"
-    semver "^6.3.1"
-
-"@babel/helper-create-class-features-plugin@^7.22.15":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz#b2e6826e0e20d337143655198b79d58fdc9bd43d"
-  integrity sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-function-name" "^7.23.0"
-    "@babel/helper-member-expression-to-functions" "^7.23.0"
-    "@babel/helper-optimise-call-expression" "^7.22.5"
-    "@babel/helper-replace-supers" "^7.22.20"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-    "@babel/helper-split-export-declaration" "^7.22.6"
-    semver "^6.3.1"
-
-"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5":
-  version "7.22.15"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1"
-  integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    regexpu-core "^5.3.1"
-    semver "^6.3.1"
-
-"@babel/helper-define-polyfill-provider@^0.4.4":
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz#64df615451cb30e94b59a9696022cffac9a10088"
-  integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==
-  dependencies:
-    "@babel/helper-compilation-targets" "^7.22.6"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    debug "^4.1.1"
-    lodash.debounce "^4.0.8"
-    resolve "^1.14.2"
-
-"@babel/helper-environment-visitor@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
-  integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
-
-"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0":
-  version "7.23.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
-  integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
-  dependencies:
-    "@babel/template" "^7.22.15"
-    "@babel/types" "^7.23.0"
-
-"@babel/helper-hoist-variables@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
-  integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
-  dependencies:
-    "@babel/types" "^7.22.5"
-
-"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.23.0":
-  version "7.23.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366"
-  integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==
-  dependencies:
-    "@babel/types" "^7.23.0"
-
-"@babel/helper-module-imports@^7.22.15":
-  version "7.22.15"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
-  integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
-  dependencies:
-    "@babel/types" "^7.22.15"
-
-"@babel/helper-module-transforms@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1"
-  integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==
-  dependencies:
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-module-imports" "^7.22.15"
-    "@babel/helper-simple-access" "^7.22.5"
-    "@babel/helper-split-export-declaration" "^7.22.6"
-    "@babel/helper-validator-identifier" "^7.22.20"
-
-"@babel/helper-optimise-call-expression@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e"
-  integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==
-  dependencies:
-    "@babel/types" "^7.22.5"
-
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295"
-  integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==
-
-"@babel/helper-remap-async-to-generator@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0"
-  integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-wrap-function" "^7.22.20"
-
-"@babel/helper-replace-supers@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793"
-  integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==
-  dependencies:
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-member-expression-to-functions" "^7.22.15"
-    "@babel/helper-optimise-call-expression" "^7.22.5"
-
-"@babel/helper-simple-access@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de"
-  integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==
-  dependencies:
-    "@babel/types" "^7.22.5"
-
-"@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847"
-  integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==
-  dependencies:
-    "@babel/types" "^7.22.5"
-
-"@babel/helper-split-export-declaration@^7.22.6":
-  version "7.22.6"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
-  integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
-  dependencies:
-    "@babel/types" "^7.22.5"
-
-"@babel/helper-string-parser@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
-  integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
-
-"@babel/helper-validator-identifier@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
-  integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-
-"@babel/helper-validator-option@^7.23.5":
-  version "7.23.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
-  integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
-
-"@babel/helper-wrap-function@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569"
-  integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==
-  dependencies:
-    "@babel/helper-function-name" "^7.22.5"
-    "@babel/template" "^7.22.15"
-    "@babel/types" "^7.22.19"
-
-"@babel/helpers@^7.23.7":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.7.tgz#eb543c36f81da2873e47b76ee032343ac83bba60"
-  integrity sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==
-  dependencies:
-    "@babel/template" "^7.22.15"
-    "@babel/traverse" "^7.23.7"
-    "@babel/types" "^7.23.6"
-
-"@babel/highlight@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
-  integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.22.20"
-    chalk "^2.4.2"
+    "@babel/helper-validator-identifier" "^7.27.1"
     js-tokens "^4.0.0"
+    picocolors "^1.1.1"
 
-"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.6", "@babel/parser@^7.4.3":
-  version "7.23.6"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b"
-  integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==
+"@babel/helper-validator-identifier@^7.27.1":
+  version "7.27.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
+  integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
 
-"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a"
-  integrity sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
+"@esbuild/aix-ppc64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
+  integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
 
-"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz#f6652bb16b94f8f9c20c50941e16e9756898dc5d"
-  integrity sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-    "@babel/plugin-transform-optional-chaining" "^7.23.3"
+"@esbuild/android-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
+  integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
 
-"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.7":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz#516462a95d10a9618f197d39ad291a9b47ae1d7b"
-  integrity sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==
-  dependencies:
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-plugin-utils" "^7.22.5"
+"@esbuild/android-arm@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
+  integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
 
-"@babel/plugin-proposal-dynamic-import@^7.10.4":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94"
-  integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.18.6"
-    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+"@esbuild/android-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
+  integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
 
-"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1"
-  integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.18.6"
-    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+"@esbuild/darwin-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae"
+  integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
 
-"@babel/plugin-proposal-optional-chaining@^7.11.0":
-  version "7.21.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea"
-  integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.20.2"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0"
-    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+"@esbuild/darwin-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
+  integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
 
-"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2":
-  version "7.21.0-placeholder-for-preset-env.2"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
-  integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
+"@esbuild/freebsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
+  integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
 
-"@babel/plugin-syntax-async-generators@^7.8.4":
-  version "7.8.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
-  integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/freebsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
+  integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
 
-"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10"
-  integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
+"@esbuild/linux-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
+  integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
 
-"@babel/plugin-syntax-class-static-block@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406"
-  integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.14.5"
+"@esbuild/linux-arm@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
+  integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
 
-"@babel/plugin-syntax-dynamic-import@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
-  integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/linux-ia32@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
+  integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
 
-"@babel/plugin-syntax-export-namespace-from@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a"
-  integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+"@esbuild/linux-loong64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
+  integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
 
-"@babel/plugin-syntax-import-assertions@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz#9c05a7f592982aff1a2768260ad84bcd3f0c77fc"
-  integrity sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
+"@esbuild/linux-mips64el@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
+  integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
 
-"@babel/plugin-syntax-import-attributes@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz#992aee922cf04512461d7dae3ff6951b90a2dc06"
-  integrity sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
+"@esbuild/linux-ppc64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
+  integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
 
-"@babel/plugin-syntax-import-meta@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
-  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.10.4"
+"@esbuild/linux-riscv64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
+  integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
 
-"@babel/plugin-syntax-json-strings@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
-  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/linux-s390x@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
+  integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
 
-"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
-  integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.10.4"
+"@esbuild/linux-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f"
+  integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
 
-"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
-  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/netbsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
+  integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
 
-"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
-  integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.10.4"
+"@esbuild/netbsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
+  integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
 
-"@babel/plugin-syntax-object-rest-spread@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
-  integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/openbsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
+  integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
 
-"@babel/plugin-syntax-optional-catch-binding@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
-  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/openbsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
+  integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
 
-"@babel/plugin-syntax-optional-chaining@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
-  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
+"@esbuild/openharmony-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
+  integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
 
-"@babel/plugin-syntax-private-property-in-object@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad"
-  integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.14.5"
+"@esbuild/sunos-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
+  integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
 
-"@babel/plugin-syntax-top-level-await@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c"
-  integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.14.5"
+"@esbuild/win32-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
+  integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
 
-"@babel/plugin-syntax-unicode-sets-regex@^7.18.6":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357"
-  integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.18.6"
-    "@babel/helper-plugin-utils" "^7.18.6"
+"@esbuild/win32-ia32@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
+  integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
 
-"@babel/plugin-transform-arrow-functions@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz#94c6dcfd731af90f27a79509f9ab7fb2120fc38b"
-  integrity sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-async-generator-functions@^7.23.7":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz#3aa0b4f2fa3788b5226ef9346cf6d16ec61f99cd"
-  integrity sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==
-  dependencies:
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-remap-async-to-generator" "^7.22.20"
-    "@babel/plugin-syntax-async-generators" "^7.8.4"
-
-"@babel/plugin-transform-async-to-generator@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz#d1f513c7a8a506d43f47df2bf25f9254b0b051fa"
-  integrity sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==
-  dependencies:
-    "@babel/helper-module-imports" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-remap-async-to-generator" "^7.22.20"
-
-"@babel/plugin-transform-block-scoped-functions@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz#fe1177d715fb569663095e04f3598525d98e8c77"
-  integrity sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-block-scoping@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz#b2d38589531c6c80fbe25e6b58e763622d2d3cf5"
-  integrity sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-class-properties@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz#35c377db11ca92a785a718b6aa4e3ed1eb65dc48"
-  integrity sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==
-  dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-class-static-block@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz#2a202c8787a8964dd11dfcedf994d36bfc844ab5"
-  integrity sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==
-  dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-class-static-block" "^7.14.5"
-
-"@babel/plugin-transform-classes@^7.23.5":
-  version "7.23.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz#e7a75f815e0c534cc4c9a39c56636c84fc0d64f2"
-  integrity sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-compilation-targets" "^7.22.15"
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-function-name" "^7.23.0"
-    "@babel/helper-optimise-call-expression" "^7.22.5"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-replace-supers" "^7.22.20"
-    "@babel/helper-split-export-declaration" "^7.22.6"
-    globals "^11.1.0"
-
-"@babel/plugin-transform-computed-properties@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz#652e69561fcc9d2b50ba4f7ac7f60dcf65e86474"
-  integrity sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/template" "^7.22.15"
-
-"@babel/plugin-transform-destructuring@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz#8c9ee68228b12ae3dff986e56ed1ba4f3c446311"
-  integrity sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-dotall-regex@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz#3f7af6054882ede89c378d0cf889b854a993da50"
-  integrity sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-duplicate-keys@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz#664706ca0a5dfe8d066537f99032fc1dc8b720ce"
-  integrity sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-dynamic-import@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz#c7629e7254011ac3630d47d7f34ddd40ca535143"
-  integrity sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
-
-"@babel/plugin-transform-exponentiation-operator@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz#ea0d978f6b9232ba4722f3dbecdd18f450babd18"
-  integrity sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==
-  dependencies:
-    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-export-namespace-from@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz#084c7b25e9a5c8271e987a08cf85807b80283191"
-  integrity sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
-
-"@babel/plugin-transform-for-of@^7.23.6":
-  version "7.23.6"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz#81c37e24171b37b370ba6aaffa7ac86bcb46f94e"
-  integrity sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-
-"@babel/plugin-transform-function-name@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz#8f424fcd862bf84cb9a1a6b42bc2f47ed630f8dc"
-  integrity sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==
-  dependencies:
-    "@babel/helper-compilation-targets" "^7.22.15"
-    "@babel/helper-function-name" "^7.23.0"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-json-strings@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz#a871d9b6bd171976efad2e43e694c961ffa3714d"
-  integrity sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-json-strings" "^7.8.3"
-
-"@babel/plugin-transform-literals@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz#8214665f00506ead73de157eba233e7381f3beb4"
-  integrity sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-logical-assignment-operators@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz#e599f82c51d55fac725f62ce55d3a0886279ecb5"
-  integrity sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
-
-"@babel/plugin-transform-member-expression-literals@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz#e37b3f0502289f477ac0e776b05a833d853cabcc"
-  integrity sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-modules-amd@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz#e19b55436a1416829df0a1afc495deedfae17f7d"
-  integrity sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==
-  dependencies:
-    "@babel/helper-module-transforms" "^7.23.3"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-modules-commonjs@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4"
-  integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==
-  dependencies:
-    "@babel/helper-module-transforms" "^7.23.3"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-simple-access" "^7.22.5"
-
-"@babel/plugin-transform-modules-systemjs@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz#fa7e62248931cb15b9404f8052581c302dd9de81"
-  integrity sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==
-  dependencies:
-    "@babel/helper-hoist-variables" "^7.22.5"
-    "@babel/helper-module-transforms" "^7.23.3"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-validator-identifier" "^7.22.20"
-
-"@babel/plugin-transform-modules-umd@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz#5d4395fccd071dfefe6585a4411aa7d6b7d769e9"
-  integrity sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==
-  dependencies:
-    "@babel/helper-module-transforms" "^7.23.3"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f"
-  integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.22.5"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-new-target@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz#5491bb78ed6ac87e990957cea367eab781c4d980"
-  integrity sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-nullish-coalescing-operator@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz#45556aad123fc6e52189ea749e33ce090637346e"
-  integrity sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
-
-"@babel/plugin-transform-numeric-separator@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz#03d08e3691e405804ecdd19dd278a40cca531f29"
-  integrity sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
-
-"@babel/plugin-transform-object-rest-spread@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz#2b9c2d26bf62710460bdc0d1730d4f1048361b83"
-  integrity sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==
-  dependencies:
-    "@babel/compat-data" "^7.23.3"
-    "@babel/helper-compilation-targets" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
-    "@babel/plugin-transform-parameters" "^7.23.3"
-
-"@babel/plugin-transform-object-super@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz#81fdb636dcb306dd2e4e8fd80db5b2362ed2ebcd"
-  integrity sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-replace-supers" "^7.22.20"
-
-"@babel/plugin-transform-optional-catch-binding@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz#318066de6dacce7d92fa244ae475aa8d91778017"
-  integrity sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-
-"@babel/plugin-transform-optional-chaining@^7.23.3", "@babel/plugin-transform-optional-chaining@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz#6acf61203bdfc4de9d4e52e64490aeb3e52bd017"
-  integrity sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
-
-"@babel/plugin-transform-parameters@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz#83ef5d1baf4b1072fa6e54b2b0999a7b2527e2af"
-  integrity sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-private-methods@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz#b2d7a3c97e278bfe59137a978d53b2c2e038c0e4"
-  integrity sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==
-  dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-private-property-in-object@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz#3ec711d05d6608fd173d9b8de39872d8dbf68bf5"
-  integrity sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-create-class-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
-
-"@babel/plugin-transform-property-literals@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz#54518f14ac4755d22b92162e4a852d308a560875"
-  integrity sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-regenerator@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz#141afd4a2057298602069fce7f2dc5173e6c561c"
-  integrity sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    regenerator-transform "^0.15.2"
-
-"@babel/plugin-transform-reserved-words@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz#4130dcee12bd3dd5705c587947eb715da12efac8"
-  integrity sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-shorthand-properties@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz#97d82a39b0e0c24f8a981568a8ed851745f59210"
-  integrity sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-spread@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz#41d17aacb12bde55168403c6f2d6bdca563d362c"
-  integrity sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-
-"@babel/plugin-transform-sticky-regex@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz#dec45588ab4a723cb579c609b294a3d1bd22ff04"
-  integrity sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-template-literals@^7.23.3", "@babel/plugin-transform-template-literals@^7.8.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz#5f0f028eb14e50b5d0f76be57f90045757539d07"
-  integrity sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-typeof-symbol@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz#9dfab97acc87495c0c449014eb9c547d8966bca4"
-  integrity sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-unicode-escapes@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz#1f66d16cab01fab98d784867d24f70c1ca65b925"
-  integrity sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-unicode-property-regex@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz#19e234129e5ffa7205010feec0d94c251083d7ad"
-  integrity sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-unicode-regex@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz#26897708d8f42654ca4ce1b73e96140fbad879dc"
-  integrity sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/plugin-transform-unicode-sets-regex@^7.23.3":
-  version "7.23.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz#4fb6f0a719c2c5859d11f6b55a050cc987f3799e"
-  integrity sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.22.15"
-    "@babel/helper-plugin-utils" "^7.22.5"
-
-"@babel/preset-env@^7.9.0":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.7.tgz#e5d69b9f14db8a13bae4d8e5ce7f360973626241"
-  integrity sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==
-  dependencies:
-    "@babel/compat-data" "^7.23.5"
-    "@babel/helper-compilation-targets" "^7.23.6"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-validator-option" "^7.23.5"
-    "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3"
-    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3"
-    "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.7"
-    "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2"
-    "@babel/plugin-syntax-async-generators" "^7.8.4"
-    "@babel/plugin-syntax-class-properties" "^7.12.13"
-    "@babel/plugin-syntax-class-static-block" "^7.14.5"
-    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
-    "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
-    "@babel/plugin-syntax-import-assertions" "^7.23.3"
-    "@babel/plugin-syntax-import-attributes" "^7.23.3"
-    "@babel/plugin-syntax-import-meta" "^7.10.4"
-    "@babel/plugin-syntax-json-strings" "^7.8.3"
-    "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
-    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
-    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
-    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
-    "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
-    "@babel/plugin-syntax-top-level-await" "^7.14.5"
-    "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
-    "@babel/plugin-transform-arrow-functions" "^7.23.3"
-    "@babel/plugin-transform-async-generator-functions" "^7.23.7"
-    "@babel/plugin-transform-async-to-generator" "^7.23.3"
-    "@babel/plugin-transform-block-scoped-functions" "^7.23.3"
-    "@babel/plugin-transform-block-scoping" "^7.23.4"
-    "@babel/plugin-transform-class-properties" "^7.23.3"
-    "@babel/plugin-transform-class-static-block" "^7.23.4"
-    "@babel/plugin-transform-classes" "^7.23.5"
-    "@babel/plugin-transform-computed-properties" "^7.23.3"
-    "@babel/plugin-transform-destructuring" "^7.23.3"
-    "@babel/plugin-transform-dotall-regex" "^7.23.3"
-    "@babel/plugin-transform-duplicate-keys" "^7.23.3"
-    "@babel/plugin-transform-dynamic-import" "^7.23.4"
-    "@babel/plugin-transform-exponentiation-operator" "^7.23.3"
-    "@babel/plugin-transform-export-namespace-from" "^7.23.4"
-    "@babel/plugin-transform-for-of" "^7.23.6"
-    "@babel/plugin-transform-function-name" "^7.23.3"
-    "@babel/plugin-transform-json-strings" "^7.23.4"
-    "@babel/plugin-transform-literals" "^7.23.3"
-    "@babel/plugin-transform-logical-assignment-operators" "^7.23.4"
-    "@babel/plugin-transform-member-expression-literals" "^7.23.3"
-    "@babel/plugin-transform-modules-amd" "^7.23.3"
-    "@babel/plugin-transform-modules-commonjs" "^7.23.3"
-    "@babel/plugin-transform-modules-systemjs" "^7.23.3"
-    "@babel/plugin-transform-modules-umd" "^7.23.3"
-    "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5"
-    "@babel/plugin-transform-new-target" "^7.23.3"
-    "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.4"
-    "@babel/plugin-transform-numeric-separator" "^7.23.4"
-    "@babel/plugin-transform-object-rest-spread" "^7.23.4"
-    "@babel/plugin-transform-object-super" "^7.23.3"
-    "@babel/plugin-transform-optional-catch-binding" "^7.23.4"
-    "@babel/plugin-transform-optional-chaining" "^7.23.4"
-    "@babel/plugin-transform-parameters" "^7.23.3"
-    "@babel/plugin-transform-private-methods" "^7.23.3"
-    "@babel/plugin-transform-private-property-in-object" "^7.23.4"
-    "@babel/plugin-transform-property-literals" "^7.23.3"
-    "@babel/plugin-transform-regenerator" "^7.23.3"
-    "@babel/plugin-transform-reserved-words" "^7.23.3"
-    "@babel/plugin-transform-shorthand-properties" "^7.23.3"
-    "@babel/plugin-transform-spread" "^7.23.3"
-    "@babel/plugin-transform-sticky-regex" "^7.23.3"
-    "@babel/plugin-transform-template-literals" "^7.23.3"
-    "@babel/plugin-transform-typeof-symbol" "^7.23.3"
-    "@babel/plugin-transform-unicode-escapes" "^7.23.3"
-    "@babel/plugin-transform-unicode-property-regex" "^7.23.3"
-    "@babel/plugin-transform-unicode-regex" "^7.23.3"
-    "@babel/plugin-transform-unicode-sets-regex" "^7.23.3"
-    "@babel/preset-modules" "0.1.6-no-external-plugins"
-    babel-plugin-polyfill-corejs2 "^0.4.7"
-    babel-plugin-polyfill-corejs3 "^0.8.7"
-    babel-plugin-polyfill-regenerator "^0.5.4"
-    core-js-compat "^3.31.0"
-    semver "^6.3.1"
-
-"@babel/preset-modules@0.1.6-no-external-plugins":
-  version "0.1.6-no-external-plugins"
-  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a"
-  integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.0.0"
-    "@babel/types" "^7.4.4"
-    esutils "^2.0.2"
-
-"@babel/regjsgen@^0.8.0":
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
-  integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
-
-"@babel/runtime@^7.8.4":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.7.tgz#dd7c88deeb218a0f8bd34d5db1aa242e0f203193"
-  integrity sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==
-  dependencies:
-    regenerator-runtime "^0.14.0"
-
-"@babel/template@^7.22.15", "@babel/template@^7.4.0":
-  version "7.22.15"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
-  integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
-  dependencies:
-    "@babel/code-frame" "^7.22.13"
-    "@babel/parser" "^7.22.15"
-    "@babel/types" "^7.22.15"
-
-"@babel/traverse@^7.23.7", "@babel/traverse@^7.4.3":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305"
-  integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==
-  dependencies:
-    "@babel/code-frame" "^7.23.5"
-    "@babel/generator" "^7.23.6"
-    "@babel/helper-environment-visitor" "^7.22.20"
-    "@babel/helper-function-name" "^7.23.0"
-    "@babel/helper-hoist-variables" "^7.22.5"
-    "@babel/helper-split-export-declaration" "^7.22.6"
-    "@babel/parser" "^7.23.6"
-    "@babel/types" "^7.23.6"
-    debug "^4.3.1"
-    globals "^11.1.0"
-
-"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
-  version "7.23.6"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd"
-  integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==
-  dependencies:
-    "@babel/helper-string-parser" "^7.23.4"
-    "@babel/helper-validator-identifier" "^7.22.20"
-    to-fast-properties "^2.0.0"
-
-"@colors/colors@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
-  integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
-
-"@esbuild/android-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
-  integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
-
-"@esbuild/android-arm@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
-  integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
-
-"@esbuild/android-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
-  integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
-
-"@esbuild/darwin-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
-  integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
-
-"@esbuild/darwin-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
-  integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
-
-"@esbuild/freebsd-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
-  integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
-
-"@esbuild/freebsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
-  integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
-
-"@esbuild/linux-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
-  integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
-
-"@esbuild/linux-arm@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
-  integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
-
-"@esbuild/linux-ia32@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
-  integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
-
-"@esbuild/linux-loong64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
-  integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
-
-"@esbuild/linux-mips64el@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
-  integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
-
-"@esbuild/linux-ppc64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
-  integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
-
-"@esbuild/linux-riscv64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
-  integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
-
-"@esbuild/linux-s390x@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
-  integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
-
-"@esbuild/linux-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
-  integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
-
-"@esbuild/netbsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
-  integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
-
-"@esbuild/openbsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
-  integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
-
-"@esbuild/sunos-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
-  integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
-
-"@esbuild/win32-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
-  integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
-
-"@esbuild/win32-ia32@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
-  integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
-
-"@esbuild/win32-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
-  integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
+"@esbuild/win32-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
+  integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
 
 "@esm-bundle/chai@^4.3.4-fix.0":
   version "4.3.4-fix.0"
@@ -1098,56 +153,40 @@
   dependencies:
     "@types/chai" "^4.2.12"
 
-"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
-  integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
-  dependencies:
-    "@jridgewell/set-array" "^1.0.1"
-    "@jridgewell/sourcemap-codec" "^1.4.10"
-    "@jridgewell/trace-mapping" "^0.3.9"
+"@hapi/bourne@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7"
+  integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==
 
 "@jridgewell/resolve-uri@^3.1.0":
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
-  integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+  integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
 
-"@jridgewell/set-array@^1.0.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
-  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+"@jridgewell/sourcemap-codec@^1.4.14":
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+  integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
 
-"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
-  version "1.4.15"
-  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
-  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
-
-"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
-  version "0.3.20"
-  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
-  integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
+"@jridgewell/trace-mapping@^0.3.12":
+  version "0.3.30"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99"
+  integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==
   dependencies:
     "@jridgewell/resolve-uri" "^3.1.0"
     "@jridgewell/sourcemap-codec" "^1.4.14"
 
-"@koa/cors@^3.1.0":
-  version "3.4.3"
-  resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.4.3.tgz#d669ee6e8d6e4f0ec4a7a7b0a17e7a3ed3752ebb"
-  integrity sha512-WPXQUaAeAMVaLTEFpoq3T2O1C+FstkjJnDQqy95Ck1UdILajsRhu6mhJ8H2f4NFPRBoCNN+qywTJfq/gGki5mw==
-  dependencies:
-    vary "^1.1.2"
+"@lit-labs/ssr-dom-shim@^1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz#55eb80ab5ef6e188f7e541c1e2bea1ef582413b8"
+  integrity sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==
 
-"@lit-labs/ssr-dom-shim@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312"
-  integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==
-
-"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.0":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.2.tgz#779ae9d265407daaf7737cb892df5ec2a86e22a0"
-  integrity sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==
+"@lit/reactive-element@^2.1.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.1.1.tgz#0662ac4a43d4898974aef9a6c5cd47b9e331919a"
+  integrity sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==
   dependencies:
-    "@lit-labs/ssr-dom-shim" "^1.1.2"
+    "@lit-labs/ssr-dom-shim" "^1.4.0"
 
 "@mdn/browser-compat-data@^4.0.0":
   version "4.2.1"
@@ -1175,76 +214,20 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@open-wc/building-utils@^2.18.3":
-  version "2.21.1"
-  resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.21.1.tgz#3d6907c2942e76b7287a4cf82d86cadac8d1e41b"
-  integrity sha512-wCyxkvkcA7vRwXJeyrIpRhDbBrVlPGAgYKsuG9n1Pyxt2aypthtZR+1q0+wPkr6h1ZYgJnM9CWQYe72AaAXxvw==
+"@open-wc/dedupe-mixin@^2.0.0":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-2.0.1.tgz#3bcd009c63a4713d8ce4e304a72eb33977e374ac"
+  integrity sha512-+R4VxvceUxHAUJXJQipkkoV9fy10vNo+OnUnGKZnVmcwxMl460KLzytnUM4S35SI073R0yZQp9ra0MbPUwVcEA==
+
+"@open-wc/scoped-elements@^3.0.2":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-3.0.6.tgz#9ba4a316d39f2b29dc188f34a7f5e4e1fc9dded1"
+  integrity sha512-w1ayJaUUmBw8tALtqQ6cBueld+op+bufujzbrOdH0uCTXnSQkONYZzOH+9jyQ8auVgKLqcxZ8oU6SzfqQhQkPg==
   dependencies:
-    "@babel/core" "^7.11.1"
-    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
-    "@webcomponents/shadycss" "^1.10.2"
-    "@webcomponents/webcomponentsjs" "^2.5.0"
-    arrify "^2.0.1"
-    browserslist "^4.16.5"
-    chokidar "^3.4.3"
-    clean-css "^5.3.1"
-    clone "^2.1.2"
-    core-js-bundle "^3.8.1"
-    deepmerge "^4.2.2"
-    es-module-shims "^1.4.1"
-    html-minifier-terser "^5.1.1"
-    lru-cache "^6.0.0"
-    minimatch "^7.4.2"
-    parse5 "^7.1.2"
-    path-is-inside "^1.0.2"
-    regenerator-runtime "^0.13.7"
-    resolve "^1.19.0"
-    rimraf "^3.0.2"
-    shady-css-scoped-element "^0.0.2"
-    systemjs "^6.8.3"
-    terser "^4.8.1"
-    valid-url "^1.0.9"
-    whatwg-fetch "^3.5.0"
-    whatwg-url "^7.1.0"
+    "@open-wc/dedupe-mixin" "^2.0.0"
+    lit "^3.0.0"
 
-"@open-wc/dedupe-mixin@^1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz#b3c58f8699b197bb5e923d624c720e67c9f324d6"
-  integrity sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==
-
-"@open-wc/karma-esm@^3.0.9":
-  version "3.0.9"
-  resolved "https://registry.yarnpkg.com/@open-wc/karma-esm/-/karma-esm-3.0.9.tgz#f2bb5c78f69685289718097b8d9cd742acfb07fe"
-  integrity sha512-GzpL/iHVBskZDmKC7cLYLBYzuSoIng7CxyMxp5Ai4VxMSwCVFG+6j/PKLOXVma+EAwwvmq2L5B9tWVbXR34Peg==
-  dependencies:
-    "@open-wc/building-utils" "^2.18.3"
-    babel-plugin-istanbul "^5.1.4"
-    chokidar "^3.0.0"
-    deepmerge "^4.2.2"
-    es-dev-server "^1.57.8"
-    minimatch "^3.0.4"
-    node-fetch "^2.6.0"
-    polyfills-loader "^1.7.4"
-    portfinder "^1.0.21"
-    request "^2.88.0"
-
-"@open-wc/scoped-elements@^2.2.4":
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.2.4.tgz#081559b62d885ac0ec043546f17f1f680294d500"
-  integrity sha512-12X4F4QGPWcvPbxAiJ4v8wQFCOu+laZHRGfTrkoj+3JzACCtuxHG49YbuqVzQ135QPKCuhP9wA0kpGGEfUegyg==
-  dependencies:
-    "@lit/reactive-element" "^1.0.0 || ^2.0.0"
-    "@open-wc/dedupe-mixin" "^1.4.0"
-
-"@open-wc/semantic-dom-diff@^0.19.9":
-  version "0.19.9"
-  resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.19.9.tgz#fd27659cbace40c6a59078233f4fa14a308a45b1"
-  integrity sha512-iUL0OPA6PeLQVEEJ/gsgkEiwOGgK4E1KS//zTB+u+OAh0NifNTfxDxIHQa7rEGvplaq2b2zztT2yyzOzj+MlAA==
-  dependencies:
-    "@types/chai" "^4.3.1"
-    "@web/test-runner-commands" "^0.6.5"
-
-"@open-wc/semantic-dom-diff@^0.20.0":
+"@open-wc/semantic-dom-diff@^0.20.0", "@open-wc/semantic-dom-diff@^0.20.1":
   version "0.20.1"
   resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz#b1bb78be455bd99fb034d9baadbb959d7d124030"
   integrity sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==
@@ -1252,112 +235,186 @@
     "@types/chai" "^4.3.1"
     "@web/test-runner-commands" "^0.9.0"
 
-"@open-wc/testing-helpers@^2.3.1":
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.3.2.tgz#c2bfa82cedd833608effa2d2367fe9524ddf4434"
-  integrity sha512-uZMGC/C1m5EiwQsff6KMmCW25TYMQlJt4ilAWIjnelWGFg9HPUiLnlFvAas3ESUP+4OXLO8Oft7p4mHvbYvAEQ==
+"@open-wc/testing-helpers@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-3.0.1.tgz#f356690fc3495467503a30a2172288aa8baef6f8"
+  integrity sha512-hyNysSatbgT2FNxHJsS3rGKcLEo6+HwDFu1UQL6jcSQUabp/tj3PyX7UnXL3H5YGv0lJArdYLSnvjLnjn3O2fw==
   dependencies:
-    "@open-wc/scoped-elements" "^2.2.4"
+    "@open-wc/scoped-elements" "^3.0.2"
     lit "^2.0.0 || ^3.0.0"
     lit-html "^2.0.0 || ^3.0.0"
 
-"@open-wc/testing@^3.2.0":
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-3.2.2.tgz#c952f4b20af0d201cc8cc436c2c3cdd338bf8177"
-  integrity sha512-byN4dJTd6ZyI9mWmI4lVj30uiu+rYvQr93g64Pd7UFBdAUgb02DHLj6fkJ1gjxA6LC/MeFd7K7mOZ4+vKrMptw==
+"@open-wc/testing@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-4.0.0.tgz#bfa51210799f86ad1a47c2dece6b9f15dd51bb94"
+  integrity sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==
   dependencies:
     "@esm-bundle/chai" "^4.3.4-fix.0"
     "@open-wc/semantic-dom-diff" "^0.20.0"
-    "@open-wc/testing-helpers" "^2.3.1"
+    "@open-wc/testing-helpers" "^3.0.0"
     "@types/chai-dom" "^1.11.0"
     "@types/sinon-chai" "^3.2.3"
     chai-a11y-axe "^1.5.0"
 
-"@rollup/plugin-node-resolve@^13.0.4":
-  version "13.3.0"
-  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c"
-  integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==
+"@puppeteer/browsers@2.10.7":
+  version "2.10.7"
+  resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.10.7.tgz#a28f622b2da0ee131c9a576d25d8c4e31fc24135"
+  integrity sha512-wHWLkQWBjHtajZeqCB74nsa/X70KheyOhySYBRmVQDJiNj0zjZR/naPCvdWjMhcG1LmjaMV/9WtTo5mpe8qWLw==
   dependencies:
-    "@rollup/pluginutils" "^3.1.0"
-    "@types/resolve" "1.17.1"
+    debug "^4.4.1"
+    extract-zip "^2.0.1"
+    progress "^2.0.3"
+    proxy-agent "^6.5.0"
+    semver "^7.7.2"
+    tar-fs "^3.1.0"
+    yargs "^17.7.2"
+
+"@rollup/plugin-node-resolve@^15.0.1":
+  version "15.3.1"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78"
+  integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==
+  dependencies:
+    "@rollup/pluginutils" "^5.0.1"
+    "@types/resolve" "1.20.2"
     deepmerge "^4.2.2"
-    is-builtin-module "^3.1.0"
     is-module "^1.0.0"
-    resolve "^1.19.0"
+    resolve "^1.22.1"
 
-"@rollup/plugin-node-resolve@^7.1.1":
-  version "7.1.3"
-  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca"
-  integrity sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q==
+"@rollup/pluginutils@^5.0.1":
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz#eac25ca5b0bdda4ba735ddaca5fbf26bd435f602"
+  integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==
   dependencies:
-    "@rollup/pluginutils" "^3.0.8"
-    "@types/resolve" "0.0.8"
-    builtin-modules "^3.1.0"
-    is-module "^1.0.0"
-    resolve "^1.14.2"
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^4.0.2"
 
-"@rollup/pluginutils@^3.0.0", "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
-  integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
-  dependencies:
-    "@types/estree" "0.0.39"
-    estree-walker "^1.0.1"
-    picomatch "^2.2.2"
+"@rollup/rollup-android-arm-eabi@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz#ba432433f5e7b419dba2be407d1d59fea6b8de48"
+  integrity sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==
 
-"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3":
-  version "1.8.6"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9"
-  integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==
+"@rollup/rollup-android-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz#4e05c86e0fb9af6eaf52fc298dcdec577477e35c"
+  integrity sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==
+
+"@rollup/rollup-darwin-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz#788fad425b4129875639e0c14b6441c5f3b69d46"
+  integrity sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==
+
+"@rollup/rollup-darwin-x64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz#d44e05bee55b781d7c2cf535d9f9169787c3599d"
+  integrity sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==
+
+"@rollup/rollup-freebsd-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz#107786b4d604495224c3543bfd2cae33ddf76500"
+  integrity sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==
+
+"@rollup/rollup-freebsd-x64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz#54e105c3da27f31084ca6913fed603627755abde"
+  integrity sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz#725c23e0766b5d9368180bc2c427a51e31d0e147"
+  integrity sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==
+
+"@rollup/rollup-linux-arm-musleabihf@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz#6946b0d2f132f2baf5657945b81565d8abd51cc0"
+  integrity sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==
+
+"@rollup/rollup-linux-arm64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz#83510a6d03e748619241a17f5a879418a963c5ed"
+  integrity sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==
+
+"@rollup/rollup-linux-arm64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz#085b98d44c10908626dd40f26bf924433bbd8471"
+  integrity sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz#13e0a4808e9f7924f2cc8c133603f627c7a00543"
+  integrity sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==
+
+"@rollup/rollup-linux-ppc64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz#aeee4e47fc9ca5d6687e686fea4696202af6b2f4"
+  integrity sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==
+
+"@rollup/rollup-linux-riscv64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz#603e4591643f1d7851a96d096cf7fcd273f7b0e1"
+  integrity sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==
+
+"@rollup/rollup-linux-riscv64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz#f8fd9b01f1888e1816d5a398789d430511286c00"
+  integrity sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==
+
+"@rollup/rollup-linux-s390x-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz#37a1fd372d9b93d2b75b2f37c482ecf52f52849b"
+  integrity sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==
+
+"@rollup/rollup-linux-x64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz#131e66dbf7e71cb2a389acc45319bd4c990e093a"
+  integrity sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==
+
+"@rollup/rollup-linux-x64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz#b7245a5ea57db9679e8bf3032c25a5d2c5f54056"
+  integrity sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==
+
+"@rollup/rollup-win32-arm64-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz#768a128bb5da3c5472c3c56aec77507d28bc7209"
+  integrity sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==
+
+"@rollup/rollup-win32-ia32-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz#ce3f3b2eebe585340631498666718f00983a6a62"
+  integrity sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==
+
+"@rollup/rollup-win32-x64-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz#c2a0e3b81262a7e9dd12ce18b350a97558dd50bc"
+  integrity sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==
+
+"@sinonjs/commons@^3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd"
+  integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==
   dependencies:
     type-detect "4.0.8"
 
-"@sinonjs/commons@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
-  integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
+"@sinonjs/fake-timers@^13.0.5":
+  version "13.0.5"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5"
+  integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==
   dependencies:
-    type-detect "4.0.8"
+    "@sinonjs/commons" "^3.0.1"
 
-"@sinonjs/commons@^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72"
-  integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==
+"@sinonjs/samsam@^8.0.1":
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.3.tgz#eb6ffaef421e1e27783cc9b52567de20cb28072d"
+  integrity sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==
   dependencies:
-    type-detect "4.0.8"
+    "@sinonjs/commons" "^3.0.1"
+    type-detect "^4.1.0"
 
-"@sinonjs/fake-timers@^10.0.2":
-  version "10.3.0"
-  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66"
-  integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==
-  dependencies:
-    "@sinonjs/commons" "^3.0.0"
-
-"@sinonjs/fake-timers@^9.1.2":
-  version "9.1.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c"
-  integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==
-  dependencies:
-    "@sinonjs/commons" "^1.7.0"
-
-"@sinonjs/samsam@^6.1.1":
-  version "6.1.3"
-  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.3.tgz#4e30bcd4700336363302a7d72cbec9b9ab87b104"
-  integrity sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==
-  dependencies:
-    "@sinonjs/commons" "^1.6.0"
-    lodash.get "^4.4.2"
-    type-detect "^4.0.8"
-
-"@sinonjs/text-encoding@^0.7.1":
-  version "0.7.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
-  integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
-
-"@socket.io/component-emitter@~3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
-  integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+"@tootallnate/quickjs-emscripten@^0.23.0":
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
+  integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
 
 "@types/accepts@*":
   version "1.3.7"
@@ -1371,64 +428,14 @@
   resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz#20a899c0d29fba1ddf5c2156a10a2bda75ee6f29"
   integrity sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==
 
-"@types/babel__core@^7.1.3":
-  version "7.20.5"
-  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
-  integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
-  dependencies:
-    "@babel/parser" "^7.20.7"
-    "@babel/types" "^7.20.7"
-    "@types/babel__generator" "*"
-    "@types/babel__template" "*"
-    "@types/babel__traverse" "*"
-
-"@types/babel__generator@*":
-  version "7.6.8"
-  resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab"
-  integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==
-  dependencies:
-    "@babel/types" "^7.0.0"
-
-"@types/babel__template@*":
-  version "7.4.4"
-  resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
-  integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
-  dependencies:
-    "@babel/parser" "^7.1.0"
-    "@babel/types" "^7.0.0"
-
-"@types/babel__traverse@*":
-  version "7.20.5"
-  resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd"
-  integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==
-  dependencies:
-    "@babel/types" "^7.20.7"
-
 "@types/body-parser@*":
-  version "1.19.5"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
-  integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
+  version "1.19.6"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
+  integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==
   dependencies:
     "@types/connect" "*"
     "@types/node" "*"
 
-"@types/browserslist-useragent@^3.0.0":
-  version "3.0.7"
-  resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.7.tgz#4c9783bf3c31aa71f8040dc74eef7d59bc3b0e05"
-  integrity sha512-rVvdB0HoQvHDkS8SPgUv2tKfnf0zKIzBh8oisvnq82R3asgpnF857UTAUJuh+3VXPqMYdZ13VWfdIDUN/1iFmQ==
-
-"@types/browserslist@^4.8.0":
-  version "4.15.0"
-  resolved "https://registry.yarnpkg.com/@types/browserslist/-/browserslist-4.15.0.tgz#ba0265b33003a2581df1fc5f483321a30205f2d2"
-  integrity sha512-h9LyKErRGZqMsHh9bd+FE8yCIal4S0DxKTOeui56VgVXqa66TKiuaIUxCAI7c1O0LjaUzOTcsMyOpO9GetozRA==
-  dependencies:
-    browserslist "*"
-
-"@types/caniuse-api@^3.0.0":
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/@types/caniuse-api/-/caniuse-api-3.0.6.tgz#68902fe2d809f65aa2b23a053f5c071e0044c443"
-  integrity sha512-yMGwHJaqwIEXc3x7EyY3CeS73QG9WeC00w2nZ0/inoRv9DiLIhfvrY6vmXMSKlpRLFxrLcAWJh3JTwYNPl3ihg==
-
 "@types/chai-dom@^1.11.0":
   version "1.11.3"
   resolved "https://registry.yarnpkg.com/@types/chai-dom/-/chai-dom-1.11.3.tgz#1659ace2698cdcd9ed8b2c007876f53e37d9cc89"
@@ -1436,10 +443,17 @@
   dependencies:
     "@types/chai" "*"
 
-"@types/chai@*", "@types/chai@^4.2.12", "@types/chai@^4.3.1":
-  version "4.3.11"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.11.tgz#e95050bf79a932cb7305dd130254ccdf9bde671c"
-  integrity sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==
+"@types/chai@*":
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.2.tgz#6f14cea18180ffc4416bc0fd12be05fdd73bdd6b"
+  integrity sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==
+  dependencies:
+    "@types/deep-eql" "*"
+
+"@types/chai@^4.2.12", "@types/chai@^4.3.1":
+  version "4.3.20"
+  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc"
+  integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==
 
 "@types/co-body@^6.1.0":
   version "6.1.3"
@@ -1454,11 +468,6 @@
   resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.3.tgz#553ce2fd5acf160b448d307649b38ffc60d39639"
   integrity sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==
 
-"@types/command-line-usage@^5.0.1":
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.4.tgz#374e4c62d78fbc5a670a0f36da10235af879a0d5"
-  integrity sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==
-
 "@types/connect@*":
   version "3.4.38"
   resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
@@ -1467,58 +476,44 @@
     "@types/node" "*"
 
 "@types/content-disposition@*":
-  version "0.5.8"
-  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537"
-  integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==
+  version "0.5.9"
+  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.9.tgz#00ca14939432869de829a4ccf6fd380fa9181750"
+  integrity sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==
 
 "@types/convert-source-map@^2.0.0":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-2.0.3.tgz#e586c22ca4af2d670d47d32d7fe365d5c5558695"
   integrity sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==
 
-"@types/cookie@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
-  integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
-
 "@types/cookies@*":
-  version "0.7.10"
-  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.10.tgz#c4881dca4dd913420c488508d192496c46eb4fd0"
-  integrity sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.1.tgz#083924ca1266e34ff240ca4d4fd6732ee5b81886"
+  integrity sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==
   dependencies:
     "@types/connect" "*"
     "@types/express" "*"
     "@types/keygrip" "*"
     "@types/node" "*"
 
-"@types/cors@^2.8.12":
-  version "2.8.17"
-  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
-  integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==
-  dependencies:
-    "@types/node" "*"
-
 "@types/debounce@^1.2.0":
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.4.tgz#cb7e85d9ad5ababfac2f27183e8ac8b576b2abb3"
   integrity sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==
 
-"@types/estree@0.0.39":
-  version "0.0.39"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
-  integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
+"@types/deep-eql@*":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd"
+  integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
 
-"@types/etag@*":
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/@types/etag/-/etag-1.8.3.tgz#0321c878a1ac1069131e4d90deab06db5ea2a0db"
-  integrity sha512-QYHv9Yeh1ZYSMPQOoxY4XC4F1r+xRUiAriB303F4G6uBsT3KKX60DjiogvVv+2VISVDuJhcIzMdbjT+Bm938QQ==
-  dependencies:
-    "@types/node" "*"
+"@types/estree@1.0.8", "@types/estree@^1.0.0":
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+  integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
 
-"@types/express-serve-static-core@^4.17.33":
-  version "4.17.41"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
-  integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
+"@types/express-serve-static-core@^5.0.0":
+  version "5.0.7"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6"
+  integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==
   dependencies:
     "@types/node" "*"
     "@types/qs" "*"
@@ -1526,24 +521,23 @@
     "@types/send" "*"
 
 "@types/express@*":
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
-  integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956"
+  integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==
   dependencies:
     "@types/body-parser" "*"
-    "@types/express-serve-static-core" "^4.17.33"
-    "@types/qs" "*"
+    "@types/express-serve-static-core" "^5.0.0"
     "@types/serve-static" "*"
 
 "@types/http-assert@*":
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf"
-  integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==
+  version "1.5.6"
+  resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.6.tgz#b6b657c38a2350d21ce213139f33b03b2b5fa431"
+  integrity sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==
 
-"@types/http-errors@*":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
-  integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
+"@types/http-errors@*", "@types/http-errors@^2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
+  integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
 
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.3":
   version "2.0.6"
@@ -1576,41 +570,24 @@
   dependencies:
     "@types/koa" "*"
 
-"@types/koa-compress@^2.0.9":
-  version "2.0.9"
-  resolved "https://registry.yarnpkg.com/@types/koa-compress/-/koa-compress-2.0.9.tgz#5d19f7d928f78b451a9afd148863e2b45f51e541"
-  integrity sha512-1Sa9OsbHd2N2N7gLpdIRHe8W99EZbfIR31D7Iisx16XgwZCnWUtGXzXQejhu74Y1pE/wILqBP6VL49ch/MVpZw==
+"@types/koa@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-3.0.0.tgz#5df2b3b30da902f01b6ec78ef56403e6eb62fa6e"
+  integrity sha512-MOcVYdVYmkSutVHZZPh8j3+dAjLyR5Tl59CN0eKgpkE1h/LBSmPAsQQuWs+bKu7WtGNn+hKfJH9Gzml+PulmDg==
   dependencies:
-    "@types/koa" "*"
+    "@types/accepts" "*"
+    "@types/content-disposition" "*"
+    "@types/cookies" "*"
+    "@types/http-assert" "*"
+    "@types/http-errors" "^2"
+    "@types/keygrip" "*"
+    "@types/koa-compose" "*"
     "@types/node" "*"
 
-"@types/koa-etag@^3.0.0":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@types/koa-etag/-/koa-etag-3.0.3.tgz#67adf67b18057e4ecdc65efd705a979ba638d521"
-  integrity sha512-pOtRwgJOyP5XWoKUQpvvGbihep7ZoCQwJiFhZ2PdAjmXhgDFm5bbX9vYbXCwLIWG+WUb8CsOCf+78uae0Ho5sg==
-  dependencies:
-    "@types/etag" "*"
-    "@types/koa" "*"
-
-"@types/koa-send@*":
-  version "4.1.6"
-  resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.6.tgz#15d90e95e3ccce669a15b6a3c56c3a650a167cea"
-  integrity sha512-vgnNGoOJkx7FrF0Jl6rbK1f8bBecqAchKpXtKuXzqIEdXTDO6dsSTjr+eZ5m7ltSjH4K/E7auNJEQCAd0McUPA==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-static@^4.0.1":
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/@types/koa-static/-/koa-static-4.0.4.tgz#ce6f2a5d14cc7ef19f9bf6ee8e4f3eadfcc77323"
-  integrity sha512-j1AUzzl7eJYEk9g01hNTlhmipFh8RFbOQmaMNLvLcNNAkPw0bdTs3XTa3V045XFlrWN0QYnblbDJv2RzawTn6A==
-  dependencies:
-    "@types/koa" "*"
-    "@types/koa-send" "*"
-
-"@types/koa@*", "@types/koa@^2.0.48", "@types/koa@^2.11.6":
-  version "2.13.12"
-  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.12.tgz#70d87a9061a81909e0ee11ca50168416e8d3e795"
-  integrity sha512-vAo1KuDSYWFDB4Cs80CHvfmzSQWeUb909aQib0C0aFx4sw0K9UZFz2m5jaEP+b3X1+yr904iQiruS0hXi31jbw==
+"@types/koa@^2.11.6":
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.15.0.tgz#eca43d76f527c803b491731f95df575636e7b6f2"
+  integrity sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==
   dependencies:
     "@types/accepts" "*"
     "@types/content-disposition" "*"
@@ -1621,38 +598,11 @@
     "@types/koa-compose" "*"
     "@types/node" "*"
 
-"@types/koa__cors@^3.0.1":
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.3.1.tgz#0ec7543c4c620fd23451bfdd3e21b9a6aadedccd"
-  integrity sha512-aFGYhTFW7651KhmZZ05VG0QZJre7QxBxDj2LF1lf6GA/wSXEfKVAJxiQQWzRV4ZoMzQIO8vJBXKsUcRuvYK9qw==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/lru-cache@^5.1.0":
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef"
-  integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
-
-"@types/mime-types@^2.1.0":
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2"
-  integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==
-
-"@types/mime@*":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
-  integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
-
 "@types/mime@^1":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
   integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
 
-"@types/minimatch@^3.0.3":
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
-  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
-
 "@types/mkdirp@^1.0.1":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.2.tgz#8d0bad7aa793abe551860be1f7ae7f3198c16666"
@@ -1660,28 +610,23 @@
   dependencies:
     "@types/node" "*"
 
-"@types/mocha@^8.2.0":
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
-  integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
+"@types/mocha@^10.0.10":
+  version "10.0.10"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.10.tgz#91f62905e8d23cbd66225312f239454a23bebfa0"
+  integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==
 
-"@types/node@*", "@types/node@>=10.0.0":
-  version "20.10.6"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
-  integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
+"@types/node@*":
+  version "24.3.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec"
+  integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
   dependencies:
-    undici-types "~5.26.4"
+    undici-types "~7.10.0"
 
 "@types/parse5@^6.0.1":
   version "6.0.3"
   resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
   integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
 
-"@types/path-is-inside@^1.0.0":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.3.tgz#bda44a673de87bae62943ed617d0a246f7a034d0"
-  integrity sha512-xZoKJ7TQYIBc/ry4CHIV3M4V96zLMdTIGPT7Du+yYWevnfoaiW5bEPpkCL1RuEySw7k+JnlL1VcLZfyOg6Sp5g==
-
 "@types/pixelmatch@^5.2.2":
   version "5.2.6"
   resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.6.tgz#fba6de304ac958495f27d85989f5c6bb7499a686"
@@ -1690,52 +635,43 @@
     "@types/node" "*"
 
 "@types/pngjs@^6.0.0":
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.4.tgz#9a457aebabd944efde1a773a0fa1d74933e8021b"
-  integrity sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.5.tgz#6dec2f7eb8284543ca4e423f3c09b119fa939ea3"
+  integrity sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==
   dependencies:
     "@types/node" "*"
 
 "@types/qs@*":
-  version "6.9.11"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda"
-  integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==
+  version "6.14.0"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
+  integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==
 
 "@types/range-parser@*":
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
   integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
 
-"@types/resolve@0.0.8":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
-  integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/resolve@1.17.1":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
-  integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
-  dependencies:
-    "@types/node" "*"
+"@types/resolve@1.20.2":
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
+  integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
 
 "@types/send@*":
-  version "0.17.4"
-  resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
-  integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
+  version "0.17.5"
+  resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74"
+  integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==
   dependencies:
     "@types/mime" "^1"
     "@types/node" "*"
 
 "@types/serve-static@*":
-  version "1.15.5"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
-  integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
+  version "1.15.8"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877"
+  integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==
   dependencies:
     "@types/http-errors" "*"
-    "@types/mime" "*"
     "@types/node" "*"
+    "@types/send" "*"
 
 "@types/sinon-chai@^3.2.3":
   version "3.2.12"
@@ -1745,17 +681,10 @@
     "@types/chai" "*"
     "@types/sinon" "*"
 
-"@types/sinon@*":
-  version "17.0.2"
-  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.2.tgz#9a769f67e62b45b7233f1fe01cb1f231d2393e1c"
-  integrity sha512-Zt6heIGsdqERkxctIpvN5Pv3edgBrhoeb3yHyxffd4InN0AX2SVNKSrhdDZKGQICVOxWP/q4DyhpfPNMSrpIiA==
-  dependencies:
-    "@types/sinonjs__fake-timers" "*"
-
-"@types/sinon@^10.0.16":
-  version "10.0.20"
-  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.20.tgz#f1585debf4c0d99f9938f4111e5479fb74865146"
-  integrity sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==
+"@types/sinon@*", "@types/sinon@^17.0.4":
+  version "17.0.4"
+  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.4.tgz#fd9a3e8e07eea1a3f4a6f82a972c899e5778f369"
+  integrity sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==
   dependencies:
     "@types/sinonjs__fake-timers" "*"
 
@@ -1769,13 +698,6 @@
   resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
   integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
 
-"@types/whatwg-url@^6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"
-  integrity sha512-tonhlcbQ2eho09am6RHnHOgvtDfDYINd5rgxD+2YSkKENooVCFsWizJz139MQW/PV8FfClyKrNe9ZbdHrSCxGg==
-  dependencies:
-    "@types/node" "*"
-
 "@types/ws@^7.4.0":
   version "7.4.7"
   resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
@@ -1790,65 +712,27 @@
   dependencies:
     "@types/node" "*"
 
-"@ungap/promise-all-settled@1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
-  integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
-
-"@web/browser-logs@^0.2.2", "@web/browser-logs@^0.2.6":
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.2.6.tgz#ec936f78c7cf7b0ef9fb990c0097a3da1a756b20"
-  integrity sha512-CNjNVhd4FplRY8PPWIAt02vAowJAVcOoTNrR/NNb/o9pka7yI9qdjpWrWhEbPr2pOXonWb52AeAgdK66B8ZH7w==
-  dependencies:
-    errorstacks "^2.2.0"
-
 "@web/browser-logs@^0.4.0":
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.4.0.tgz#8c4adddac46be02dff1a605312132053b3737d0a"
-  integrity sha512-/EBiDAUCJ2DzZhaFxTPRIznEPeafdLbXShIL6aTu7x73x7ZoxSDv7DGuTsh2rWNMUa4+AKli4UORrpyv6QBOiA==
-  dependencies:
-    errorstacks "^2.2.0"
-
-"@web/config-loader@^0.1.3":
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.1.3.tgz#8325ea54f75ef2ee7166783e64e66936db25bff7"
-  integrity sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==
-  dependencies:
-    semver "^7.3.4"
-
-"@web/dev-server-core@^0.4.1":
   version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.4.1.tgz#803faff45281ee296d0dda02dfdd905c330db4d8"
-  integrity sha512-KdYwejXZwIZvb6tYMCqU7yBiEOPfKLQ3V9ezqqEz8DA9V9R3oQWaowckvCpFB9IxxPfS/P8/59OkdzGKQjcIUw==
+  resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.4.1.tgz#1be4464c9b3dca537b459d49cb1873a6deef0609"
+  integrity sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==
   dependencies:
-    "@types/koa" "^2.11.6"
-    "@types/ws" "^7.4.0"
-    "@web/parse5-utils" "^1.3.1"
-    chokidar "^3.4.3"
-    clone "^2.1.2"
-    es-module-lexer "^1.0.0"
-    get-stream "^6.0.0"
-    is-stream "^2.0.0"
-    isbinaryfile "^5.0.0"
-    koa "^2.13.0"
-    koa-etag "^4.0.0"
-    koa-send "^5.0.1"
-    koa-static "^5.0.0"
-    lru-cache "^6.0.0"
-    mime-types "^2.1.27"
-    parse5 "^6.0.1"
-    picomatch "^2.2.2"
-    ws "^7.4.2"
+    errorstacks "^2.4.1"
 
-"@web/dev-server-core@^0.7.0":
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.7.0.tgz#ffe71dd272ecb73a2b0c1ee23f3fad812780b998"
-  integrity sha512-1FJe6cJ3r0x0ZmxY/FnXVduQD4lKX7QgYhyS6N+VmIpV+tBU4sGRbcrmeoYeY+nlnPa6p2oNuonk3X5ln/W95g==
+"@web/config-loader@^0.3.0":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.3.3.tgz#13d4852c479d1f3adbc1f24d79c7e6eeedeb6e5d"
+  integrity sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==
+
+"@web/dev-server-core@^0.7.2", "@web/dev-server-core@^0.7.3", "@web/dev-server-core@^0.7.4":
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.7.5.tgz#b283d46eb2c0384e848311ec9da25d06bbbc47df"
+  integrity sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==
   dependencies:
     "@types/koa" "^2.11.6"
     "@types/ws" "^7.4.0"
     "@web/parse5-utils" "^2.1.0"
-    chokidar "^3.4.3"
+    chokidar "^4.0.1"
     clone "^2.1.2"
     es-module-lexer "^1.0.0"
     get-stream "^6.0.0"
@@ -1862,59 +746,51 @@
     mime-types "^2.1.27"
     parse5 "^6.0.1"
     picomatch "^2.2.2"
-    ws "^7.4.2"
+    ws "^7.5.10"
 
-"@web/dev-server-esbuild@^0.3.6":
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-0.3.6.tgz#838100894937443b96bfc4266c7795d27ed4afac"
-  integrity sha512-VDcZOzvmbg/z/8Q54hHqFwt9U4cacQJZxgS8YXAvyFuG85HAJ/Q55P7Tr++1NlRS8wQEos6QK2ERUWNjEVOhqQ==
+"@web/dev-server-esbuild@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.4.tgz#7ac39eb4350da39fb3f7c5ce5c0fd423a8511346"
+  integrity sha512-ia1LxBwwRiQBYhJ7/RtLenHyPjzle3SvTw3jOZaeGv8UGXVPOkQV8fR05caOtW/DPPZaZovNAybzRKVnNiYIZg==
   dependencies:
     "@mdn/browser-compat-data" "^4.0.0"
-    "@web/dev-server-core" "^0.4.1"
-    esbuild "^0.16 || ^0.17"
+    "@web/dev-server-core" "^0.7.4"
+    esbuild "^0.25.0"
     parse5 "^6.0.1"
     ua-parser-js "^1.0.33"
 
-"@web/dev-server-rollup@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.4.1.tgz#3c6606bac8e497498b5b47bf9e0c544c321b38ef"
-  integrity sha512-Ebsv7Ovd9MufeH3exvikBJ7GmrZA5OmHnOgaiHcwMJ2eQBJA5/I+/CbRjsLX97ICj/ZwZG//p2ITRz8W3UfSqg==
+"@web/dev-server-rollup@^0.6.1":
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz#d0a4f69e4a659d2b79f172e86236cea6c872e81c"
+  integrity sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==
   dependencies:
-    "@rollup/plugin-node-resolve" "^13.0.4"
-    "@web/dev-server-core" "^0.4.1"
+    "@rollup/plugin-node-resolve" "^15.0.1"
+    "@web/dev-server-core" "^0.7.2"
     nanocolors "^0.2.1"
     parse5 "^6.0.1"
-    rollup "^2.67.0"
-    whatwg-url "^11.0.0"
+    rollup "^4.4.0"
+    whatwg-url "^14.0.0"
 
-"@web/dev-server@^0.1.35":
-  version "0.1.38"
-  resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.1.38.tgz#d755092d66aeb923c546237a6c460439ea3ddd29"
-  integrity sha512-WUq7Zi8KeJ5/UZmmpZ+kzUpUlFlMP/rcreJKYg9Lxiz998KYl4G5Rv24akX0piTuqXG7r6h+zszg8V/hdzjCoA==
+"@web/dev-server@^0.4.0":
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.4.6.tgz#18a4421d474a9bd1bfc90419a7a798ed0b5538c5"
+  integrity sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==
   dependencies:
     "@babel/code-frame" "^7.12.11"
     "@types/command-line-args" "^5.0.0"
-    "@web/config-loader" "^0.1.3"
-    "@web/dev-server-core" "^0.4.1"
-    "@web/dev-server-rollup" "^0.4.1"
+    "@web/config-loader" "^0.3.0"
+    "@web/dev-server-core" "^0.7.2"
+    "@web/dev-server-rollup" "^0.6.1"
     camelcase "^6.2.0"
     command-line-args "^5.1.1"
     command-line-usage "^7.0.1"
     debounce "^1.2.0"
     deepmerge "^4.2.2"
-    ip "^1.1.5"
+    internal-ip "^6.2.0"
     nanocolors "^0.2.1"
     open "^8.0.2"
     portfinder "^1.0.32"
 
-"@web/parse5-utils@^1.3.1":
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-1.3.1.tgz#6727be4d7875a9ecb96a5b3003bd271da763f8b4"
-  integrity sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==
-  dependencies:
-    "@types/parse5" "^6.0.1"
-    parse5 "^6.0.1"
-
 "@web/parse5-utils@^2.1.0":
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.1.0.tgz#3d33aca62c66773492f2fba89d23a45f8b57ba4a"
@@ -1923,23 +799,15 @@
     "@types/parse5" "^6.0.1"
     parse5 "^6.0.1"
 
-"@web/test-runner-chrome@^0.10.7":
-  version "0.10.7"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.10.7.tgz#2dc35da47aa8b98c59f9e229a70ea3f443303e0c"
-  integrity sha512-DKJVHhHh3e/b6/erfKOy0a4kGfZ47qMoQRgROxi9T4F9lavEY3E5/MQ7hapHFM2lBF4vDrm+EWjtBdOL8o42tw==
+"@web/test-runner-chrome@^0.18.1":
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.18.1.tgz#490a371990084b8d050dd778bd58e47d21522332"
+  integrity sha512-eO6ctCaqSguGM6G3cFobGHnrEs9wlv9Juj/Akyr4XLjeEMTheNULdvOXw9Bygi+QC/ir/0snMmt+/YKnfy8rYA==
   dependencies:
-    "@web/test-runner-core" "^0.10.20"
-    "@web/test-runner-coverage-v8" "^0.4.8"
+    "@web/test-runner-core" "^0.13.0"
+    "@web/test-runner-coverage-v8" "^0.8.0"
     chrome-launcher "^0.15.0"
-    puppeteer-core "^13.1.3"
-
-"@web/test-runner-commands@^0.6.3", "@web/test-runner-commands@^0.6.5", "@web/test-runner-commands@^0.6.6":
-  version "0.6.6"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.6.6.tgz#e0e8c4ce6dcd91e5b18cf2212511ee6108e31070"
-  integrity sha512-2DcK/+7f8QTicQpGFq/TmvKHDK/6Zald6rn1zqRlmj3pcH8fX6KHNVMU60Za9QgAKdorMBPfd8dJwWba5otzdw==
-  dependencies:
-    "@web/test-runner-core" "^0.10.29"
-    mkdirp "^1.0.4"
+    puppeteer-core "^24.0.0"
 
 "@web/test-runner-commands@^0.9.0":
   version "0.9.0"
@@ -1949,42 +817,10 @@
     "@web/test-runner-core" "^0.13.0"
     mkdirp "^1.0.4"
 
-"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.27", "@web/test-runner-core@^0.10.29":
-  version "0.10.29"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.10.29.tgz#d8d909c849151cf19013d6084f89a31e400557d5"
-  integrity sha512-0/ZALYaycEWswHhpyvl5yqo0uIfCmZe8q14nGPi1dMmNiqLcHjyFGnuIiLexI224AW74ljHcHllmDlXK9FUKGA==
-  dependencies:
-    "@babel/code-frame" "^7.12.11"
-    "@types/babel__code-frame" "^7.0.2"
-    "@types/co-body" "^6.1.0"
-    "@types/convert-source-map" "^2.0.0"
-    "@types/debounce" "^1.2.0"
-    "@types/istanbul-lib-coverage" "^2.0.3"
-    "@types/istanbul-reports" "^3.0.0"
-    "@web/browser-logs" "^0.2.6"
-    "@web/dev-server-core" "^0.4.1"
-    chokidar "^3.4.3"
-    cli-cursor "^3.1.0"
-    co-body "^6.1.0"
-    convert-source-map "^2.0.0"
-    debounce "^1.2.0"
-    dependency-graph "^0.11.0"
-    globby "^11.0.1"
-    ip "^1.1.5"
-    istanbul-lib-coverage "^3.0.0"
-    istanbul-lib-report "^3.0.0"
-    istanbul-reports "^3.0.2"
-    log-update "^4.0.0"
-    nanocolors "^0.2.1"
-    nanoid "^3.1.25"
-    open "^8.0.2"
-    picomatch "^2.2.2"
-    source-map "^0.7.3"
-
 "@web/test-runner-core@^0.13.0":
-  version "0.13.0"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.13.0.tgz#a3799461002fcb969b0baa100d88be6c1ff504f4"
-  integrity sha512-mUrETPg9n4dHWEk+D46BU3xVhQf+ljT4cG7FSpmF7AIOsXWgWHoaXp6ReeVcEmM5fmznXec2O/apTb9hpGrP3w==
+  version "0.13.4"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.13.4.tgz#df6a76b3e970adbbd3ba39e870f4bf657d6dc43f"
+  integrity sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==
   dependencies:
     "@babel/code-frame" "^7.12.11"
     "@types/babel__code-frame" "^7.0.2"
@@ -1994,15 +830,15 @@
     "@types/istanbul-lib-coverage" "^2.0.3"
     "@types/istanbul-reports" "^3.0.0"
     "@web/browser-logs" "^0.4.0"
-    "@web/dev-server-core" "^0.7.0"
-    chokidar "^3.4.3"
+    "@web/dev-server-core" "^0.7.3"
+    chokidar "^4.0.1"
     cli-cursor "^3.1.0"
     co-body "^6.1.0"
     convert-source-map "^2.0.0"
     debounce "^1.2.0"
     dependency-graph "^0.11.0"
     globby "^11.0.1"
-    ip "^1.1.5"
+    internal-ip "^6.2.0"
     istanbul-lib-coverage "^3.0.0"
     istanbul-lib-report "^3.0.1"
     istanbul-reports "^3.0.2"
@@ -2013,95 +849,70 @@
     picomatch "^2.2.2"
     source-map "^0.7.3"
 
-"@web/test-runner-coverage-v8@^0.4.8":
-  version "0.4.9"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.4.9.tgz#334d80cd19fc68c08ec3339b1b1d2725078b51a2"
-  integrity sha512-y9LWL4uY25+fKQTljwr0XTYjeWIwU4h8eYidVuLoW3n1CdFkaddv+smrGzzF5j8XY+Mp6TmV9NdxjvMWqVkDdw==
+"@web/test-runner-coverage-v8@^0.8.0":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz#783e9f685f14cafc34a6bf323f7d9268c1477933"
+  integrity sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==
   dependencies:
-    "@web/test-runner-core" "^0.10.20"
+    "@web/test-runner-core" "^0.13.0"
     istanbul-lib-coverage "^3.0.0"
-    picomatch "^2.2.2"
-    v8-to-istanbul "^8.0.0"
-
-"@web/test-runner-coverage-v8@^0.5.0":
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.5.0.tgz#d1b033fd4baddaf5636a41cd017e321a338727a6"
-  integrity sha512-4eZs5K4JG7zqWEhVSO8utlscjbVScV7K6JVwoWWcObFTGAaBMbDVzwGRimyNSzvmfTdIO/Arze4CeUUfCl4iLQ==
-  dependencies:
-    "@web/test-runner-core" "^0.10.20"
-    istanbul-lib-coverage "^3.0.0"
+    lru-cache "^8.0.4"
     picomatch "^2.2.2"
     v8-to-istanbul "^9.0.1"
 
-"@web/test-runner-mocha@^0.7.5":
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-mocha/-/test-runner-mocha-0.7.5.tgz#696f8cb7f5118a72bd7ac5778367ae3bd3fb92cd"
-  integrity sha512-12/OBq6efPCAvJpcz3XJs2OO5nHe7GtBibZ8Il1a0QtsGpRmuJ4/m1EF0Fj9f6KHg7JdpGo18A37oE+5hXjHwg==
-  dependencies:
-    "@types/mocha" "^8.2.0"
-    "@web/test-runner-core" "^0.10.20"
-
-"@web/test-runner-playwright@^0.9.0":
+"@web/test-runner-mocha@^0.9.0":
   version "0.9.0"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-playwright/-/test-runner-playwright-0.9.0.tgz#c13b71ecfe763ae5d15dff586a35a9840c238b1f"
-  integrity sha512-RhWkz1CY3KThHoX89yZ/gz9wDSPujxd2wMWNxqhov4y/XDI+0TS44TWKBfWXnuvlQFZPi8JFT7KibCo3pb/Mcg==
+  resolved "https://registry.yarnpkg.com/@web/test-runner-mocha/-/test-runner-mocha-0.9.0.tgz#4fbfa5c3222c8c787fdcc057dd932a0763267b10"
+  integrity sha512-ZL9F6FXd0DBQvo/h/+mSfzFTSRVxzV9st/AHhpgABtUtV/AIpVE9to6+xdkpu6827kwjezdpuadPfg+PlrBWqQ==
   dependencies:
-    "@web/test-runner-core" "^0.10.20"
-    "@web/test-runner-coverage-v8" "^0.5.0"
-    playwright "^1.22.2"
+    "@web/test-runner-core" "^0.13.0"
 
-"@web/test-runner-visual-regression@^0.7.1":
-  version "0.7.1"
-  resolved "https://registry.yarnpkg.com/@web/test-runner-visual-regression/-/test-runner-visual-regression-0.7.1.tgz#e9e153c45922ce56cf5458d20a637642fbe7e8a1"
-  integrity sha512-Pl+uyT1gmQ+vPMyJTgB8HRUyZV83PfGtopPGpsps4heGgQSP6H3XT6wvYtx0sprudFu1a8XG130XoVmK/FyRPA==
+"@web/test-runner-playwright@^0.11.1":
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-playwright/-/test-runner-playwright-0.11.1.tgz#d993112ae2126eb74c1c5a171d6ea44c2dd24b4e"
+  integrity sha512-l9tmX0LtBqMaKAApS4WshpB87A/M8sOHZyfCobSGuYqnREgz5rqQpX314yx+4fwHXLLTa5N64mTrawsYkLjliw==
+  dependencies:
+    "@web/test-runner-core" "^0.13.0"
+    "@web/test-runner-coverage-v8" "^0.8.0"
+    playwright "^1.53.0"
+
+"@web/test-runner-visual-regression@^0.10.0":
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/@web/test-runner-visual-regression/-/test-runner-visual-regression-0.10.0.tgz#b20e17f0427a8ce0db18c69e34e8d730336b4a85"
+  integrity sha512-fz+OPvPXpGlQdkyEKeIRr1t1/hIWb11JmmFUZWKgrifvheuLlLGko0VwOxIOQ7Ht11RcepsMXl4PzTuxkPv6rA==
   dependencies:
     "@types/mkdirp" "^1.0.1"
     "@types/pixelmatch" "^5.2.2"
     "@types/pngjs" "^6.0.0"
-    "@web/test-runner-commands" "^0.6.6"
-    "@web/test-runner-core" "^0.10.29"
+    "@web/test-runner-commands" "^0.9.0"
+    "@web/test-runner-core" "^0.13.0"
     mkdirp "^1.0.4"
     pixelmatch "^5.2.1"
-    pngjs "^6.0.0"
+    pngjs "^7.0.0"
 
-"@web/test-runner@^0.14.0":
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.14.1.tgz#a637e45c9b6ce7860ab780b5ac82dbfa1ed824f9"
-  integrity sha512-S2/Xp/bZBJdbWeTQxhs45cO9Khwqx99X+rrx8l0uDR0Ju/+kX+yC3RpjnOY1ooKD3rjkoEAE82soZTZNz+aKIg==
+"@web/test-runner@^0.20.2":
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.20.2.tgz#3045de8f14eb9f9b8aa51aa7fd9849dd95bb0a93"
+  integrity sha512-zfEGYEDnS0EI8qgoWFjmtkIXhqP15W40NW3dCaKtbxj5eU0a7E53f3GV/tZGD0GlZKF8d4Fyw+AFrwOJU9Z4GA==
   dependencies:
-    "@web/browser-logs" "^0.2.2"
-    "@web/config-loader" "^0.1.3"
-    "@web/dev-server" "^0.1.35"
-    "@web/test-runner-chrome" "^0.10.7"
-    "@web/test-runner-commands" "^0.6.3"
-    "@web/test-runner-core" "^0.10.27"
-    "@web/test-runner-mocha" "^0.7.5"
+    "@web/browser-logs" "^0.4.0"
+    "@web/config-loader" "^0.3.0"
+    "@web/dev-server" "^0.4.0"
+    "@web/test-runner-chrome" "^0.18.1"
+    "@web/test-runner-commands" "^0.9.0"
+    "@web/test-runner-core" "^0.13.0"
+    "@web/test-runner-mocha" "^0.9.0"
     camelcase "^6.2.0"
     command-line-args "^5.1.1"
-    command-line-usage "^6.1.1"
-    convert-source-map "^1.7.0"
+    command-line-usage "^7.0.1"
+    convert-source-map "^2.0.0"
     diff "^5.0.0"
     globby "^11.0.1"
     nanocolors "^0.2.1"
     portfinder "^1.0.32"
     source-map "^0.7.3"
 
-"@webcomponents/shadycss@^1.10.2":
-  version "1.11.2"
-  resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.2.tgz#7539b0ad29598aa2eafee8b341059e20ac9e1006"
-  integrity sha512-vRq+GniJAYSBmTRnhCYPAPq6THYqovJ/gzGThWbgEZUQaBccndGTi1hdiUP15HzEco0I6t4RCtXyX0rsSmwgPw==
-
-"@webcomponents/webcomponentsjs@^2.4.0", "@webcomponents/webcomponentsjs@^2.5.0":
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz#ab21f027594fa827c1889e8b646da7be27c7908a"
-  integrity sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==
-
-abortcontroller-polyfill@^1.4.0:
-  version "1.7.5"
-  resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed"
-  integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==
-
-accepts@^1.3.5, accepts@~1.3.4:
+accepts@^1.3.5:
   version "1.3.8"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
   integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
@@ -2114,27 +925,15 @@
   resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
   integrity sha512-ltexLD/Bzwr1tDskQQFi88L4akbn8zFLIFIc00vFkH3G4hNEHruuJVcJuJTeUXLxms9dSon+cHSCmfFThnowFQ==
 
-agent-base@6:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
-  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
-  dependencies:
-    debug "4"
+agent-base@^7.1.0, agent-base@^7.1.2:
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
+  integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
 
-ajv@^6.12.3:
-  version "6.12.6"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
-  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
-  dependencies:
-    fast-deep-equal "^3.1.1"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.4.1"
-    uri-js "^4.2.2"
-
-ansi-colors@4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
-  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+ansi-colors@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
+  integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
 
 ansi-escapes@^4.3.0:
   version "4.3.2"
@@ -2143,28 +942,11 @@
   dependencies:
     type-fest "^0.21.3"
 
-ansi-regex@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
-  integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
-
-ansi-regex@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
-  integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==
-
 ansi-regex@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
-ansi-styles@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
-  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
-  dependencies:
-    color-convert "^1.9.0"
-
 ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
@@ -2172,11 +954,6 @@
   dependencies:
     color-convert "^2.0.1"
 
-any-promise@^1.0.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
-  integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
-
 anymatch@~3.1.2:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
@@ -2195,11 +972,6 @@
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
   integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
 
-array-back@^4.0.1, array-back@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e"
-  integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==
-
 array-back@^6.2.2:
   version "6.2.2"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157"
@@ -2210,191 +982,100 @@
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
   integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
 
-arrify@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
-  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
-
-asn1@~0.2.3:
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
-  integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
+ast-types@^0.13.4:
+  version "0.13.4"
+  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782"
+  integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==
   dependencies:
-    safer-buffer "~2.1.0"
-
-assert-plus@1.0.0, assert-plus@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
-  integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
+    tslib "^2.0.1"
 
 astral-regex@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
-async@^2.6.4:
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
-  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
-  dependencies:
-    lodash "^4.17.14"
-
-asynckit@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
-  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
-
-aws-sign2@~0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
-  integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==
-
-aws4@^1.8.0:
-  version "1.12.0"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
-  integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
+async@^3.2.6:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+  integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
 
 axe-core@^4.3.3:
-  version "4.8.3"
-  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.3.tgz#205df863dd9917d5979e9435dab4d47692759051"
-  integrity sha512-d5ZQHPSPkF9Tw+yfyDcRoUOc4g/8UloJJe5J8m4L5+c7AtDdjDLRxew/knnI4CxvtdxEUVgWz4x3OIQUIFiMfw==
+  version "4.10.3"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc"
+  integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==
 
-babel-plugin-istanbul@^5.1.4:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
-  integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.0.0"
-    find-up "^3.0.0"
-    istanbul-lib-instrument "^3.3.0"
-    test-exclude "^5.2.3"
-
-babel-plugin-polyfill-corejs2@^0.4.7:
-  version "0.4.7"
-  resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz#679d1b94bf3360f7682e11f2cb2708828a24fe8c"
-  integrity sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==
-  dependencies:
-    "@babel/compat-data" "^7.22.6"
-    "@babel/helper-define-polyfill-provider" "^0.4.4"
-    semver "^6.3.1"
-
-babel-plugin-polyfill-corejs3@^0.8.7:
-  version "0.8.7"
-  resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04"
-  integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==
-  dependencies:
-    "@babel/helper-define-polyfill-provider" "^0.4.4"
-    core-js-compat "^3.33.1"
-
-babel-plugin-polyfill-regenerator@^0.5.4:
-  version "0.5.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz#c6fc8eab610d3a11eb475391e52584bacfc020f4"
-  integrity sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==
-  dependencies:
-    "@babel/helper-define-polyfill-provider" "^0.4.4"
+b4a@^1.6.4:
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4"
+  integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==
 
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base64-js@^1.3.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
-  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+bare-events@^2.2.0, bare-events@^2.5.4:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.6.1.tgz#f793b28bdc3dcf147d7cf01f882a6f0b12ccc4a2"
+  integrity sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==
 
-base64id@2.0.0, base64id@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
-  integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-
-bcrypt-pbkdf@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
-  integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
+bare-fs@^4.0.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.2.1.tgz#bd9c2b02a07b9206ccba97f6ea9ce9658582f119"
+  integrity sha512-mELROzV0IhqilFgsl1gyp48pnZsaV9xhQapHLDsvn4d4ZTfbFhcghQezl7FTEDNBcGqLUnNI3lUlm6ecrLWdFA==
   dependencies:
-    tweetnacl "^0.14.3"
+    bare-events "^2.5.4"
+    bare-path "^3.0.0"
+    bare-stream "^2.6.4"
+
+bare-os@^3.0.1:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.2.tgz#b3c4f5ad5e322c0fd0f3c29fc97d19009e2796e5"
+  integrity sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==
+
+bare-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-3.0.0.tgz#b59d18130ba52a6af9276db3e96a2e3d3ea52178"
+  integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==
+  dependencies:
+    bare-os "^3.0.1"
+
+bare-stream@^2.6.4:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.7.0.tgz#5b9e7dd0a354d06e82d6460c426728536c35d789"
+  integrity sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==
+  dependencies:
+    streamx "^2.21.0"
+
+basic-ftp@^5.0.2:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
+  integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
 
 binary-extensions@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
-  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-bl@^4.0.3:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
-  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
-  dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
-
-body-parser@^1.19.0:
-  version "1.20.2"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
-  integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
-  dependencies:
-    bytes "3.1.2"
-    content-type "~1.0.5"
-    debug "2.6.9"
-    depd "2.0.0"
-    destroy "1.2.0"
-    http-errors "2.0.0"
-    iconv-lite "0.4.24"
-    on-finished "2.4.1"
-    qs "6.11.0"
-    raw-body "2.5.2"
-    type-is "~1.6.18"
-    unpipe "1.0.0"
-
-brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
-  dependencies:
-    balanced-match "^1.0.0"
-    concat-map "0.0.1"
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
+  integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
 
 brace-expansion@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
-  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
+  integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
   dependencies:
     balanced-match "^1.0.0"
 
-braces@^3.0.2, braces@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
-  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+braces@^3.0.3, braces@~3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
   dependencies:
-    fill-range "^7.0.1"
+    fill-range "^7.1.1"
 
-browser-stdout@1.3.1:
+browser-stdout@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
   integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
 
-browserslist-useragent@^3.0.2:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.1.4.tgz#b2cb15a3a46d5c535f96335c4b8d97f94c3657a9"
-  integrity sha512-o9V55790uae98Kwn+vwyO+ww07OreiH1BUc9bjjlUbIL3Fh43fyoasZxZ2EiI4ErfEIKwbycQ1pvwOBlySJ7ow==
-  dependencies:
-    browserslist "^4.19.1"
-    electron-to-chromium "^1.4.67"
-    semver "^7.3.5"
-    useragent "^2.3.0"
-    yamlparser "^0.0.2"
-
-browserslist@*, browserslist@^4.0.0, browserslist@^4.16.5, browserslist@^4.19.1, browserslist@^4.22.2, browserslist@^4.9.1:
-  version "4.22.2"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b"
-  integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==
-  dependencies:
-    caniuse-lite "^1.0.30001565"
-    electron-to-chromium "^1.4.601"
-    node-releases "^2.0.14"
-    update-browserslist-db "^1.0.13"
-
 buffer-crc32@~0.2.3:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -2405,20 +1086,7 @@
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
-buffer@^5.2.1, buffer@^5.5.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
-  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.1.13"
-
-builtin-modules@^3.1.0, builtin-modules@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
-  integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
-
-bytes@3.1.2, bytes@^3.0.0:
+bytes@3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
   integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
@@ -2431,53 +1099,27 @@
     mime-types "^2.1.18"
     ylru "^1.2.0"
 
-call-bind@^1.0.0:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
-  integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+  integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
   dependencies:
+    es-errors "^1.3.0"
     function-bind "^1.1.2"
-    get-intrinsic "^1.2.1"
-    set-function-length "^1.1.1"
 
-camel-case@^4.1.1:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
-  integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
+call-bound@^1.0.2, call-bound@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+  integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
   dependencies:
-    pascal-case "^3.1.2"
-    tslib "^2.0.3"
-
-camelcase@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
-  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+    call-bind-apply-helpers "^1.0.2"
+    get-intrinsic "^1.3.0"
 
 camelcase@^6.0.0, camelcase@^6.2.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
   integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
 
-caniuse-api@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
-  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
-  dependencies:
-    browserslist "^4.0.0"
-    caniuse-lite "^1.0.0"
-    lodash.memoize "^4.1.2"
-    lodash.uniq "^4.5.0"
-
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001033, caniuse-lite@^1.0.30001565:
-  version "1.0.30001572"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz#1ccf7dc92d2ee2f92ed3a54e11b7b4a3041acfa0"
-  integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==
-
-caseless@~0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
-  integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
-
 chai-a11y-axe@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/chai-a11y-axe/-/chai-a11y-axe-1.5.0.tgz#aafa37f91f53baeafe98219768e5dee8776cf655"
@@ -2492,15 +1134,6 @@
   dependencies:
     chalk "^4.1.2"
 
-chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
-  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
-  dependencies:
-    ansi-styles "^3.2.1"
-    escape-string-regexp "^1.0.5"
-    supports-color "^5.3.0"
-
 chalk@^4.1.0, chalk@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -2509,10 +1142,10 @@
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chokidar@3.5.3, chokidar@^3.0.0, chokidar@^3.4.3, chokidar@^3.5.1:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
-  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+chokidar@^3.5.3:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
+  integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
   dependencies:
     anymatch "~3.1.2"
     braces "~3.0.2"
@@ -2524,10 +1157,12 @@
   optionalDependencies:
     fsevents "~2.3.2"
 
-chownr@^1.1.1:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
-  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+chokidar@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
+  integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
+  dependencies:
+    readdirp "^4.0.1"
 
 chrome-launcher@^0.15.0:
   version "0.15.2"
@@ -2539,19 +1174,13 @@
     is-wsl "^2.2.0"
     lighthouse-logger "^1.0.0"
 
-clean-css@^4.2.3:
-  version "4.2.4"
-  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
-  integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==
+chromium-bidi@8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-8.0.0.tgz#d73c9beed40317adf2bcfeb9a47087003cd467ec"
+  integrity sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==
   dependencies:
-    source-map "~0.6.0"
-
-clean-css@^5.3.1:
-  version "5.3.3"
-  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
-  integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==
-  dependencies:
-    source-map "~0.6.0"
+    mitt "^3.0.1"
+    zod "^3.24.1"
 
 cli-cursor@^3.1.0:
   version "3.1.0"
@@ -2569,16 +1198,26 @@
     strip-ansi "^6.0.0"
     wrap-ansi "^7.0.0"
 
+cliui@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
+  integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.1"
+    wrap-ansi "^7.0.0"
+
 clone@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
   integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
 
 co-body@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.1.0.tgz#d87a8efc3564f9bfe3aced8ef5cd04c7a8766547"
-  integrity sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.2.0.tgz#afd776d60e5659f4eee862df83499698eb1aea1b"
+  integrity sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==
   dependencies:
+    "@hapi/bourne" "^3.0.0"
     inflation "^2.0.0"
     qs "^6.5.2"
     raw-body "^2.3.3"
@@ -2589,13 +1228,6 @@
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
   integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
 
-color-convert@^1.9.0:
-  version "1.9.3"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
-  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
-  dependencies:
-    color-name "1.1.3"
-
 color-convert@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -2603,24 +1235,12 @@
   dependencies:
     color-name "~1.1.4"
 
-color-name@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-
 color-name@~1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-combined-stream@^1.0.6, combined-stream@~1.0.6:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
-  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
-  dependencies:
-    delayed-stream "~1.0.0"
-
-command-line-args@^5.0.2, command-line-args@^5.1.1, command-line-args@^5.2.1:
+command-line-args@^5.1.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e"
   integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==
@@ -2630,58 +1250,16 @@
     lodash.camelcase "^4.3.0"
     typical "^4.0.0"
 
-command-line-usage@^6.1.0, command-line-usage@^6.1.1:
-  version "6.1.3"
-  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957"
-  integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==
-  dependencies:
-    array-back "^4.0.2"
-    chalk "^2.4.2"
-    table-layout "^1.0.2"
-    typical "^5.2.0"
-
-command-line-usage@^7.0.0, command-line-usage@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-7.0.1.tgz#e540afef4a4f3bc501b124ffde33956309100655"
-  integrity sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==
+command-line-usage@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-7.0.3.tgz#6bce992354f6af10ecea2b631bfdf0c8b3bfaea3"
+  integrity sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==
   dependencies:
     array-back "^6.2.2"
     chalk-template "^0.4.0"
-    table-layout "^3.0.0"
+    table-layout "^4.1.0"
     typical "^7.1.1"
 
-commander@^2.20.0:
-  version "2.20.3"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
-  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
-
-commander@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
-  integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
-
-compressible@^2.0.0:
-  version "2.0.18"
-  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
-  integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
-  dependencies:
-    mime-db ">= 1.43.0 < 2"
-
-concat-map@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-
-connect@^3.7.0:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
-  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
-  dependencies:
-    debug "2.6.9"
-    finalhandler "1.1.2"
-    parseurl "~1.3.3"
-    utils-merge "1.0.1"
-
 content-disposition@~0.5.2:
   version "0.5.4"
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
@@ -2689,26 +1267,16 @@
   dependencies:
     safe-buffer "5.2.1"
 
-content-type@^1.0.4, content-type@~1.0.5:
+content-type@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
   integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
 
-convert-source-map@^1.6.0, convert-source-map@^1.7.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
-  integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
-
 convert-source-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
   integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
 
-cookie@~0.4.1:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
-  integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
-
 cookies@~0.9.0:
   version "0.9.1"
   resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3"
@@ -2717,82 +1285,40 @@
     depd "~2.0.0"
     keygrip "~1.1.0"
 
-core-js-bundle@^3.6.0, core-js-bundle@^3.8.1:
-  version "3.35.0"
-  resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.35.0.tgz#dd87c465625b52a35dd891a930789d11cae00fe0"
-  integrity sha512-gyqx4VKhV1tGhoxeYoxVchR1vMxbQJrC8BrCPnIU163Oyf9//GYQNU2eH7lmjav2K+5WGAWgLyZGxAfGggwJKA==
-
-core-js-compat@^3.31.0, core-js-compat@^3.33.1:
-  version "3.35.0"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.0.tgz#c149a3d1ab51e743bc1da61e39cb51f461a41873"
-  integrity sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==
+cross-spawn@^7.0.3:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
   dependencies:
-    browserslist "^4.22.2"
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
 
-core-util-is@1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-  integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
-
-cors@~2.8.5:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
-  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
-cross-fetch@3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
-  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
-  dependencies:
-    node-fetch "2.6.7"
-
-custom-event@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
-  integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==
-
-dashdash@^1.12.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
-  integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==
-  dependencies:
-    assert-plus "^1.0.0"
-
-date-format@^4.0.14:
-  version "4.0.14"
-  resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400"
-  integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==
+data-uri-to-buffer@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b"
+  integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==
 
 debounce@^1.2.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
   integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
 
-debug@2.6.9, debug@^2.6.9:
+debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@^4.4.1:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+  integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
+  dependencies:
+    ms "^2.1.3"
+
+debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
   dependencies:
     ms "2.0.0"
 
-debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
-  dependencies:
-    ms "2.1.2"
-
-debug@4.3.3:
-  version "4.3.3"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
-  integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
-  dependencies:
-    ms "2.1.2"
-
-debug@^3.1.0, debug@^3.2.7:
+debug@^3.1.0:
   version "3.2.7"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
   integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -2809,34 +1335,31 @@
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
   integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==
 
-deep-extend@~0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
-  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
-
 deepmerge@^4.2.2:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
   integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
 
-define-data-property@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
-  integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
+default-gateway@^6.0.0:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71"
+  integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==
   dependencies:
-    get-intrinsic "^1.2.1"
-    gopd "^1.0.1"
-    has-property-descriptors "^1.0.0"
+    execa "^5.0.0"
 
 define-lazy-prop@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
   integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
 
-delayed-stream@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
-  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+degenerator@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5"
+  integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==
+  dependencies:
+    ast-types "^0.13.4"
+    escodegen "^2.1.0"
+    esprima "^4.0.1"
 
 delegates@^1.0.0:
   version "1.0.0"
@@ -2858,30 +1381,25 @@
   resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
   integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
 
-destroy@1.2.0, destroy@^1.0.4:
+destroy@^1.0.4:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
   integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
 
-devtools-protocol@0.0.981744:
-  version "0.0.981744"
-  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf"
-  integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==
+devtools-protocol@0.0.1475386:
+  version "0.0.1475386"
+  resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz#5378401a2c5698ab68c3482c9b7816ff62ec652b"
+  integrity sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==
 
-di@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-  integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==
+diff@^5.0.0, diff@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
+  integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
 
-diff@5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
-  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
-
-diff@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
-  integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
+diff@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a"
+  integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==
 
 dir-glob@^3.0.1:
   version "3.0.1"
@@ -2890,272 +1408,163 @@
   dependencies:
     path-type "^4.0.0"
 
-dom-serialize@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
-  integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==
+dunder-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
   dependencies:
-    custom-event "~1.0.0"
-    ent "~2.2.0"
-    extend "^3.0.0"
-    void-elements "^2.0.0"
-
-dot-case@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
-  integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
-  dependencies:
-    no-case "^3.0.4"
-    tslib "^2.0.3"
-
-dynamic-import-polyfill@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dynamic-import-polyfill/-/dynamic-import-polyfill-0.1.1.tgz#e1f9eb1876ee242bd56572f8ed4df768e143083f"
-  integrity sha512-m953zv0w5oDagTItWm6Auhmk/pY7EiejaqiVbnzSS3HIjh1FCUeK7WzuaVtWPNs58A+/xpIE+/dVk6pKsrua8g==
-
-ecc-jsbn@~0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
-  integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==
-  dependencies:
-    jsbn "~0.1.0"
-    safer-buffer "^2.1.0"
+    call-bind-apply-helpers "^1.0.1"
+    es-errors "^1.3.0"
+    gopd "^1.2.0"
 
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
 
-electron-to-chromium@^1.4.601, electron-to-chromium@^1.4.67:
-  version "1.4.617"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.617.tgz#3b0dde6c54e5f0f26db75ce6c6ae751e5df4bf75"
-  integrity sha512-sYNE3QxcDS4ANW1k4S/wWYMXjCVcFSOX3Bg8jpuMFaXt/x8JCmp0R1Xe1ZXDX4WXnSRBf+GJ/3eGWicUuQq5cg==
-
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
-encodeurl@^1.0.2, encodeurl@~1.0.2:
+encodeurl@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
 
-end-of-stream@^1.1.0, end-of-stream@^1.4.1:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
-  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+end-of-stream@^1.1.0:
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
+  integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
   dependencies:
     once "^1.4.0"
 
-engine.io-parser@~5.2.1:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb"
-  integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
-
-engine.io@~6.5.2:
-  version "6.5.4"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc"
-  integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==
-  dependencies:
-    "@types/cookie" "^0.4.1"
-    "@types/cors" "^2.8.12"
-    "@types/node" ">=10.0.0"
-    accepts "~1.3.4"
-    base64id "2.0.0"
-    cookie "~0.4.1"
-    cors "~2.8.5"
-    debug "~4.3.1"
-    engine.io-parser "~5.2.1"
-    ws "~8.11.0"
-
-ent@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
-  integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==
-
-entities@^4.4.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
-  integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
-
-error-ex@^1.3.1:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
-  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
-  dependencies:
-    is-arrayish "^0.2.1"
-
-errorstacks@^2.2.0:
+errorstacks@^2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.1.tgz#05adf6de1f5b04a66f2c12cc0593e1be2b18cd0f"
   integrity sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==
 
-es-dev-server@^1.57.8:
-  version "1.60.2"
-  resolved "https://registry.yarnpkg.com/es-dev-server/-/es-dev-server-1.60.2.tgz#cca56fe452d46c3ec531c19745e0aa9d7b71e1b3"
-  integrity sha512-Lp9kZzawJ35HDKiqLNb/YbD2VufF+3tdxHgbP/kfdLI5JLgDJV4SuKTWWny3ZuBUAlZKGre7a0iXUByGQqfdPA==
-  dependencies:
-    "@babel/core" "^7.11.1"
-    "@babel/plugin-proposal-dynamic-import" "^7.10.4"
-    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4"
-    "@babel/plugin-proposal-optional-chaining" "^7.11.0"
-    "@babel/plugin-syntax-class-properties" "^7.8.3"
-    "@babel/plugin-syntax-import-meta" "^7.10.4"
-    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
-    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
-    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
-    "@babel/plugin-transform-template-literals" "^7.8.3"
-    "@babel/preset-env" "^7.9.0"
-    "@koa/cors" "^3.1.0"
-    "@open-wc/building-utils" "^2.18.3"
-    "@rollup/plugin-node-resolve" "^7.1.1"
-    "@rollup/pluginutils" "^3.0.0"
-    "@types/babel__core" "^7.1.3"
-    "@types/browserslist" "^4.8.0"
-    "@types/browserslist-useragent" "^3.0.0"
-    "@types/caniuse-api" "^3.0.0"
-    "@types/command-line-args" "^5.0.0"
-    "@types/command-line-usage" "^5.0.1"
-    "@types/debounce" "^1.2.0"
-    "@types/koa" "^2.0.48"
-    "@types/koa-compress" "^2.0.9"
-    "@types/koa-etag" "^3.0.0"
-    "@types/koa-static" "^4.0.1"
-    "@types/koa__cors" "^3.0.1"
-    "@types/lru-cache" "^5.1.0"
-    "@types/mime-types" "^2.1.0"
-    "@types/minimatch" "^3.0.3"
-    "@types/path-is-inside" "^1.0.0"
-    "@types/whatwg-url" "^6.4.0"
-    browserslist "^4.9.1"
-    browserslist-useragent "^3.0.2"
-    builtin-modules "^3.1.0"
-    camelcase "^5.3.1"
-    caniuse-api "^3.0.0"
-    caniuse-lite "^1.0.30001033"
-    chokidar "^3.0.0"
-    command-line-args "^5.0.2"
-    command-line-usage "^6.1.0"
-    debounce "^1.2.0"
-    deepmerge "^4.2.2"
-    es-module-lexer "^0.3.13"
-    get-stream "^5.1.0"
-    is-stream "^2.0.0"
-    isbinaryfile "^4.0.2"
-    koa "^2.7.0"
-    koa-compress "^3.0.0"
-    koa-etag "^3.0.0"
-    koa-static "^5.0.0"
-    lru-cache "^5.1.1"
-    mime-types "^2.1.27"
-    minimatch "^3.0.4"
-    open "^7.0.3"
-    parse5 "^5.1.1"
-    path-is-inside "^1.0.2"
-    polyfills-loader "^1.7.4"
-    portfinder "^1.0.21"
-    rollup "^2.7.2"
-    strip-ansi "^5.2.0"
-    systemjs "^6.3.1"
-    tslib "^1.11.1"
-    useragent "^2.3.0"
-    whatwg-url "^7.0.0"
+es-define-property@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
 
-es-module-lexer@^0.3.13:
-  version "0.3.26"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.26.tgz#7b507044e97d5b03b01d4392c74ffeb9c177a83b"
-  integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==
+es-errors@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
 
 es-module-lexer@^1.0.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
-  integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
+  integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
 
-es-module-shims@^0.4.6:
-  version "0.4.7"
-  resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-0.4.7.tgz#1419b65bbd38dfe91ab8ea5d7b4b454561e44641"
-  integrity sha512-0LTiSQoPWwdcaTVIQXhGlaDwTneD0g9/tnH1PNs3zHFFH+xoCeJclDM3rQeqF9nurXPfMKm3l9+kfPRa5VpbKg==
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+  integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+  dependencies:
+    es-errors "^1.3.0"
 
-es-module-shims@^1.4.1:
-  version "1.8.2"
-  resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-1.8.2.tgz#54e09c96e1a8e897e55aa9ef3b12d9a818b19b25"
-  integrity sha512-7vIYVzpOhXtpc3Yn03itB+GSgVZFW7oL4kdydA+iL+IEi7HiSLBUxM05QFw4SxTl6e++pMpGqZPo2+vdNs3TbA==
-
-"esbuild@^0.16 || ^0.17":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
-  integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
+esbuild@^0.25.0:
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976"
+  integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==
   optionalDependencies:
-    "@esbuild/android-arm" "0.17.19"
-    "@esbuild/android-arm64" "0.17.19"
-    "@esbuild/android-x64" "0.17.19"
-    "@esbuild/darwin-arm64" "0.17.19"
-    "@esbuild/darwin-x64" "0.17.19"
-    "@esbuild/freebsd-arm64" "0.17.19"
-    "@esbuild/freebsd-x64" "0.17.19"
-    "@esbuild/linux-arm" "0.17.19"
-    "@esbuild/linux-arm64" "0.17.19"
-    "@esbuild/linux-ia32" "0.17.19"
-    "@esbuild/linux-loong64" "0.17.19"
-    "@esbuild/linux-mips64el" "0.17.19"
-    "@esbuild/linux-ppc64" "0.17.19"
-    "@esbuild/linux-riscv64" "0.17.19"
-    "@esbuild/linux-s390x" "0.17.19"
-    "@esbuild/linux-x64" "0.17.19"
-    "@esbuild/netbsd-x64" "0.17.19"
-    "@esbuild/openbsd-x64" "0.17.19"
-    "@esbuild/sunos-x64" "0.17.19"
-    "@esbuild/win32-arm64" "0.17.19"
-    "@esbuild/win32-ia32" "0.17.19"
-    "@esbuild/win32-x64" "0.17.19"
+    "@esbuild/aix-ppc64" "0.25.9"
+    "@esbuild/android-arm" "0.25.9"
+    "@esbuild/android-arm64" "0.25.9"
+    "@esbuild/android-x64" "0.25.9"
+    "@esbuild/darwin-arm64" "0.25.9"
+    "@esbuild/darwin-x64" "0.25.9"
+    "@esbuild/freebsd-arm64" "0.25.9"
+    "@esbuild/freebsd-x64" "0.25.9"
+    "@esbuild/linux-arm" "0.25.9"
+    "@esbuild/linux-arm64" "0.25.9"
+    "@esbuild/linux-ia32" "0.25.9"
+    "@esbuild/linux-loong64" "0.25.9"
+    "@esbuild/linux-mips64el" "0.25.9"
+    "@esbuild/linux-ppc64" "0.25.9"
+    "@esbuild/linux-riscv64" "0.25.9"
+    "@esbuild/linux-s390x" "0.25.9"
+    "@esbuild/linux-x64" "0.25.9"
+    "@esbuild/netbsd-arm64" "0.25.9"
+    "@esbuild/netbsd-x64" "0.25.9"
+    "@esbuild/openbsd-arm64" "0.25.9"
+    "@esbuild/openbsd-x64" "0.25.9"
+    "@esbuild/openharmony-arm64" "0.25.9"
+    "@esbuild/sunos-x64" "0.25.9"
+    "@esbuild/win32-arm64" "0.25.9"
+    "@esbuild/win32-ia32" "0.25.9"
+    "@esbuild/win32-x64" "0.25.9"
 
 escalade@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
-  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+  integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
 
-escape-html@^1.0.3, escape-html@~1.0.3:
+escape-html@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
 
-escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
+escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
-escape-string-regexp@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
-  integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+escodegen@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
+  integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^5.2.0"
+    esutils "^2.0.2"
+  optionalDependencies:
+    source-map "~0.6.1"
 
-estree-walker@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
-  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
+esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-etag@^1.3.0, etag@^1.8.1:
+etag@^1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
 
-eventemitter3@^4.0.0:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
-  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+execa@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+  dependencies:
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
+    strip-final-newline "^2.0.0"
 
-extend@^3.0.0, extend@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
-  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
-
-extract-zip@2.0.1:
+extract-zip@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
   integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@@ -3166,41 +1575,26 @@
   optionalDependencies:
     "@types/yauzl" "^2.9.1"
 
-extsprintf@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
-  integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
-
-extsprintf@^1.2.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
-  integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
-
-fast-deep-equal@^3.1.1:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
-  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-fifo@^1.2.0, fast-fifo@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
+  integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
 
 fast-glob@^3.2.9:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
-  integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+  integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.walk" "^1.2.3"
     glob-parent "^5.1.2"
     merge2 "^1.3.0"
-    micromatch "^4.0.4"
-
-fast-json-stable-stringify@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
-  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+    micromatch "^4.0.8"
 
 fastq@^1.6.0:
-  version "1.16.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.16.0.tgz#83b9a9375692db77a822df081edb6a9cf6839320"
-  integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+  integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
   dependencies:
     reusify "^1.0.4"
 
@@ -3211,26 +1605,13 @@
   dependencies:
     pend "~1.2.0"
 
-fill-range@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
-  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+fill-range@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
   dependencies:
     to-regex-range "^5.0.1"
 
-finalhandler@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
-  integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    parseurl "~1.3.3"
-    statuses "~1.5.0"
-    unpipe "~1.0.0"
-
 find-replace@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
@@ -3238,7 +1619,7 @@
   dependencies:
     array-back "^3.0.1"
 
-find-up@5.0.0:
+find-up@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
   integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
@@ -3246,69 +1627,16 @@
     locate-path "^6.0.0"
     path-exists "^4.0.0"
 
-find-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
-  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
-  dependencies:
-    locate-path "^3.0.0"
-
-find-up@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
-  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
-  dependencies:
-    locate-path "^5.0.0"
-    path-exists "^4.0.0"
-
 flat@^5.0.2:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
   integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
 
-flatted@^3.2.7:
-  version "3.2.9"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf"
-  integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
-
-follow-redirects@^1.0.0:
-  version "1.15.4"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
-  integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
-
-forever-agent@~0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
-  integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
-
-form-data@~2.3.2:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
-  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
-  dependencies:
-    asynckit "^0.4.0"
-    combined-stream "^1.0.6"
-    mime-types "^2.1.12"
-
 fresh@~0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
 
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
-fs-extra@^8.1.0:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
-  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
-  dependencies:
-    graceful-fs "^4.2.0"
-    jsonfile "^4.0.0"
-    universalify "^0.1.0"
-
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -3329,25 +1657,34 @@
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
   integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
 
-gensync@^1.0.0-beta.2:
-  version "1.0.0-beta.2"
-  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
-  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
-
 get-caller-file@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
-  integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
+get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+  integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
   dependencies:
+    call-bind-apply-helpers "^1.0.2"
+    es-define-property "^1.0.1"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.1.1"
     function-bind "^1.1.2"
-    has-proto "^1.0.1"
-    has-symbols "^1.0.3"
-    hasown "^2.0.0"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
+    has-symbols "^1.1.0"
+    hasown "^2.0.2"
+    math-intrinsics "^1.1.0"
+
+get-proto@^1.0.0, get-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+  integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+  dependencies:
+    dunder-proto "^1.0.1"
+    es-object-atoms "^1.0.0"
 
 get-stream@^5.1.0:
   version "5.2.0"
@@ -3361,12 +1698,14 @@
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
-getpass@^0.1.1:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
-  integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==
+get-uri@^6.0.1:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16"
+  integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==
   dependencies:
-    assert-plus "^1.0.0"
+    basic-ftp "^5.0.2"
+    data-uri-to-buffer "^6.0.2"
+    debug "^4.3.4"
 
 glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
@@ -3375,34 +1714,16 @@
   dependencies:
     is-glob "^4.0.1"
 
-glob@7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
-  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+glob@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
+  integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
   dependencies:
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
     inherits "2"
-    minimatch "^3.0.4"
+    minimatch "^5.0.1"
     once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.1.3, glob@^7.1.7:
-  version "7.2.3"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
-  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.1.1"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-globals@^11.1.0:
-  version "11.12.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
-  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 
 globby@^11.0.1:
   version "11.1.0"
@@ -3416,105 +1737,45 @@
     merge2 "^1.4.1"
     slash "^3.0.0"
 
-gopd@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
-  integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
-  dependencies:
-    get-intrinsic "^1.1.3"
-
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
-  version "4.2.11"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
-  integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
-
-growl@1.10.5:
-  version "1.10.5"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
-  integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
-
-har-schema@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
-  integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==
-
-har-validator@~5.1.3:
-  version "5.1.5"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
-  integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
-  dependencies:
-    ajv "^6.12.3"
-    har-schema "^2.0.0"
-
-has-flag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
-  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+gopd@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
 
 has-flag@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-property-descriptors@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340"
-  integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+  integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
   dependencies:
-    get-intrinsic "^1.2.2"
+    has-symbols "^1.0.3"
 
-has-proto@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
-  integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
-
-has-symbols@^1.0.2, has-symbols@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
-  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
-
-has-tostringtag@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
-  integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
-  dependencies:
-    has-symbols "^1.0.2"
-
-hasown@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
-  integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
+hasown@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
   dependencies:
     function-bind "^1.1.2"
 
-he@1.2.0, he@^1.2.0:
+he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-hosted-git-info@^2.1.4:
-  version "2.8.9"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
-  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
-
 html-escaper@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
-html-minifier-terser@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054"
-  integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==
-  dependencies:
-    camel-case "^4.1.1"
-    clean-css "^4.2.3"
-    commander "^4.1.1"
-    he "^1.2.0"
-    param-case "^3.0.3"
-    relateurl "^0.2.7"
-    terser "^4.6.3"
-
 http-assert@^1.3.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f"
@@ -3555,32 +1816,27 @@
     setprototypeof "1.1.0"
     statuses ">= 1.4.0 < 2"
 
-http-proxy@^1.18.1:
-  version "1.18.1"
-  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
-  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
+http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
+  integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
   dependencies:
-    eventemitter3 "^4.0.0"
-    follow-redirects "^1.0.0"
-    requires-port "^1.0.0"
+    agent-base "^7.1.0"
+    debug "^4.3.4"
 
-http-signature@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
-  integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==
+https-proxy-agent@^7.0.6:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
+  integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
   dependencies:
-    assert-plus "^1.0.0"
-    jsprim "^1.2.2"
-    sshpk "^1.7.0"
-
-https-proxy-agent@5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
-  integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
-  dependencies:
-    agent-base "6"
+    agent-base "^7.1.2"
     debug "4"
 
+human-signals@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
 iconv-lite@0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -3588,15 +1844,10 @@
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.13:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
-  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
 ignore@^5.2.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
-  integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
+  integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
 
 inflation@^2.0.0:
   version "2.1.0"
@@ -3611,7 +1862,7 @@
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
+inherits@2, inherits@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -3621,20 +1872,30 @@
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
 
-intersection-observer@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.7.0.tgz#ee16bee978db53516ead2f0a8154b09b400bbdc9"
-  integrity sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg==
+internal-ip@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-6.2.0.tgz#d5541e79716e406b74ac6b07b856ef18dc1621c1"
+  integrity sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==
+  dependencies:
+    default-gateway "^6.0.0"
+    ipaddr.js "^1.9.1"
+    is-ip "^3.1.0"
+    p-event "^4.2.0"
 
-ip@^1.1.5:
-  version "1.1.8"
-  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
-  integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==
+ip-address@^10.0.1:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed"
+  integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
 
-is-arrayish@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
-  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+ip-regex@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
+
+ipaddr.js@^1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
 is-binary-path@~2.1.0:
   version "2.1.0"
@@ -3643,19 +1904,12 @@
   dependencies:
     binary-extensions "^2.0.0"
 
-is-builtin-module@^3.1.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
-  integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==
+is-core-module@^2.16.0:
+  version "2.16.1"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
+  integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
   dependencies:
-    builtin-modules "^3.3.0"
-
-is-core-module@^2.13.0:
-  version "2.13.1"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
-  integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
-  dependencies:
-    hasown "^2.0.0"
+    hasown "^2.0.2"
 
 is-docker@^2.0.0, is-docker@^2.1.1:
   version "2.2.1"
@@ -3673,11 +1927,14 @@
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
 is-generator-function@^1.0.7:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
-  integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca"
+  integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==
   dependencies:
-    has-tostringtag "^1.0.0"
+    call-bound "^1.0.3"
+    get-proto "^1.0.0"
+    has-tostringtag "^1.0.2"
+    safe-regex-test "^1.1.0"
 
 is-glob@^4.0.1, is-glob@~4.0.1:
   version "4.0.3"
@@ -3686,6 +1943,13 @@
   dependencies:
     is-extglob "^2.1.1"
 
+is-ip@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
+  integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
+  dependencies:
+    ip-regex "^4.0.0"
+
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@@ -3701,76 +1965,48 @@
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
   integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
 
+is-regex@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
+  integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==
+  dependencies:
+    call-bound "^1.0.2"
+    gopd "^1.2.0"
+    has-tostringtag "^1.0.2"
+    hasown "^2.0.2"
+
 is-stream@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
   integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
-is-typedarray@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
-  integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
-
 is-unicode-supported@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
   integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
 
-is-wsl@^2.1.1, is-wsl@^2.2.0:
+is-wsl@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
   integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
   dependencies:
     is-docker "^2.0.0"
 
-isarray@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
-  integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
-
-isbinaryfile@^4.0.2, isbinaryfile@^4.0.8:
-  version "4.0.10"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3"
-  integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==
-
 isbinaryfile@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.0.tgz#034b7e54989dab8986598cbcea41f66663c65234"
-  integrity sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.5.tgz#5a29c56b97ef337394c3ec4cc304066042ae81e8"
+  integrity sha512-vh9MWXjhhblwrHlt/yutrubDuBD01kKFscyVndE2/VEeEU5aAizrzuWAsEaCjo997k+IluhN6C4jgxfS69SCIw==
 
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 
-isstream@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
-  integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
-
-istanbul-lib-coverage@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
-  integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
-
 istanbul-lib-coverage@^3.0.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
   integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
 
-istanbul-lib-instrument@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
-  integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
-  dependencies:
-    "@babel/generator" "^7.4.0"
-    "@babel/parser" "^7.4.3"
-    "@babel/template" "^7.4.0"
-    "@babel/traverse" "^7.4.3"
-    "@babel/types" "^7.4.0"
-    istanbul-lib-coverage "^2.0.5"
-    semver "^6.0.0"
-
 istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
@@ -3781,9 +2017,9 @@
     supports-color "^7.1.0"
 
 istanbul-reports@^3.0.2:
-  version "3.1.6"
-  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a"
-  integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
+  integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
   dependencies:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
@@ -3793,128 +2029,13 @@
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
-js-yaml@4.1.0:
+js-yaml@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
   integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
   dependencies:
     argparse "^2.0.1"
 
-jsbn@~0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
-  integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
-
-jsesc@^2.5.1:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
-  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
-
-jsesc@~0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
-  integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
-
-json-parse-better-errors@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
-  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
-
-json-schema-traverse@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
-  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-
-json-schema@0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
-  integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
-
-json-stringify-safe@~5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
-  integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
-
-json5@^2.2.3:
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
-  integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
-
-jsonfile@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
-  integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
-jsprim@^1.2.2:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
-  integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
-  dependencies:
-    assert-plus "1.0.0"
-    extsprintf "1.3.0"
-    json-schema "0.4.0"
-    verror "1.10.0"
-
-just-extend@^4.0.2:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
-  integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
-
-karma-chrome-launcher@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9"
-  integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==
-  dependencies:
-    which "^1.2.1"
-
-karma-mocha-reporter@^2.2.5:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz#15120095e8ed819186e47a0b012f3cd741895560"
-  integrity sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==
-  dependencies:
-    chalk "^2.1.0"
-    log-symbols "^2.1.0"
-    strip-ansi "^4.0.0"
-
-karma-mocha@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d"
-  integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==
-  dependencies:
-    minimist "^1.2.3"
-
-karma@^6.4.4:
-  version "6.4.4"
-  resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492"
-  integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==
-  dependencies:
-    "@colors/colors" "1.5.0"
-    body-parser "^1.19.0"
-    braces "^3.0.2"
-    chokidar "^3.5.1"
-    connect "^3.7.0"
-    di "^0.0.1"
-    dom-serialize "^2.2.1"
-    glob "^7.1.7"
-    graceful-fs "^4.2.6"
-    http-proxy "^1.18.1"
-    isbinaryfile "^4.0.8"
-    lodash "^4.17.21"
-    log4js "^6.4.1"
-    mime "^2.5.2"
-    minimatch "^3.0.4"
-    mkdirp "^0.5.5"
-    qjobs "^1.2.0"
-    range-parser "^1.2.1"
-    rimraf "^3.0.2"
-    socket.io "^4.7.2"
-    source-map "^0.6.1"
-    tmp "^0.2.1"
-    ua-parser-js "^0.7.30"
-    yargs "^16.1.1"
-
 keygrip@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
@@ -3927,16 +2048,6 @@
   resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
   integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
 
-koa-compress@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-3.1.0.tgz#00fb0af695dc4661c6de261a18da669626ea3ca1"
-  integrity sha512-0m24/yS/GbhWI+g9FqtvStY+yJwTObwoxOvPok6itVjRen7PBWkjsJ8pre76m+99YybXLKhOJ62mJ268qyBFMQ==
-  dependencies:
-    bytes "^3.0.0"
-    compressible "^2.0.0"
-    koa-is-json "^1.0.0"
-    statuses "^1.0.0"
-
 koa-convert@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
@@ -3945,14 +2056,6 @@
     co "^4.6.0"
     koa-compose "^4.1.0"
 
-koa-etag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-3.0.0.tgz#9ef7382ddd5a82ab0deb153415c915836f771d3f"
-  integrity sha512-HYU1zIsH4S9xOlUZGuZIP1PIiJ0EkBXgwL8PjFECb/pUYmAee8gfcvIovregBMYxECDhLulEWT2+ZRsA/lczCQ==
-  dependencies:
-    etag "^1.3.0"
-    mz "^2.1.0"
-
 koa-etag@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-4.0.0.tgz#2c2bb7ae69ca1ac6ced09ba28dcb78523c810414"
@@ -3960,11 +2063,6 @@
   dependencies:
     etag "^1.8.1"
 
-koa-is-json@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
-  integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==
-
 koa-send@^5.0.0, koa-send@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
@@ -3982,10 +2080,10 @@
     debug "^3.1.0"
     koa-send "^5.0.0"
 
-koa@^2.13.0, koa@^2.7.0:
-  version "2.15.0"
-  resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.0.tgz#d24ae1b0ff378bf12eb3df584ab4204e4c12ac2b"
-  integrity sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==
+koa@^2.13.0:
+  version "2.16.2"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.16.2.tgz#1292a3b415a9b9f3dd6872c1835229630a4ecae3"
+  integrity sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==
   dependencies:
     accepts "^1.3.5"
     cache-content-type "^1.0.0"
@@ -4019,55 +2117,30 @@
     debug "^2.6.9"
     marky "^1.2.2"
 
-lit-element@^4.0.0:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.2.tgz#1a519896d5ab7c7be7a8729f400499e38779c093"
-  integrity sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==
+lit-element@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.2.1.tgz#0a3782f36eaa545862fe07f84abcb14b2903a042"
+  integrity sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==
   dependencies:
-    "@lit-labs/ssr-dom-shim" "^1.1.2"
-    "@lit/reactive-element" "^2.0.0"
-    lit-html "^3.1.0"
+    "@lit-labs/ssr-dom-shim" "^1.4.0"
+    "@lit/reactive-element" "^2.1.0"
+    lit-html "^3.3.0"
 
-"lit-html@^2.0.0 || ^3.0.0", lit-html@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.1.0.tgz#a7b93dd682073f2e2029656f4e9cd91e8034c196"
-  integrity sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==
+"lit-html@^2.0.0 || ^3.0.0", lit-html@^3.3.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.3.1.tgz#f0a7e4b9ea0a1d034eb28a4bf2d1b0a0096253e3"
+  integrity sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==
   dependencies:
     "@types/trusted-types" "^2.0.2"
 
-"lit@^2.0.0 || ^3.0.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lit/-/lit-3.1.0.tgz#76429b85dc1f5169fed499a0f7e89e2e619010c9"
-  integrity sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==
+"lit@^2.0.0 || ^3.0.0", lit@^3.0.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lit/-/lit-3.3.1.tgz#9dc79be626bc9a3b824de98b107dd662cabdeda6"
+  integrity sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==
   dependencies:
-    "@lit/reactive-element" "^2.0.0"
-    lit-element "^4.0.0"
-    lit-html "^3.1.0"
-
-load-json-file@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
-  integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^4.0.0"
-    pify "^3.0.0"
-    strip-bom "^3.0.0"
-
-locate-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
-  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
-  dependencies:
-    p-locate "^3.0.0"
-    path-exists "^3.0.0"
-
-locate-path@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
-  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
-  dependencies:
-    p-locate "^4.1.0"
+    "@lit/reactive-element" "^2.1.0"
+    lit-element "^4.2.0"
+    lit-html "^3.3.0"
 
 locate-path@^6.0.0:
   version "6.0.0"
@@ -4076,47 +2149,12 @@
   dependencies:
     p-locate "^5.0.0"
 
-lodash.assignwith@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb"
-  integrity sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==
-
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
   integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
 
-lodash.debounce@^4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
-  integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
-
-lodash.get@^4.4.2:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-  integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
-
-lodash.memoize@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
-  integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
-
-lodash.sortby@^4.7.0:
-  version "4.7.0"
-  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
-  integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
-
-lodash.uniq@^4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-  integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
-
-lodash@^4.17.14, lodash@^4.17.21:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-log-symbols@4.1.0:
+log-symbols@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
   integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
@@ -4124,13 +2162,6 @@
     chalk "^4.1.0"
     is-unicode-supported "^0.1.0"
 
-log-symbols@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
-  integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
-  dependencies:
-    chalk "^2.0.1"
-
 log-update@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
@@ -4141,45 +2172,10 @@
     slice-ansi "^4.0.0"
     wrap-ansi "^6.2.0"
 
-log4js@^6.4.1:
-  version "6.9.1"
-  resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6"
-  integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==
-  dependencies:
-    date-format "^4.0.14"
-    debug "^4.3.4"
-    flatted "^3.2.7"
-    rfdc "^1.3.0"
-    streamroller "^3.1.5"
-
-lower-case@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
-  integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
-  dependencies:
-    tslib "^2.0.3"
-
-lru-cache@4.1.x:
-  version "4.1.5"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
-  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
-  dependencies:
-    pseudomap "^1.0.2"
-    yallist "^2.1.2"
-
-lru-cache@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
-  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
-  dependencies:
-    yallist "^3.0.2"
-
-lru-cache@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
-  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
-  dependencies:
-    yallist "^4.0.0"
+lru-cache@^7.14.1:
+  version "7.18.3"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
+  integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
 
 lru-cache@^8.0.4:
   version "8.0.5"
@@ -4194,249 +2190,152 @@
     semver "^7.5.3"
 
 marky@^1.2.2:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0"
-  integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/marky/-/marky-1.3.0.tgz#422b63b0baf65022f02eda61a238eccdbbc14997"
+  integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==
+
+math-intrinsics@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
 
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
 
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
 merge2@^1.3.0, merge2@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-micromatch@^4.0.4:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
-  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+micromatch@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+  integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
   dependencies:
-    braces "^3.0.2"
+    braces "^3.0.3"
     picomatch "^2.3.1"
 
-mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
+mime-db@1.52.0:
   version "1.52.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
+mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34:
   version "2.1.35"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
   integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
   dependencies:
     mime-db "1.52.0"
 
-mime@^2.5.2:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
-  integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
-
 mimic-fn@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-minimatch@4.2.1:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4"
-  integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimatch@^3.0.4, minimatch@^3.1.1:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
-  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimatch@^7.4.2:
-  version "7.4.6"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb"
-  integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==
+minimatch@^5.0.1, minimatch@^5.1.6:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
+  integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
   dependencies:
     brace-expansion "^2.0.1"
 
-minimist@^1.2.3, minimist@^1.2.6:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
-  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
-
-mkdirp-classic@^0.5.2:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
-  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-
-mkdirp@^0.5.5, mkdirp@^0.5.6:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
-  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
-  dependencies:
-    minimist "^1.2.6"
+mitt@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
+  integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
 
 mkdirp@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
-mocha@9.2.2:
-  version "9.2.2"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9"
-  integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==
+mocha@^10.2.0:
+  version "10.8.2"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96"
+  integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==
   dependencies:
-    "@ungap/promise-all-settled" "1.1.2"
-    ansi-colors "4.1.1"
-    browser-stdout "1.3.1"
-    chokidar "3.5.3"
-    debug "4.3.3"
-    diff "5.0.0"
-    escape-string-regexp "4.0.0"
-    find-up "5.0.0"
-    glob "7.2.0"
-    growl "1.10.5"
-    he "1.2.0"
-    js-yaml "4.1.0"
-    log-symbols "4.1.0"
-    minimatch "4.2.1"
-    ms "2.1.3"
-    nanoid "3.3.1"
-    serialize-javascript "6.0.0"
-    strip-json-comments "3.1.1"
-    supports-color "8.1.1"
-    which "2.0.2"
-    workerpool "6.2.0"
-    yargs "16.2.0"
-    yargs-parser "20.2.4"
-    yargs-unparser "2.0.0"
+    ansi-colors "^4.1.3"
+    browser-stdout "^1.3.1"
+    chokidar "^3.5.3"
+    debug "^4.3.5"
+    diff "^5.2.0"
+    escape-string-regexp "^4.0.0"
+    find-up "^5.0.0"
+    glob "^8.1.0"
+    he "^1.2.0"
+    js-yaml "^4.1.0"
+    log-symbols "^4.1.0"
+    minimatch "^5.1.6"
+    ms "^2.1.3"
+    serialize-javascript "^6.0.2"
+    strip-json-comments "^3.1.1"
+    supports-color "^8.1.1"
+    workerpool "^6.5.1"
+    yargs "^16.2.0"
+    yargs-parser "^20.2.9"
+    yargs-unparser "^2.0.0"
 
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
   integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
 
-ms@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
-  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-ms@2.1.3, ms@^2.1.1:
+ms@^2.1.1, ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
-mz@^2.1.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
-  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
-  dependencies:
-    any-promise "^1.0.0"
-    object-assign "^4.0.1"
-    thenify-all "^1.0.0"
-
 nanocolors@^0.2.1:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.2.13.tgz#dfd1ed0bfab05e9fe540eb6874525f0a1684099b"
   integrity sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==
 
-nanoid@3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
-  integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
-
 nanoid@^3.1.25:
-  version "3.3.7"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
-  integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+  version "3.3.11"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
+  integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
 
 negotiator@0.6.3:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
   integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
 
-nise@^5.1.1:
-  version "5.1.5"
-  resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.5.tgz#f2aef9536280b6c18940e32ba1fbdc770b8964ee"
-  integrity sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==
-  dependencies:
-    "@sinonjs/commons" "^2.0.0"
-    "@sinonjs/fake-timers" "^10.0.2"
-    "@sinonjs/text-encoding" "^0.7.1"
-    just-extend "^4.0.2"
-    path-to-regexp "^1.7.0"
-
-no-case@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
-  integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
-  dependencies:
-    lower-case "^2.0.2"
-    tslib "^2.0.3"
-
-node-fetch@2.6.7:
-  version "2.6.7"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
-  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
-  dependencies:
-    whatwg-url "^5.0.0"
-
-node-fetch@^2.6.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
-  integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
-  dependencies:
-    whatwg-url "^5.0.0"
-
-node-releases@^2.0.14:
-  version "2.0.14"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
-  integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
-
-normalize-package-data@^2.3.2:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
-  integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
-  dependencies:
-    hosted-git-info "^2.1.4"
-    resolve "^1.10.0"
-    semver "2 || 3 || 4 || 5"
-    validate-npm-package-license "^3.0.1"
+netmask@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
+  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
 
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
-oauth-sign@~0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
-  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+npm-run-path@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
 
-object-assign@^4, object-assign@^4.0.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
-  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+object-inspect@^1.13.3:
+  version "1.13.4"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
+  integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
 
-object-inspect@^1.9.0:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
-  integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
-
-on-finished@2.4.1, on-finished@^2.3.0:
+on-finished@^2.3.0:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
   integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
   dependencies:
     ee-first "1.1.1"
 
-on-finished@~2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
-  integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==
-  dependencies:
-    ee-first "1.1.1"
-
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -4444,7 +2343,7 @@
   dependencies:
     wrappy "1"
 
-onetime@^5.1.0:
+onetime@^5.1.0, onetime@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
   integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
@@ -4456,14 +2355,6 @@
   resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
   integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==
 
-open@^7.0.3:
-  version "7.4.2"
-  resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
-  integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
-  dependencies:
-    is-docker "^2.0.0"
-    is-wsl "^2.1.1"
-
 open@^8.0.2:
   version "8.4.2"
   resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
@@ -4473,17 +2364,17 @@
     is-docker "^2.1.1"
     is-wsl "^2.2.0"
 
-os-tmpdir@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
-  integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
-
-p-limit@^2.0.0, p-limit@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
-  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+p-event@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
+  integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
   dependencies:
-    p-try "^2.0.0"
+    p-timeout "^3.1.0"
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
 
 p-limit@^3.0.2:
   version "3.1.0"
@@ -4492,20 +2383,6 @@
   dependencies:
     yocto-queue "^0.1.0"
 
-p-locate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
-  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
-  dependencies:
-    p-limit "^2.0.0"
-
-p-locate@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
-  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
-  dependencies:
-    p-limit "^2.2.0"
-
 p-locate@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
@@ -4513,96 +2390,65 @@
   dependencies:
     p-limit "^3.0.2"
 
-p-try@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
-  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-param-case@^3.0.3:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
-  integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
+p-timeout@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+  integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
   dependencies:
-    dot-case "^3.0.4"
-    tslib "^2.0.3"
+    p-finally "^1.0.0"
 
-parse-json@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
-  integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==
+pac-proxy-agent@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df"
+  integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
   dependencies:
-    error-ex "^1.3.1"
-    json-parse-better-errors "^1.0.1"
+    "@tootallnate/quickjs-emscripten" "^0.23.0"
+    agent-base "^7.1.2"
+    debug "^4.3.4"
+    get-uri "^6.0.1"
+    http-proxy-agent "^7.0.0"
+    https-proxy-agent "^7.0.6"
+    pac-resolver "^7.0.1"
+    socks-proxy-agent "^8.0.5"
 
-parse5@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
-  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
+pac-resolver@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6"
+  integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==
+  dependencies:
+    degenerator "^5.0.0"
+    netmask "^2.0.2"
 
 parse5@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
-parse5@^7.1.2:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
-  integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
-  dependencies:
-    entities "^4.4.0"
-
-parseurl@^1.3.2, parseurl@~1.3.3:
+parseurl@^1.3.2:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
   integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
 
-pascal-case@^3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
-  integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
-  dependencies:
-    no-case "^3.0.4"
-    tslib "^2.0.3"
-
-path-exists@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
-  integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==
-
 path-exists@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
   integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
 
-path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
+path-is-absolute@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
 
-path-is-inside@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
-  integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
 path-parse@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
-path-to-regexp@^1.7.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
-  integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
-  dependencies:
-    isarray "0.0.1"
-
-path-type@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
-  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
-  dependencies:
-    pify "^3.0.0"
-
 path-type@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -4613,25 +2459,20 @@
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
   integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
 
-performance-now@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
-  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
-
-picocolors@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
-  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+picocolors@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
 
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 
-pify@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
-  integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
+picomatch@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
+  integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
 
 pixelmatch@^5.2.1:
   version "5.3.0"
@@ -4640,24 +2481,17 @@
   dependencies:
     pngjs "^6.0.0"
 
-pkg-dir@4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
-  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
-  dependencies:
-    find-up "^4.0.0"
+playwright-core@1.55.0:
+  version "1.55.0"
+  resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.0.tgz#ec8a9f8ef118afb3e86e0f46f1393e3bea32adf4"
+  integrity sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==
 
-playwright-core@1.40.1:
-  version "1.40.1"
-  resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05"
-  integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==
-
-playwright@^1.22.2:
-  version "1.40.1"
-  resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae"
-  integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==
+playwright@^1.53.0:
+  version "1.55.0"
+  resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.0.tgz#7aca7ac3ffd9e083a8ad8b2514d6f9ba401cc78b"
+  integrity sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==
   dependencies:
-    playwright-core "1.40.1"
+    playwright-core "1.55.0"
   optionalDependencies:
     fsevents "2.3.2"
 
@@ -4666,110 +2500,74 @@
   resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
   integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
 
-polyfills-loader@^1.7.4:
-  version "1.7.6"
-  resolved "https://registry.yarnpkg.com/polyfills-loader/-/polyfills-loader-1.7.6.tgz#5cff98bfc9689cf10e44bdd32f498cfeb4374c51"
-  integrity sha512-AiLIgmGFmzcvsqewyKsqWb7H8CnWNTSQBoM0u+Mauzmp0DsjObXmnZdeqvTn0HNwc1wYHHTOta82WjSjG341eQ==
-  dependencies:
-    "@babel/core" "^7.11.1"
-    "@open-wc/building-utils" "^2.18.3"
-    "@webcomponents/webcomponentsjs" "^2.4.0"
-    abortcontroller-polyfill "^1.4.0"
-    core-js-bundle "^3.6.0"
-    deepmerge "^4.2.2"
-    dynamic-import-polyfill "^0.1.1"
-    es-module-shims "^0.4.6"
-    intersection-observer "^0.7.0"
-    parse5 "^5.1.1"
-    regenerator-runtime "^0.13.3"
-    resize-observer-polyfill "^1.5.1"
-    systemjs "^6.3.1"
-    terser "^4.6.7"
-    whatwg-fetch "^3.0.0"
+pngjs@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26"
+  integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
 
-portfinder@^1.0.21, portfinder@^1.0.32:
-  version "1.0.32"
-  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
-  integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
+portfinder@^1.0.32:
+  version "1.0.37"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.37.tgz#92b754ef89a11801c8efe4b0e5cd845b0064c212"
+  integrity sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==
   dependencies:
-    async "^2.6.4"
-    debug "^3.2.7"
-    mkdirp "^0.5.6"
+    async "^3.2.6"
+    debug "^4.3.6"
 
-progress@2.0.3:
+progress@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
-proxy-from-env@1.1.0:
+proxy-agent@^6.5.0:
+  version "6.5.0"
+  resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d"
+  integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==
+  dependencies:
+    agent-base "^7.1.2"
+    debug "^4.3.4"
+    http-proxy-agent "^7.0.1"
+    https-proxy-agent "^7.0.6"
+    lru-cache "^7.14.1"
+    pac-proxy-agent "^7.1.0"
+    proxy-from-env "^1.1.0"
+    socks-proxy-agent "^8.0.5"
+
+proxy-from-env@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
   integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
 
-pseudomap@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
-  integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==
-
-psl@^1.1.28:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
-  integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
-
 pump@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
-  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d"
+  integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==
   dependencies:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
-punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
   integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
 
-puppeteer-core@^13.1.3:
-  version "13.7.0"
-  resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-13.7.0.tgz#3344bee3994163f49120a55ddcd144a40575ba5b"
-  integrity sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==
+puppeteer-core@^24.0.0:
+  version "24.17.0"
+  resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-24.17.0.tgz#66eaa3532d06c22fbbdc36e09f416cb69b8c8637"
+  integrity sha512-RYOBKFiF+3RdwIZTEacqNpD567gaFcBAOKTT7742FdB1icXudrPI7BlZbYTYWK2wgGQUXt9Zi1Yn+D5PmCs4CA==
   dependencies:
-    cross-fetch "3.1.5"
-    debug "4.3.4"
-    devtools-protocol "0.0.981744"
-    extract-zip "2.0.1"
-    https-proxy-agent "5.0.1"
-    pkg-dir "4.2.0"
-    progress "2.0.3"
-    proxy-from-env "1.1.0"
-    rimraf "3.0.2"
-    tar-fs "2.1.1"
-    unbzip2-stream "1.4.3"
-    ws "8.5.0"
-
-qjobs@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
-  integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
-
-qs@6.11.0:
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
-  integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
-  dependencies:
-    side-channel "^1.0.4"
+    "@puppeteer/browsers" "2.10.7"
+    chromium-bidi "8.0.0"
+    debug "^4.4.1"
+    devtools-protocol "0.0.1475386"
+    typed-query-selector "^2.12.0"
+    ws "^8.18.3"
 
 qs@^6.5.2:
-  version "6.11.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
-  integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
+  version "6.14.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
+  integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
   dependencies:
-    side-channel "^1.0.4"
-
-qs@~6.5.2:
-  version "6.5.3"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
-  integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
+    side-channel "^1.1.0"
 
 queue-microtask@^1.2.2:
   version "1.2.3"
@@ -4783,12 +2581,7 @@
   dependencies:
     safe-buffer "^5.1.0"
 
-range-parser@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
-  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-
-raw-body@2.5.2, raw-body@^2.3.3:
+raw-body@^2.3.3:
   version "2.5.2"
   resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
   integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
@@ -4798,31 +2591,10 @@
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-read-pkg-up@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
-  integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
-  dependencies:
-    find-up "^3.0.0"
-    read-pkg "^3.0.0"
-
-read-pkg@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
-  integrity sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==
-  dependencies:
-    load-json-file "^4.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^3.0.0"
-
-readable-stream@^3.1.1, readable-stream@^3.4.0:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
-  integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
-  dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
+readdirp@^4.0.1:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
+  integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
 
 readdirp@~3.6.0:
   version "3.6.0"
@@ -4831,110 +2603,11 @@
   dependencies:
     picomatch "^2.2.1"
 
-reduce-flatten@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27"
-  integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==
-
-regenerate-unicode-properties@^10.1.0:
-  version "10.1.1"
-  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480"
-  integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==
-  dependencies:
-    regenerate "^1.4.2"
-
-regenerate@^1.4.2:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
-  integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-
-regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.7:
-  version "0.13.11"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
-  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
-
-regenerator-runtime@^0.14.0:
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
-  integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
-
-regenerator-transform@^0.15.2:
-  version "0.15.2"
-  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4"
-  integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==
-  dependencies:
-    "@babel/runtime" "^7.8.4"
-
-regexpu-core@^5.3.1:
-  version "5.3.2"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b"
-  integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==
-  dependencies:
-    "@babel/regjsgen" "^0.8.0"
-    regenerate "^1.4.2"
-    regenerate-unicode-properties "^10.1.0"
-    regjsparser "^0.9.1"
-    unicode-match-property-ecmascript "^2.0.0"
-    unicode-match-property-value-ecmascript "^2.1.0"
-
-regjsparser@^0.9.1:
-  version "0.9.1"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709"
-  integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==
-  dependencies:
-    jsesc "~0.5.0"
-
-relateurl@^0.2.7:
-  version "0.2.7"
-  resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
-  integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
-
-request@^2.88.0:
-  version "2.88.2"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
-  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.3"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.5.0"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
   integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
 
-require-main-filename@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
-  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-
-requires-port@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
-  integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
-
-resize-observer-polyfill@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
-  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
-
 resolve-path@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
@@ -4943,12 +2616,12 @@
     http-errors "~1.6.2"
     path-is-absolute "1.0.1"
 
-resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0:
-  version "1.22.8"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
-  integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
+resolve@^1.22.1:
+  version "1.22.10"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
+  integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
   dependencies:
-    is-core-module "^2.13.0"
+    is-core-module "^2.16.0"
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
@@ -4961,27 +2634,37 @@
     signal-exit "^3.0.2"
 
 reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+  integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
 
-rfdc@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
-  integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
-
-rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
-  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+rollup@^4.4.0:
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.49.0.tgz#9751ad9d06a47a4496c3c5c238b27b1422c8b0eb"
+  integrity sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==
   dependencies:
-    glob "^7.1.3"
-
-rollup@^2.67.0, rollup@^2.7.2:
-  version "2.79.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
-  integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
+    "@types/estree" "1.0.8"
   optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.49.0"
+    "@rollup/rollup-android-arm64" "4.49.0"
+    "@rollup/rollup-darwin-arm64" "4.49.0"
+    "@rollup/rollup-darwin-x64" "4.49.0"
+    "@rollup/rollup-freebsd-arm64" "4.49.0"
+    "@rollup/rollup-freebsd-x64" "4.49.0"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.49.0"
+    "@rollup/rollup-linux-arm-musleabihf" "4.49.0"
+    "@rollup/rollup-linux-arm64-gnu" "4.49.0"
+    "@rollup/rollup-linux-arm64-musl" "4.49.0"
+    "@rollup/rollup-linux-loongarch64-gnu" "4.49.0"
+    "@rollup/rollup-linux-ppc64-gnu" "4.49.0"
+    "@rollup/rollup-linux-riscv64-gnu" "4.49.0"
+    "@rollup/rollup-linux-riscv64-musl" "4.49.0"
+    "@rollup/rollup-linux-s390x-gnu" "4.49.0"
+    "@rollup/rollup-linux-x64-gnu" "4.49.0"
+    "@rollup/rollup-linux-x64-musl" "4.49.0"
+    "@rollup/rollup-win32-arm64-msvc" "4.49.0"
+    "@rollup/rollup-win32-ia32-msvc" "4.49.0"
+    "@rollup/rollup-win32-x64-msvc" "4.49.0"
     fsevents "~2.3.2"
 
 run-parallel@^1.1.9:
@@ -4991,50 +2674,37 @@
   dependencies:
     queue-microtask "^1.2.2"
 
-safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+safe-buffer@5.2.1, safe-buffer@^5.1.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+safe-regex-test@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1"
+  integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==
+  dependencies:
+    call-bound "^1.0.2"
+    es-errors "^1.3.0"
+    is-regex "^1.2.1"
+
+"safer-buffer@>= 2.1.2 < 3":
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-"semver@2 || 3 || 4 || 5":
-  version "5.7.2"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
-  integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
+semver@^7.5.3, semver@^7.7.2:
+  version "7.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+  integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
 
-semver@^6.0.0, semver@^6.3.1:
-  version "6.3.1"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
-  integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-
-semver@^7.3.4, semver@^7.3.5, semver@^7.5.3:
-  version "7.5.4"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
-  integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
-  dependencies:
-    lru-cache "^6.0.0"
-
-serialize-javascript@6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
-  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+serialize-javascript@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
+  integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
   dependencies:
     randombytes "^2.1.0"
 
-set-function-length@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
-  integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==
-  dependencies:
-    define-data-property "^1.1.1"
-    get-intrinsic "^1.2.1"
-    gopd "^1.0.1"
-    has-property-descriptors "^1.0.0"
-
 setprototypeof@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
@@ -5045,35 +2715,72 @@
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
   integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
 
-shady-css-scoped-element@^0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/shady-css-scoped-element/-/shady-css-scoped-element-0.0.2.tgz#c538fcfe2317e979cd02dfec533898b95b4ea8fe"
-  integrity sha512-Dqfl70x6JiwYDujd33ZTbtCK0t52E7+H2swdWQNSTzfsolSa6LJHnTpN4T9OpJJEq4bxuzHRLFO9RBcy/UfrMQ==
-
-side-channel@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
-  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
   dependencies:
-    call-bind "^1.0.0"
-    get-intrinsic "^1.0.2"
-    object-inspect "^1.9.0"
+    shebang-regex "^3.0.0"
 
-signal-exit@^3.0.2:
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+side-channel-list@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
+  integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
+  dependencies:
+    es-errors "^1.3.0"
+    object-inspect "^1.13.3"
+
+side-channel-map@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
+  integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+  dependencies:
+    call-bound "^1.0.2"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.5"
+    object-inspect "^1.13.3"
+
+side-channel-weakmap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
+  integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
+  dependencies:
+    call-bound "^1.0.2"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.5"
+    object-inspect "^1.13.3"
+    side-channel-map "^1.0.1"
+
+side-channel@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
+  integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
+  dependencies:
+    es-errors "^1.3.0"
+    object-inspect "^1.13.3"
+    side-channel-list "^1.0.0"
+    side-channel-map "^1.0.1"
+    side-channel-weakmap "^1.0.2"
+
+signal-exit@^3.0.2, signal-exit@^3.0.3:
   version "3.0.7"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
   integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
 
-sinon@^13.0.2:
-  version "13.0.2"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-13.0.2.tgz#c6a8ddd655dc1415bbdc5ebf0e5b287806850c3a"
-  integrity sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA==
+sinon@^20.0.0:
+  version "20.0.0"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-20.0.0.tgz#4b653468735f7152ba694d05498c2b5d024ab006"
+  integrity sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==
   dependencies:
-    "@sinonjs/commons" "^1.8.3"
-    "@sinonjs/fake-timers" "^9.1.2"
-    "@sinonjs/samsam" "^6.1.1"
-    diff "^5.0.0"
-    nise "^5.1.1"
+    "@sinonjs/commons" "^3.0.1"
+    "@sinonjs/fake-timers" "^13.0.5"
+    "@sinonjs/samsam" "^8.0.1"
+    diff "^7.0.0"
     supports-color "^7.2.0"
 
 slash@^3.0.0:
@@ -5090,35 +2797,29 @@
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
-socket.io-adapter@~2.5.2:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
-  integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==
-  dependencies:
-    ws "~8.11.0"
+smart-buffer@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
+  integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
 
-socket.io-parser@~4.2.4:
-  version "4.2.4"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
-  integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+socks-proxy-agent@^8.0.5:
+  version "8.0.5"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee"
+  integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==
   dependencies:
-    "@socket.io/component-emitter" "~3.1.0"
-    debug "~4.3.1"
+    agent-base "^7.1.2"
+    debug "^4.3.4"
+    socks "^2.8.3"
 
-socket.io@^4.7.2:
-  version "4.7.5"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8"
-  integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==
+socks@^2.8.3:
+  version "2.8.7"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea"
+  integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==
   dependencies:
-    accepts "~1.3.4"
-    base64id "~2.0.0"
-    cors "~2.8.5"
-    debug "~4.3.2"
-    engine.io "~6.5.2"
-    socket.io-adapter "~2.5.2"
-    socket.io-parser "~4.2.4"
+    ip-address "^10.0.1"
+    smart-buffer "^4.2.0"
 
-source-map-support@^0.5.21, source-map-support@~0.5.12:
+source-map-support@^0.5.21:
   version "0.5.21"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
   integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
@@ -5126,82 +2827,37 @@
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
+source-map@^0.6.0, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
 source-map@^0.7.3:
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
-  integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
-
-spdx-correct@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
-  integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==
-  dependencies:
-    spdx-expression-parse "^3.0.0"
-    spdx-license-ids "^3.0.0"
-
-spdx-exceptions@^2.1.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
-  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
-
-spdx-expression-parse@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
-  integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
-  dependencies:
-    spdx-exceptions "^2.1.0"
-    spdx-license-ids "^3.0.0"
-
-spdx-license-ids@^3.0.0:
-  version "3.0.16"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
-  integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
-
-sshpk@^1.7.0:
-  version "1.18.0"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
-  integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==
-  dependencies:
-    asn1 "~0.2.3"
-    assert-plus "^1.0.0"
-    bcrypt-pbkdf "^1.0.0"
-    dashdash "^1.12.0"
-    ecc-jsbn "~0.1.1"
-    getpass "^0.1.1"
-    jsbn "~0.1.0"
-    safer-buffer "^2.0.2"
-    tweetnacl "~0.14.0"
+  version "0.7.6"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02"
+  integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==
 
 statuses@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
   integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
 
-"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.0.0, statuses@^1.5.0, statuses@~1.5.0:
+"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
 
-stream-read-all@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/stream-read-all/-/stream-read-all-3.0.1.tgz#60762ae45e61d93ba0978cda7f3913790052ad96"
-  integrity sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==
-
-streamroller@^3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff"
-  integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==
+streamx@^2.15.0, streamx@^2.21.0:
+  version "2.22.1"
+  resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.22.1.tgz#c97cbb0ce18da4f4db5a971dc9ab68ff5dc7f5a5"
+  integrity sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==
   dependencies:
-    date-format "^4.0.14"
-    debug "^4.3.4"
-    fs-extra "^8.1.0"
+    fast-fifo "^1.3.2"
+    text-decoder "^1.1.0"
+  optionalDependencies:
+    bare-events "^2.2.0"
 
-string-width@^4.1.0, string-width@^4.2.0:
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5210,27 +2866,6 @@
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.1"
 
-string_decoder@^1.1.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
-  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
-  dependencies:
-    safe-buffer "~5.2.0"
-
-strip-ansi@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
-  integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==
-  dependencies:
-    ansi-regex "^3.0.0"
-
-strip-ansi@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
-  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
-  dependencies:
-    ansi-regex "^4.1.0"
-
 strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -5238,30 +2873,16 @@
   dependencies:
     ansi-regex "^5.0.1"
 
-strip-bom@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
-  integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-json-comments@3.1.1:
+strip-json-comments@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-supports-color@8.1.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
-  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
-  dependencies:
-    has-flag "^4.0.0"
-
-supports-color@^5.3.0:
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
-  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
-  dependencies:
-    has-flag "^3.0.0"
-
 supports-color@^7.1.0, supports-color@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -5269,116 +2890,52 @@
   dependencies:
     has-flag "^4.0.0"
 
+supports-color@^8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
 supports-preserve-symlinks-flag@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
-systemjs@^6.3.1, systemjs@^6.8.3:
-  version "6.14.3"
-  resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.3.tgz#c1d6e4ff5f9ff7106e5bb3d451360b1a066bde8a"
-  integrity sha512-hQv45irdhXudAOr8r6SVSpJSGtogdGZUbJBRKCE5nsIS7tsxxvnIHqT4IOPWj+P+HcSzeWzHlGCGpmhPDIKe+w==
-
-table-layout@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04"
-  integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==
+table-layout@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-4.1.1.tgz#0f72965de1a5c0c1419c9ba21cae4e73a2f73a42"
+  integrity sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==
   dependencies:
-    array-back "^4.0.1"
-    deep-extend "~0.6.0"
-    typical "^5.2.0"
-    wordwrapjs "^4.0.0"
-
-table-layout@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-3.0.2.tgz#69c2be44388a5139b48c59cf21e73b488021769a"
-  integrity sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==
-  dependencies:
-    "@75lb/deep-merge" "^1.1.1"
     array-back "^6.2.2"
-    command-line-args "^5.2.1"
-    command-line-usage "^7.0.0"
-    stream-read-all "^3.0.1"
-    typical "^7.1.1"
     wordwrapjs "^5.1.0"
 
-tar-fs@2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
-  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+tar-fs@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.0.tgz#4675e2254d81410e609d91581a762608de999d25"
+  integrity sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==
   dependencies:
-    chownr "^1.1.1"
-    mkdirp-classic "^0.5.2"
     pump "^3.0.0"
-    tar-stream "^2.1.4"
+    tar-stream "^3.1.5"
+  optionalDependencies:
+    bare-fs "^4.0.1"
+    bare-path "^3.0.0"
 
-tar-stream@^2.1.4:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
-  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+tar-stream@^3.1.5:
+  version "3.1.7"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b"
+  integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==
   dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
+    b4a "^1.6.4"
+    fast-fifo "^1.2.0"
+    streamx "^2.15.0"
 
-terser@^4.6.3, terser@^4.6.7, terser@^4.8.1:
-  version "4.8.1"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f"
-  integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==
+text-decoder@^1.1.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65"
+  integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==
   dependencies:
-    commander "^2.20.0"
-    source-map "~0.6.1"
-    source-map-support "~0.5.12"
-
-test-exclude@^5.2.3:
-  version "5.2.3"
-  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
-  integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
-  dependencies:
-    glob "^7.1.3"
-    minimatch "^3.0.4"
-    read-pkg-up "^4.0.0"
-    require-main-filename "^2.0.0"
-
-thenify-all@^1.0.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
-  integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
-  dependencies:
-    thenify ">= 3.1.0 < 4"
-
-"thenify@>= 3.1.0 < 4":
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
-  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
-  dependencies:
-    any-promise "^1.0.0"
-
-through@^2.3.8:
-  version "2.3.8"
-  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
-
-tmp@0.0.x:
-  version "0.0.33"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
-  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
-  dependencies:
-    os-tmpdir "~1.0.2"
-
-tmp@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
-  integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
-  dependencies:
-    rimraf "^3.0.0"
-
-to-fast-properties@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
-  integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+    b4a "^1.6.4"
 
 to-regex-range@^5.0.1:
   version "5.0.1"
@@ -5392,71 +2949,39 @@
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
-tough-cookie@~2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
-  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+tr46@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca"
+  integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
   dependencies:
-    psl "^1.1.28"
-    punycode "^2.1.1"
+    punycode "^2.3.1"
 
-tr46@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
-  integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==
-  dependencies:
-    punycode "^2.1.0"
-
-tr46@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
-  integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
-  dependencies:
-    punycode "^2.1.1"
-
-tr46@~0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
-  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
-
-tslib@^1.11.1:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
-  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-tslib@^2.0.3:
-  version "2.6.2"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
-  integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
+tslib@^2.0.1:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
 
 tsscmp@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
   integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
 
-tunnel-agent@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
-  integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
-  dependencies:
-    safe-buffer "^5.0.1"
-
-tweetnacl@^0.14.3, tweetnacl@~0.14.0:
-  version "0.14.5"
-  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
-  integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
-
-type-detect@4.0.8, type-detect@^4.0.8:
+type-detect@4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
+type-detect@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
+  integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
+
 type-fest@^0.21.3:
   version "0.21.3"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
   integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
 
-type-is@^1.6.16, type-is@~1.6.18:
+type-is@^1.6.16:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
   integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -5464,241 +2989,79 @@
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+typed-query-selector@^2.12.0:
+  version "2.12.0"
+  resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2"
+  integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==
+
 typical@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
   integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
 
-typical@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
-  integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
-
 typical@^7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/typical/-/typical-7.1.1.tgz#ba177ab7ab103b78534463ffa4c0c9754523ac1f"
-  integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==
-
-ua-parser-js@^0.7.30:
-  version "0.7.37"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832"
-  integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==
+  version "7.3.0"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-7.3.0.tgz#930376be344228709f134613911fa22aa09617a4"
+  integrity sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==
 
 ua-parser-js@^1.0.33:
-  version "1.0.37"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
-  integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==
+  version "1.0.41"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.41.tgz#bd04dc9ec830fcf9e4fad35cf22dcedd2e3b4e9c"
+  integrity sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==
 
-unbzip2-stream@1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
-  integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
-  dependencies:
-    buffer "^5.2.1"
-    through "^2.3.8"
+undici-types@~7.10.0:
+  version "7.10.0"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350"
+  integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
 
-undici-types@~5.26.4:
-  version "5.26.5"
-  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
-  integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
-
-unicode-canonical-property-names-ecmascript@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
-  integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==
-
-unicode-match-property-ecmascript@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3"
-  integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==
-  dependencies:
-    unicode-canonical-property-names-ecmascript "^2.0.0"
-    unicode-property-aliases-ecmascript "^2.0.0"
-
-unicode-match-property-value-ecmascript@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0"
-  integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==
-
-unicode-property-aliases-ecmascript@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
-  integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
-
-universalify@^0.1.0:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
-  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
-
-unpipe@1.0.0, unpipe@~1.0.0:
+unpipe@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
   integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
 
-update-browserslist-db@^1.0.13:
-  version "1.0.13"
-  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
-  integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==
-  dependencies:
-    escalade "^3.1.1"
-    picocolors "^1.0.0"
-
-uri-js@^4.2.2:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
-  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
-  dependencies:
-    punycode "^2.1.0"
-
-useragent@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
-  integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==
-  dependencies:
-    lru-cache "4.1.x"
-    tmp "0.0.x"
-
-util-deprecate@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-utils-merge@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-  integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
-
-uuid@^3.3.2:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
-  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-
-v8-to-istanbul@^8.0.0:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"
-  integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==
-  dependencies:
-    "@types/istanbul-lib-coverage" "^2.0.1"
-    convert-source-map "^1.6.0"
-    source-map "^0.7.3"
-
 v8-to-istanbul@^9.0.1:
-  version "9.2.0"
-  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"
-  integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"
+  integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==
   dependencies:
     "@jridgewell/trace-mapping" "^0.3.12"
     "@types/istanbul-lib-coverage" "^2.0.1"
     convert-source-map "^2.0.0"
 
-valid-url@^1.0.9:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
-  integrity sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==
-
-validate-npm-package-license@^3.0.1:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
-  integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
-  dependencies:
-    spdx-correct "^3.0.0"
-    spdx-expression-parse "^3.0.0"
-
-vary@^1, vary@^1.1.2:
+vary@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
 
-verror@1.10.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
-  integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
-  dependencies:
-    assert-plus "^1.0.0"
-    core-util-is "1.0.2"
-    extsprintf "^1.2.0"
-
-void-elements@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-  integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==
-
-webidl-conversions@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
-  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
-
-webidl-conversions@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
-  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
-
 webidl-conversions@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
   integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
 
-whatwg-fetch@^3.0.0, whatwg-fetch@^3.5.0:
-  version "3.6.20"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70"
-  integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==
-
-whatwg-url@^11.0.0:
-  version "11.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
-  integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+whatwg-url@^14.0.0:
+  version "14.2.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663"
+  integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
   dependencies:
-    tr46 "^3.0.0"
+    tr46 "^5.1.0"
     webidl-conversions "^7.0.0"
 
-whatwg-url@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
-  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
-  dependencies:
-    tr46 "~0.0.3"
-    webidl-conversions "^3.0.0"
-
-whatwg-url@^7.0.0, whatwg-url@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
-  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
-  dependencies:
-    lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
-
-which@2.0.2:
+which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
     isexe "^2.0.0"
 
-which@^1.2.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
-  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
-  dependencies:
-    isexe "^2.0.0"
-
-wordwrapjs@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f"
-  integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==
-  dependencies:
-    reduce-flatten "^2.0.0"
-    typical "^5.2.0"
-
 wordwrapjs@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-5.1.0.tgz#4c4d20446dcc670b14fa115ef4f8fd9947af2b3a"
   integrity sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==
 
-workerpool@6.2.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
-  integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
+workerpool@^6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
+  integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
 
 wrap-ansi@^6.2.0:
   version "6.2.0"
@@ -5723,57 +3086,32 @@
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
-ws@8.5.0:
-  version "8.5.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
-  integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
+ws@^7.5.10:
+  version "7.5.10"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
+  integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
 
-ws@^7.4.2:
-  version "7.5.9"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
-  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
-
-ws@~8.11.0:
-  version "8.11.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
-  integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
+ws@^8.18.3:
+  version "8.18.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
+  integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
 
 y18n@^5.0.5:
   version "5.0.8"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
-yallist@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
-  integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==
-
-yallist@^3.0.2:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
-  integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
-
-yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
-yamlparser@^0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/yamlparser/-/yamlparser-0.0.2.tgz#32393e6afc70c8ca066b6650ac6738b481678ebc"
-  integrity sha512-Cou9FCGblEENtn1/8La5wkDM/ISMh2bzu5Wh7dYzCzA0o9jD4YGyLkUJxe84oPBGoB92f+Oy4ZjVhA8S0C2wlQ==
-
-yargs-parser@20.2.4:
-  version "20.2.4"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
-  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
-
-yargs-parser@^20.2.2:
+yargs-parser@^20.2.2, yargs-parser@^20.2.9:
   version "20.2.9"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
   integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
 
-yargs-unparser@2.0.0:
+yargs-parser@^21.1.1:
+  version "21.1.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+  integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs-unparser@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
   integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
@@ -5783,7 +3121,7 @@
     flat "^5.0.2"
     is-plain-obj "^2.1.0"
 
-yargs@16.2.0, yargs@^16.1.1:
+yargs@^16.2.0:
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
   integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
@@ -5796,6 +3134,19 @@
     y18n "^5.0.5"
     yargs-parser "^20.2.2"
 
+yargs@^17.7.2:
+  version "17.7.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
+  dependencies:
+    cliui "^8.0.1"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.3"
+    y18n "^5.0.5"
+    yargs-parser "^21.1.1"
+
 yauzl@^2.10.0:
   version "2.10.0"
   resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
@@ -5805,11 +3156,16 @@
     fd-slicer "~1.1.0"
 
 ylru@^1.2.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785"
-  integrity sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.4.0.tgz#0cf0aa57e9c24f8a2cbde0cc1ca2c9592ac4e0f6"
+  integrity sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==
 
 yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zod@^3.24.1:
+  version "3.25.76"
+  resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
+  integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
diff --git a/proto/BUILD b/proto/BUILD
index 624568b..c6b5627 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -24,8 +24,13 @@
     deps = [":entities_proto"],
 )
 
-cc_proto_library(
-    name = "entities_cc_proto",
+proto_library(
+    name = "flow_proto",
+    srcs = ["flow.proto"],
+)
+
+java_proto_library(
+    name = "flow_java_proto",
     visibility = ["//visibility:public"],
-    deps = [":entities_proto"],
+    deps = [":flow_proto"],
 )
diff --git a/proto/cache.proto b/proto/cache.proto
index cfdbd4e..4f8d1dc 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -715,7 +715,7 @@
 
 // Serialized form of
 // com.google.gerrit.server.patch.filediff.FileDiffOutput
-// Next ID: 15
+// Next ID: 17
 message FileDiffOutputProto {
   // Next ID: 5
   message Edit {
@@ -749,4 +749,38 @@
   bool negative = 12;
   string old_mode = 13; // ENUM as string
   string new_mode = 14; // ENUM as string
+  bytes old_sha = 15;
+  bytes new_sha = 16;
+}
+
+// Serialized form of com.google.gerrit.server.ReviewerSet
+// Next ID: 2
+message ReviewerSetProto {
+  // Next ID: 4
+  message ReviewerProto {
+    devtools.gerritcodereview.Account_Id account_id = 1;
+    string state = 2;
+    int64 timestamp = 3;
+  }
+  repeated ReviewerProto reviewers = 1;
+}
+
+// Serialized form of com.google.gerrit.server.git.ChangesByProjectCacheImpl.CachedProjectChanges
+// Next ID: 3
+message CachedProjectChangesProto {
+  // Next ID: 2
+  message NonPrivateChangesProto {
+    map<int32, bytes> changes = 1;
+  }
+
+  // Serialized form of com.google.gerrit.server.git.ChangesByProjectCacheImpl.PrivateChange
+  // Next ID: 4
+  message PrivateChangeProto {
+    devtools.gerritcodereview.Change change = 1;
+    ReviewerSetProto reviewers = 2;
+    bytes meta_revision = 3;
+  }
+
+  map<string, NonPrivateChangesProto> non_private_changes_by_branch = 1;
+  map<int32, PrivateChangeProto> private_changes = 2;
 }
diff --git a/proto/entities.proto b/proto/entities.proto
index 5ecd258..4f222c2 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -116,13 +116,26 @@
 }
 
 // Serialized form of com.google.gerrit.entities.PatchSet.Conflicts.
-// Next ID: 4
+// Next ID: 7
 message Conflicts {
+  optional ObjectId base = 4;
   optional ObjectId ours = 1;
   optional ObjectId theirs = 2;
   optional bool containsConflicts = 3;
+  optional string merge_strategy = 5;
+  optional NoMergeBaseReason noBaseReason = 6;
 }
 
+// Serialized form of com.google.gerrit.extensions.common.NoMergeBaseReason.
+// Next ID: 3
+enum NoMergeBaseReason {
+  HISTORIC_DATA_WITHOUT_BASE = 0;
+  NO_COMMON_ANCESTOR = 1;
+  COMPUTED_BASE = 2;
+  ONE_SIDED_MERGE_STRATEGY = 3;
+  NO_MERGE_PERFORMED = 4;
+ }
+
 // Serialized form of com.google.gerrit.extensions.common.MergeInput.
 // Next ID: 5
 message MergeInput {
@@ -140,7 +153,7 @@
 }
 
 // Serialized form of com.google.gerrit.extensions.api.accounts.AccountInput.
-// Next ID: 8
+// Next ID: 9
 message AccountInput {
  optional string username = 1;
  optional string name = 2;
@@ -149,6 +162,15 @@
  optional string ssh_key = 5;
  optional string http_password = 6;
  repeated string groups = 7;
+ repeated AuthTokenInput tokens = 8;
+}
+
+// Serialized form of com.google.gerrit.extensions.auth.AuthTokenInput.
+// Next ID: 4
+message AuthTokenInput {
+ optional string id = 1;
+ optional string token = 2;
+ optional string lifetime = 3;
 }
 
 // Serialized form of com.google.gerrit.extensions.client.ListChangesOption.
@@ -461,7 +483,7 @@
     // 0-based
     optional int32 end_char = 4;
   }
-  
+
   // Next Id: 5
   message InFilePosition {
     optional string file_path = 1;
diff --git a/proto/flow.proto b/proto/flow.proto
new file mode 100644
index 0000000..87e8c2b
--- /dev/null
+++ b/proto/flow.proto
@@ -0,0 +1,48 @@
+syntax = "proto2";
+
+package devtools.gerritcodereview.flow;
+
+option java_package = "com.google.gerrit.server.flow.proto";
+option java_multiple_files = true;
+
+message FlowAction {
+  // The name of the action.
+  optional string name = 1;
+  // Parameters for the action.
+  repeated string parameters = 2;
+}
+
+message FlowExpression {
+  // The condition which must be satisfied for the action to be triggered.
+  //
+  // Can contain multiple obstacles separated by comma.
+  optional string condition = 1;
+  // The action that should be triggered when the condition is satisfied.
+  optional FlowAction action = 2;
+}
+
+message FlowStageEvaluationStatus {
+  enum State {
+    UNKNOWN = 0;
+    // The previous stages are not finished yet, the condition of the stage is
+    // not satisfied yet or the action has not been executed yet.
+    PENDING = 1;
+    // The condition of the stage is satisfied and the action has been executed.
+    DONE = 2;
+    // The stage has a non-recoverable error, e.g. performing the action has
+    // failed.
+    FAILED = 3;
+    // The stage has been terminated without having been executed, e.g. because
+    // a previous stage failed or because it wasn't done within a timeout.
+    TERMINATED = 4;
+  }
+  optional State state = 1;
+  // Message contains extra information about the stage execution. Such as error
+  // details if the stage failed.
+  optional string message = 2;
+  // The timestamp at which all previous stages completed and evaluation of the
+  // current this stage started.
+  optional int64 start_time_millis = 3;
+  // The timestamp at which the stage became DONE/FAILED/TERMINATED.
+  optional int64 end_time_millis = 4;
+}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 86970f8..eb84a2c 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -33,6 +33,7 @@
   {@param? defaultDashboardHex: ?}
   {@param? dashboardQuery: ?}
   {@param? userIsAuthenticated: ?}
+  {@param? submitRequirementsHex: ?}
   <!DOCTYPE html>{\n}
   <html lang="en">{\n}
   <meta charset="utf-8">{\n}
@@ -95,9 +96,11 @@
       {if $userIsAuthenticated}
         <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/edit/?download-commands=true" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
       {/if}
+      {if $submitRequirementsHex}
+        <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}?O={$submitRequirementsHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+      {/if}
     {/if}
     <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
-    <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/robotcomments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
     <link rel="preload" href="{$canonicalPath}/changes/?q=change:{$changeNum}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
     {if $userIsAuthenticated}
       <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/drafts?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
@@ -141,9 +144,9 @@
   {/if}
   <link rel="preload" as="style" href="{$staticResourcePath}/styles/main.css">{\n}
 
-  <script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
+  <script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-loader.js"></script>{\n}
 
-  // Content between webcomponents-lite and the load of the main app element
+  // Content between webcomponents-loader and the load of the main app element
   // run before polymer-resin is installed so may have security consequences.
   // Contact your local security engineer if you have any questions, and
   // CC them on any changes that load content before gr-app.js.
diff --git a/resources/com/google/gerrit/server/change/ChangeMessages.properties b/resources/com/google/gerrit/server/change/ChangeMessages.properties
deleted file mode 100644
index c78f5a3..0000000
--- a/resources/com/google/gerrit/server/change/ChangeMessages.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
-revertSubmissionDefaultMessage = This reverts commit {0}.
-revertSubmissionUserMessage = Revert \"{0}\"\n\n{1}
-revertSubmissionOfRevertSubmissionUserMessage = Revert^{0} \"{1}\"\n\n{2}
-
-reviewerCantSeeChange = {0} does not have permission to see this change
-reviewerInvalid = {0} is not a valid user identifier
-reviewerNotFoundUserOrGroup = {0} does not identify a registered user or group
-
-groupRemovalIsNotAllowed = Groups can't be removed from reviewers, so can't remove {0}.
-groupIsNotAllowed = The group {0} cannot be added as reviewer.
-groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
-groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
diff --git a/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/resources/com/google/gerrit/server/config/CapabilityConstants.properties
deleted file mode 100644
index 95206f9..0000000
--- a/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ /dev/null
@@ -1,25 +0,0 @@
-accessDatabase = Access Database
-administrateServer = Administrate Server
-batchChangesLimit = Batch Changes Limit
-createAccount = Create Account
-createGroup = Create Group
-deleteGroup = Delete Group
-createProject = Create Project
-emailReviewers = Email Reviewers
-flushCaches = Flush Caches
-killTask = Kill Task
-maintainServer = Maintain Server
-modifyAccount = Modify Account
-priority = Priority
-readAs = Read As
-queryLimit = Query Limit
-runAs = Run As
-runGC = Run Garbage Collection
-streamEvents = Stream Events
-viewAccess = View Access
-viewAllAccounts = View All Accounts
-viewCaches = View Caches
-viewConnections = View Connections
-viewPlugins = View Plugins
-viewQueue = View Queue
-viewSecondaryEmails = View Secondary Emails
diff --git a/resources/com/google/gerrit/server/mail/AuthTokenExpired.soy b/resources/com/google/gerrit/server/mail/AuthTokenExpired.soy
new file mode 100644
index 0000000..3c63d51
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AuthTokenExpired.soy
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template.AuthTokenExpired}
+
+/**
+ * The .AuthTokenExpired template will determine the contents of the email related to
+ * the token being expired.
+ */
+{template AuthTokenExpired kind="text"}
+  {@param email: ?}
+  The authentication token with id "{$email.tokenId}" on Gerrit Code Review at
+  {sp}{$email.gerritHost} has expired at {$email.expirationDate}.
+
+  {\n}
+  {\n}
+
+  To generate a new token visit
+  {\n}
+  {$email.authTokenSettingsUrl}.
+  {\n}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/AuthTokenExpiredHtml.soy b/resources/com/google/gerrit/server/mail/AuthTokenExpiredHtml.soy
new file mode 100644
index 0000000..298e187
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AuthTokenExpiredHtml.soy
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template.AuthTokenExpiredHtml}
+
+{template AuthTokenExpiredHtml}
+  {@param email: ?}
+  <p>
+    The authentication token with id "{$email.tokenId}" on Gerrit Code Review
+    at {$email.gerritHost} has expired at {$email.expirationDate}.
+  </p>
+
+  <p>
+    To generate a new token visit{sp}
+    <a href="{$email.authTokenSettingsUrl}">this link</a>.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/AuthTokenUpdate.soy b/resources/com/google/gerrit/server/mail/AuthTokenUpdate.soy
new file mode 100644
index 0000000..d2df39c
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AuthTokenUpdate.soy
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template.AuthTokenUpdate}
+
+/**
+ * The .AuthTokenUpdate template will determine the contents of the email related to
+ * adding, changing or deleting the token.
+ */
+{template AuthTokenUpdate kind="text"}
+  {@param email: ?}
+  The authentication token with id "{$email.tokenId}" was {$email.operation}{sp}
+  on Gerrit Code Review at {sp}{$email.gerritHost}.
+
+  If this is not expected, please contact your Gerrit Administrators
+  immediately.
+
+  {\n}
+  {\n}
+
+  You can also manage your authentication tokens by visiting
+  {\n}
+  {$email.authTokenSettingsUrl}
+  {\n}
+  {if $email.userNameEmail}
+    (while signed in as {$email.userNameEmail})
+  {else}
+    (while signed in as {$email.email})
+  {/if}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/AuthTokenUpdateHtml.soy b/resources/com/google/gerrit/server/mail/AuthTokenUpdateHtml.soy
new file mode 100644
index 0000000..f50ef63
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AuthTokenUpdateHtml.soy
@@ -0,0 +1,46 @@
+/**
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template.AuthTokenUpdateHtml}
+
+{template AuthTokenUpdateHtml}
+  {@param email: ?}
+  <p>
+    The authentication tokens with id "{$email.tokenId}" was {$email.operation}{sp}
+    on Gerrit Code Review at {$email.gerritHost}.
+  </p>
+
+  <p>
+    If this is not expected, please contact your Gerrit Administrators
+    immediately.
+  </p>
+
+  <p>
+    You can also manage your authentication tokens by following{sp}
+    <a href="{$email.authTokenSettingsUrl}">this link</a>
+    {sp}
+    {if $email.userNameEmail}
+      (while signed in as {$email.userNameEmail})
+    {else}
+      (while signed in as {$email.email})
+    {/if}.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/AuthTokenWillExpire.soy b/resources/com/google/gerrit/server/mail/AuthTokenWillExpire.soy
new file mode 100644
index 0000000..53e6f13
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AuthTokenWillExpire.soy
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template.AuthTokenWillExpire}
+
+/**
+ * The AuthTokenWillExpire template will determine the contents of the email related to
+ * the token about to expire.
+ */
+{template AuthTokenWillExpire kind="text"}
+  {@param email: ?}
+  The authentication token with id "{$email.tokenId}" on Gerrit Code Review at
+  {sp}{$email.gerritHost} will expire on {$email.expirationDate}.
+
+  {\n}
+  {\n}
+
+  To generate a new token visit
+  {\n}
+  {$email.authTokenSettingsUrl}.
+  {\n}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/AuthTokenWillExpireHtml.soy b/resources/com/google/gerrit/server/mail/AuthTokenWillExpireHtml.soy
new file mode 100644
index 0000000..5fc1aa2
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AuthTokenWillExpireHtml.soy
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{namespace com.google.gerrit.server.mail.template.AuthTokenWillExpireHtml}
+
+{template AuthTokenWillExpireHtml}
+  {@param email: ?}
+  <p>
+    The authentication token with id "{$email.tokenId}" on Gerrit Code Review
+    at {$email.gerritHost} will expire on {$email.expirationDate}.
+  </p>
+
+  <p>
+    To generate a new token visit{sp}
+    <a href="{$email.authTokenSettingsUrl}">this link</a>.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index 8c1840c..9d33c10 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -69,11 +69,6 @@
     {\n}
 
     {for $comment in $group.comments}
-      {if $comment.isRobotComment}
-        Robot Comment from {$comment.robotId} (run ID {$comment.robotRunId}):
-        {\n}
-      {/if}
-
       {for $line, $index in $comment.lines}
         {if $index == 0}
           {if $comment.startLine != 0}
diff --git a/resources/com/google/gerrit/server/mail/CommentHtml.soy b/resources/com/google/gerrit/server/mail/CommentHtml.soy
index 320122e..efa8e5a 100644
--- a/resources/com/google/gerrit/server/mail/CommentHtml.soy
+++ b/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -149,16 +149,6 @@
         <ul style="{$ulStyle}">
           {for $comment in $group.comments}
             <li style="{$commentLiStyle}">
-              {if $comment.isRobotComment}
-                <p style="{$commentHeaderStyle}">
-                  Robot Comment from{sp}
-                  {if $comment.robotUrl}<a href="{$comment.robotUrl}">{/if}
-                  {$comment.robotId}
-                  {if $comment.robotUrl}</a>{/if}{sp}
-                  (run ID {$comment.robotRunId}):
-                </p>
-              {/if}
-
               <p style="{$commentHeaderStyle}">
                 <a href="{$comment.link}">
                   {if $comment.startLine == 0}
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index db68105..93c88e5 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -15,6 +15,7 @@
 BUCK = text/x-python
 BUILD = text/x-python
 bazel = text/x-python
+bzlmod = text/x-python
 c = text/x-csrc
 cfg = text/x-ttcn-cfg
 cjs = text/javascript
@@ -85,6 +86,8 @@
 f90 = text/x-fortran
 gitmodules = text/x-ini
 glsl = x-shader/x-vertex
+gn = text/x-gn
+gni = text/x-gn
 go = text/x-go
 gradle = text/x-groovy
 gradlew = text/x-sh
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 57d30a1..0827458 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -107,6 +107,9 @@
         srcs = srcs + [
             "@plugins_npm//:node_modules",
         ],
+        args = [
+            "--bundleConfigAsCjs=true",
+        ],
         entry_point = entry_point,
         format = "iife",
         rollup_bin = "//tools/node_tools:rollup-bin",
@@ -114,7 +117,7 @@
         sourcemap = "hidden",
         config_file = "//plugins:rollup.config.js",
         deps = [
-            "@tools_npm//rollup-plugin-node-resolve",
+            "@tools_npm//@rollup/plugin-node-resolve",
         ],
     )
 
@@ -144,39 +147,6 @@
         ]),
     )
 
-def karma_test(name, srcs, data):
-    """Creates a Karma test target.
-
-    It can be used both for the main Gerrit js bundle, but also for plugins. So
-    it should be extremely easy to add Karma test capabilities for new plugins.
-
-    We are sharing one karma.conf.js file. If you want to customize that, then
-    consider using command line arguments that the config file can process, see
-    the `root` argument for an example.
-
-    Args:
-      name: The name of the test rule.
-      srcs: The shell script to invoke, where you can set command line
-        arguments for Karma and its config.
-      data: The bundle of JavaScript files with the tests included.
-    """
-
-    native.sh_test(
-        name = name,
-        size = "enormous",
-        srcs = srcs,
-        args = [
-            "$(location //polygerrit-ui:karma_bin)",
-            "$(location //polygerrit-ui:karma.conf.js)",
-        ],
-        data = data + [
-            "//polygerrit-ui:karma_bin",
-            "//polygerrit-ui:karma.conf.js",
-        ],
-        # Should not run sandboxed.
-        tags = ["karma", "local", "manual"],
-    )
-
 def web_test_runner(name, srcs, data):
     """Creates a Web Test Runner test target.
 
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index 7b1375a..f2ae73f 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -50,7 +50,8 @@
     # post process the XML into our favorite format.
     native.genrule(
         name = "gen_license_txt_" + name,
-        cmd = "python3 $(location //tools/bzl:license-map.py) %s %s %s > $@" % (" ".join(opts), " ".join(json_maps_locations), " ".join(xmls)),
+        # Use sed 's/\\r$$//' to remove non-Unix line endings.
+        cmd = "python3 $(location //tools/bzl:license-map.py) %s %s %s | sed 's/\\r$$//' > $@" % (" ".join(opts), " ".join(json_maps_locations), " ".join(xmls)),
         outs = [name + ".gen.txt"],
         tools = tools,
         **kwargs
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index d2470d3..bfe8e87 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -32,6 +32,23 @@
         target_suffix = "",
         deploy_env = [],
         **kwargs):
+    """Builds a Gerrit plugin.
+
+    Args:
+      name: The name of the plugin.
+      deps: List of additional dependencies for the plugin.
+      provided_deps: List of dependencies that are provided by Gerrit and should not be bundled.
+      srcs: List of Java source files for the plugin.
+      resources: List of resource files to be included in the plugin JAR.
+      resource_jars: List of JARs containing resources.
+      runtime_deps: List of runtime dependencies.
+      manifest_entries: List of additional lines to add to the plugin's manifest file.
+      dir_name: The directory name for the plugin, used in stamping. Defaults to `name`.
+      target_suffix: Suffix to append to the final plugin JAR name.
+      deploy_env: Environment variables for the deploy JAR.
+      **kwargs: Additional arguments passed to the underlying `java_library` and `java_binary` rules.
+
+    This rule creates a deployable .jar file for a Gerrit plugin."""
     java_library(
         name = name + "__plugin",
         srcs = srcs,
diff --git a/tools/deps.bzl b/tools/deps.bzl
index ab7f533..cca3100 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -371,8 +371,8 @@
 
     maven_jar(
         name = "icu4j",
-        artifact = "com.ibm.icu:icu4j:57.1",
-        sha1 = "198ea005f41219f038f4291f0b0e9f3259730e92",
+        artifact = "com.ibm.icu:icu4j:77.1",
+        sha1 = "38693cf0b1d7362a8b726af74dc06026a7c23809",
     )
 
     maven_jar(
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 2c70939..6b2a22c 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -24,6 +24,7 @@
     "//lib/auto:auto-value",
     "//lib/prolog:compiler-lib",
     "//proto:entities_java_proto",
+    "//proto:flow_java_proto",
 ]
 
 java_library(
diff --git a/tools/js/eslint-rules/BUILD b/tools/js/eslint-rules/BUILD
deleted file mode 100644
index 476c4ff..0000000
--- a/tools/js/eslint-rules/BUILD
+++ /dev/null
@@ -1,11 +0,0 @@
-package(default_visibility = ["//visibility:public"])
-
-# To load eslint rules from a directory, we must pass a directory
-# name to it. We can't get the directory name in bazel, but we can calculate
-# use a file from this directory. We are using README.md for it.
-exports_files(["README.md"])
-
-filegroup(
-    name = "eslint-rules-srcs",
-    srcs = glob(["**/*.js"]),
-)
diff --git a/tools/js/eslint-rules/README.md b/tools/js/eslint-rules/README.md
deleted file mode 100644
index b425d74..0000000
--- a/tools/js/eslint-rules/README.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# Eslint rules for polygerrit
-This directory contains custom eslint rules for polygerrit.
-
-## ts-imports-js
-This rule must be used only for `.ts` files.
-The rule ensures that:
-* All import paths either a relative paths or module imports.
-```typescript
-// Correct imports
-import './file1'; // relative path
-import '../abc/file2'; // relative path
-import 'module_name/xyz'; // import from the module_name
-
-// Incorrect imports
-import '/usr/home/file3'; // absolute path
-```
-* All *relative* import paths has a short form (i.e. don't include extension):
-```typescript
-// Correct imports
-import './file1'; // relative path without extension
-import data from 'module_name/file2.json'; // file in a module, can be anything
-
-// Incorrect imports
-import './file1.js'; // relative path with extension
-```
-
-* Imported `.js` and `.d.ts` files both exists (only for a relative import path):
-
-Example:
-```
-polygerrit-ui/app
- |- ex.ts
- |- abc
-     |- correct_ts.ts
-     |- correct_js.js
-     |- correct_js.d.ts
-     |- incorrect_1.js
-     |- incorrect_2.d.ts
-```
-```typescript
-// The ex.ts file:
-// Correct imports
-import {x} from './abc/correct_js'; // correct_js.js and correct_js.d.ts exist
-import {x} from './abc/correct_ts'; // import from .ts - d.ts is not required
-
-// Incorrect imports
-import {x} from './abc/incorrect_1'; // incorrect_1.d.ts doesn't exist
-import {x} from './abc/incorrect_2'; // incorrect_2.js doesn't exist
-```
-
-To fix the last two imports 2 files must be added: `incorrect_1.d.ts` and
-`incorrect_2.js`.
-
-## goog-module-id
-Enforce correct usage of goog.declareModuleId:
-* The goog.declareModuleId must be used only in `.js` files which have
-associated `.d.ts` files.
-* The module name is correct. The correct module name is constructed from the
-file path using the folowing rules
-rules:
-  1. Get part of the path after the '/polygerrit-ui/app/':
-    `/usr/home/gerrit/polygerrit-ui/app/elements/shared/x/y.js` ->
-    `elements/shared/x/y.js`
-  2. Discard `.js` extension and replace all `/` with `.`:
-    `elements/shared/x/y.js` -> `elements.shared.x.y`
-  3. Add `polygerrit.` prefix:
-    `elements.shared.x.y` -> `polygerrit.elements.shared.x.y`
-    The last string is a module name.
-
-Example:
-```javascript
-// polygerrit-ui/app/elements/shared/x/y.js
-goog.declareModuleId('polygerrit.elements.shared.x.y');
-```
diff --git a/tools/js/eslint.bzl b/tools/js/eslint.bzl
index 320f8da..91090d2 100644
--- a/tools/js/eslint.bzl
+++ b/tools/js/eslint.bzl
@@ -25,29 +25,29 @@
     eslint(
         name = "lint",
         srcs = native.glob(["**/*.ts"]),
-        config = ".eslintrc.js",
+        config = "eslint.config.js",
         data = [
             "tsconfig.json",
-            "//plugins:.eslintrc.js",
+            "//plugins:eslint.config.js",
             "//plugins:.prettierrc.js",
             "//plugins:tsconfig-plugins-base.json",
             "@npm//typescript",
         ],
         extensions = [".ts"],
-        ignore = "//plugins:.eslintignore",
         plugins = [
             "@npm//eslint-config-google",
             "@npm//eslint-plugin-html",
             "@npm//eslint-plugin-import",
             "@npm//eslint-plugin-jsdoc",
             "@npm//eslint-plugin-lit",
+            "@npm//eslint-plugin-n",
             "@npm//eslint-plugin-prettier",
             "@npm//eslint-plugin-regex",
             "@npm//gts",
         ],
     )
 
-def eslint(name, plugins, srcs, config, ignore, size = "large", extensions = [".js"], data = []):
+def eslint(name, plugins, srcs, config, size = "large", extensions = [".js"], data = []):
     """ Macro to define eslint rules for files.
 
     Args:
@@ -55,7 +55,6 @@
         plugins: list of npm dependencies with plugins, for example "@npm//eslint-config-google"
         srcs: list of files to be checked (ignored in {name}_bin rule)
         config: eslint config file
-        ignore: eslint ignore file
         size: eslint test size, supported values are: small, medium, large and enormous,
             with implied timeout labels: short, moderate, long, and eternal
         extensions: list of file extensions to be checked. This is an additional filter for
@@ -74,24 +73,10 @@
     """
     entry_point = "@npm//:node_modules/eslint/bin/eslint.js"
 
-    # There are custom eslint rules in eslint-rules directory. Eslint loads
-    # custom rules from a directory specified with the --rulesdir argument.
-    # When bazel runs eslint, it places the eslint-rules directory into
-    # some location in the filesystem, and the location is not known in advance.
-    # It is not possible to get the directory location in bazel directly.
-    # Instead, we can use dirname to get a directory for a file in the
-    # eslint-rules directory.
-    # README.md is the most "stable" file in the eslint-rules directory
-    # (i.e. it is unlikely will be removed), and we are using it to calculate
-    # exact directory path in bazel.
-    eslint_rules_toplevel_file = "//tools/js/eslint-rules:README.md"
     bin_data = [
         "@npm//eslint:eslint",
         config,
-        ignore,
-        "//tools/js/eslint-rules:eslint-rules-srcs",
         "//tools/js:eslint-chdir.js",
-        eslint_rules_toplevel_file,
     ] + plugins + data
     common_templated_args = [
         "--node_options=--require=$$(rlocation $(rootpath //tools/js:eslint-chdir.js))",
@@ -102,11 +87,6 @@
         # See note and example here:
         # https://bazelbuild.github.io/rules_nodejs/Built-ins.html#nodejs_binary
         "$$(rlocation $(rootpath {}))".format(config),
-        "--ignore-path",
-        "$$(rlocation $(rootpath {}))".format(ignore),
-        # Load custom rules from eslint-rules directory
-        "--rulesdir",
-        "$$(dirname $$(rlocation $(rootpath {})))".format(eslint_rules_toplevel_file),
     ]
     nodejs_test(
         name = name + "_test",
@@ -119,6 +99,8 @@
             "--ignore-pattern",
             "*_test_require_patch.js",
             "--ignore-pattern",
+            "*_test_require_patch.cjs",
+            "--ignore-pattern",
             "*_test_loader.js",
             "./",  # Relative to the config file location
         ],
@@ -141,6 +123,8 @@
             "--ignore-pattern",
             "*_bin_require_patch.js",
             "--ignore-pattern",
+            "*_bin_require_patch.cjs",
+            "--ignore-pattern",
             "*_bin_loader.js",
         ],
     )
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 50c3c74..0596126 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.12.9-SNAPSHOT</version>
+  <version>3.13.8-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index e7d6f66..db78bbb 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.12.9-SNAPSHOT</version>
+  <version>3.13.8-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index d2ae30f..dbd3db0 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.12.9-SNAPSHOT</version>
+  <version>3.13.8-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 4520494..ffe5b82 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.12.9-SNAPSHOT</version>
+  <version>3.13.8-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/tools/node_tools/BUILD b/tools/node_tools/BUILD
index b14c0c6..a36d3f8 100644
--- a/tools/node_tools/BUILD
+++ b/tools/node_tools/BUILD
@@ -12,8 +12,8 @@
     # Otherwise remote build execution fails with the too many
     # files error when it builds :release target.
     data = [
+        "@tools_npm//@rollup/plugin-terser",
         "@tools_npm//rollup",
-        "@tools_npm//rollup-plugin-terser",
     ],
     # The entry point must be "@tools_npm:node_modules/rollup/dist/bin/rollup",
     # But bazel doesn't run it correctly with the following command line:
diff --git a/tools/node_tools/node_modules_licenses/BUILD b/tools/node_tools/node_modules_licenses/BUILD
index 03e9c2b5..f80b3ee 100644
--- a/tools/node_tools/node_modules_licenses/BUILD
+++ b/tools/node_tools/node_modules_licenses/BUILD
@@ -24,6 +24,9 @@
 # As a workaround we are using rollup_bundle to group all files together.
 rollup_bundle(
     name = "license-map-generator-bundle",
+    args = [
+        "--bundleConfigAsCjs=true",
+    ],
     config_file = "rollup.config.js",
     entry_point = "license-map-generator.ts",
     format = "cjs",
@@ -31,8 +34,8 @@
     silent = True,
     deps = [
         ":licenses-map",
+        "@tools_npm//@rollup/plugin-node-resolve",
         "@tools_npm//rollup",
-        "@tools_npm//rollup-plugin-node-resolve",
     ],
 )
 
diff --git a/tools/node_tools/node_modules_licenses/tsconfig.json b/tools/node_tools/node_modules_licenses/tsconfig.json
index 4a57571..9a76949 100644
--- a/tools/node_tools/node_modules_licenses/tsconfig.json
+++ b/tools/node_tools/node_modules_licenses/tsconfig.json
@@ -8,8 +8,8 @@
         ]
       }
     ],
-    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
+    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index bd4eb5c..4016d1f 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -3,28 +3,23 @@
   "description": "Gerrit Build Tools",
   "browser": false,
   "dependencies": {
-    "@bazel/rollup": "^5.5.0",
-    "@bazel/typescript": "^5.5.0",
-    "@bazel/concatjs": "^5.5.0",
-    "@types/node": "^10.17.12",
+    "@bazel/rollup": "^5.8.1",
+    "@bazel/typescript": "^5.8.1",
+    "@bazel/concatjs": "^5.8.1",
+    "@types/node": "^22.15.2",
     "@types/parse5": "^4.0.0",
     "@types/parse5-html-rewriting-stream": "^5.1.2",
     "dom5": "^3.0.1",
     "parse5-html-rewriting-stream": "^5.1.1",
-    "rollup": "^2.79.1",
-    "rollup-plugin-commonjs": "^10.1.0",
-    "rollup-plugin-define": "^1.0.1",
-    "rollup-plugin-node-resolve": "^5.2.0",
-    "rollup-plugin-terser": "^7.0.2",
-    "terser": "~5.8.0",
-    "typescript": "^4.9.5"
+    "rollup": "^4.40.0",
+    "@rollup/plugin-commonjs": "^28.0.3",
+    "@rollup/plugin-replace": "^6.0.2",
+    "@rollup/plugin-node-resolve": "^16.0.1",
+    "@rollup/plugin-terser": "^0.4.4",
+    "terser": "~5.39.0",
+    "typescript": "^5.8.3"
   },
   "devDependencies": {},
   "license": "Apache-2.0",
-  "private": true,
-  "resolutions": {
-    "lodash": "4.17.21",
-    "wct-local": "2.1.6",
-    "launchpad": "git+https://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be"
-  }
+  "private": true
 }
\ No newline at end of file
diff --git a/tools/node_tools/polygerrit_app_preprocessor/BUILD b/tools/node_tools/polygerrit_app_preprocessor/BUILD
index 8d39d2b..52f6a3e 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/BUILD
+++ b/tools/node_tools/polygerrit_app_preprocessor/BUILD
@@ -19,6 +19,9 @@
 #rollup_bundle - workaround for https://github.com/bazelbuild/rules_nodejs/issues/1522
 rollup_bundle(
     name = "preprocessor-bundle",
+    args = [
+        "--bundleConfigAsCjs=true",
+    ],
     config_file = "rollup.config.js",
     entry_point = "preprocessor.ts",
     format = "cjs",
@@ -26,12 +29,15 @@
     silent = True,
     deps = [
         ":preprocessor",
-        "@tools_npm//rollup-plugin-node-resolve",
+        "@tools_npm//@rollup/plugin-node-resolve",
     ],
 )
 
 rollup_bundle(
     name = "links-updater-bundle",
+    args = [
+        "--bundleConfigAsCjs=true",
+    ],
     config_file = "rollup.config.js",
     entry_point = "links-updater.ts",
     format = "cjs",
@@ -39,7 +45,7 @@
     silent = True,
     deps = [
         ":preprocessor",
-        "@tools_npm//rollup-plugin-node-resolve",
+        "@tools_npm//@rollup/plugin-node-resolve",
     ],
 )
 
diff --git a/tools/node_tools/polygerrit_app_preprocessor/rollup.config.js b/tools/node_tools/polygerrit_app_preprocessor/rollup.config.js
index 7109bde..bf0f8511 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/rollup.config.js
+++ b/tools/node_tools/polygerrit_app_preprocessor/rollup.config.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-
 export default {
   external: ['fs', 'path', 'parse5', 'dom5', 'parse5-html-rewriting-stream'],
   onwarn: warn => {
diff --git a/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json b/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
index d3c7d1d..56e8b2d 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
+++ b/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021' or 'ESNEXT'. */
+    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/tools/node_tools/utils/tsconfig.json b/tools/node_tools/utils/tsconfig.json
index d6bffa0..60cb677 100644
--- a/tools/node_tools/utils/tsconfig.json
+++ b/tools/node_tools/utils/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021' or 'ESNEXT'. */
+    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 9f3cada..0e24b81 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -2,98 +2,75 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.10.4":
-  version "7.22.13"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
-  integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
-  dependencies:
-    "@babel/highlight" "^7.22.13"
-    chalk "^2.4.2"
-
-"@babel/helper-validator-identifier@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
-  integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-
-"@babel/highlight@^7.22.13":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
-  integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.22.20"
-    chalk "^2.4.2"
-    js-tokens "^4.0.0"
-
-"@bazel/concatjs@^5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/concatjs/-/concatjs-5.5.0.tgz#e6104ed70595cae59463ae6b0b5389252566221e"
-  integrity sha512-hwG+ahivR20Z3iTOlkUz3OdwnW/PUaZyyz8BIX+GNUTg6U3rPHK51CavUirMupOU/LRJ5GyCwBNAAtjCyquo2g==
+"@bazel/concatjs@^5.8.1":
+  version "5.8.1"
+  resolved "https://registry.yarnpkg.com/@bazel/concatjs/-/concatjs-5.8.1.tgz#dd20882429e382cae79c08cbd3238dfc680d2d67"
+  integrity sha512-TkARsNUxgi3bjFeGwIGlffmQglNhuR9qK9uE7uKhdBZvQE5caAWVCjYiMTzo3viKDhwKn5QNRcHY5huuJMVFfA==
   dependencies:
     protobufjs "6.8.8"
     source-map-support "0.5.9"
     tsutils "3.21.0"
 
-"@bazel/rollup@^5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.5.0.tgz#1e152d6147ef5583ec9fd872756c9d0635db73c7"
-  integrity sha512-8SRbgVfaYdNb6PyIypj8jzzJHhlIRyMH3s5KpXODsjD+mXECH4jQxJ8VcRkt0f0exsgB12gK5dmoUK/F2PDKCw==
+"@bazel/rollup@^5.8.1":
+  version "5.8.1"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.8.1.tgz#ee876c35595b456f700d258385412e4f0dd57c15"
+  integrity sha512-Ys+UWbRp1TY2j+z15N+SZgID/nuqAtJTgJDsz0NZVjm8F8KzmgXxLDnBb/cUKFVk83pNOAi84G/bq1tINjMSNA==
   dependencies:
-    "@bazel/worker" "5.5.0"
+    "@bazel/worker" "5.8.1"
 
-"@bazel/typescript@^5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-5.5.0.tgz#053c255acb1b3cac23d24984cd8d5d5542fe1f7c"
-  integrity sha512-Ord0+nCj+B1M4NDbe0uqZf2FyOCzaDAlc4DAsr5UKJrArCipIbMTEAxlsEk+WAYBNAFGO/FS9/zlDtLceqpHqw==
+"@bazel/typescript@^5.8.1":
+  version "5.8.1"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-5.8.1.tgz#74a76af434fad7930893cf3e98b4cc201e52dc65"
+  integrity sha512-NAJ8WQHZL1WE1YmRoCrq/1hhG15Mvy/viWh6TkvFnBeEhNUiQUsA5GYyhU1ztnBIYW03nATO3vwhAEfO7Q0U5g==
   dependencies:
-    "@bazel/worker" "5.5.0"
-    protobufjs "6.8.8"
+    "@bazel/worker" "5.8.1"
     semver "5.6.0"
     source-map-support "0.5.9"
     tsutils "3.21.0"
 
-"@bazel/worker@5.5.0":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.5.0.tgz#d30b75e46f2052d33bcda251b328d36655a5636f"
-  integrity sha512-pYfjJKg4D1CQ/AJ1UGC5ySyH09gDqNiBrQJ0uMYVewIBW24uOAkKsJfTE2y4ES0UL1Ik758WO0la0mJeFOKScg==
+"@bazel/worker@5.8.1":
+  version "5.8.1"
+  resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.8.1.tgz#65af7a70dd2f1aaedd6c19330abd9a198f96e7b2"
+  integrity sha512-GMyZSNW3F34f9GjbJqvs1aHyed5BNrNeiDzNJhC1fIizo/UeBM21oBBONIYLBDoBtq936U85VyPZ76JaP/83hw==
   dependencies:
     google-protobuf "^3.6.1"
 
-"@jridgewell/gen-mapping@^0.3.0":
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
-  integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+"@jridgewell/gen-mapping@^0.3.5":
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
+  integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
   dependencies:
-    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/set-array" "^1.2.1"
     "@jridgewell/sourcemap-codec" "^1.4.10"
-    "@jridgewell/trace-mapping" "^0.3.9"
+    "@jridgewell/trace-mapping" "^0.3.24"
 
 "@jridgewell/resolve-uri@^3.1.0":
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
-  integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+  integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
 
-"@jridgewell/set-array@^1.0.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
-  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+"@jridgewell/set-array@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
+  integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
 
 "@jridgewell/source-map@^0.3.3":
-  version "0.3.5"
-  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91"
-  integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a"
+  integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==
   dependencies:
-    "@jridgewell/gen-mapping" "^0.3.0"
-    "@jridgewell/trace-mapping" "^0.3.9"
+    "@jridgewell/gen-mapping" "^0.3.5"
+    "@jridgewell/trace-mapping" "^0.3.25"
 
-"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
-  version "1.4.15"
-  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
-  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
+  integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
 
-"@jridgewell/trace-mapping@^0.3.9":
-  version "0.3.20"
-  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
-  integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+  version "0.3.25"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
+  integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
   dependencies:
     "@jridgewell/resolve-uri" "^3.1.0"
     "@jridgewell/sourcemap-codec" "^1.4.14"
@@ -151,118 +128,174 @@
   resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
   integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
 
-"@rollup/pluginutils@^4.0.0":
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
-  integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
+"@rollup/plugin-commonjs@^28.0.3":
+  version "28.0.3"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz#44c2cc7c955c6113b96696b55e6bc2446bd67913"
+  integrity sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==
   dependencies:
-    estree-walker "^2.0.1"
-    picomatch "^2.2.2"
+    "@rollup/pluginutils" "^5.0.1"
+    commondir "^1.0.1"
+    estree-walker "^2.0.2"
+    fdir "^6.2.0"
+    is-reference "1.2.1"
+    magic-string "^0.30.3"
+    picomatch "^4.0.2"
 
-"@sindresorhus/is@^4.0.0":
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
-  integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
-
-"@szmarczak/http-timer@^4.0.5":
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
-  integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==
+"@rollup/plugin-node-resolve@^16.0.1":
+  version "16.0.1"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz#2fc6b54ca3d77e12f3fb45b2a55b50720de4c95d"
+  integrity sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==
   dependencies:
-    defer-to-connect "^2.0.0"
+    "@rollup/pluginutils" "^5.0.1"
+    "@types/resolve" "1.20.2"
+    deepmerge "^4.2.2"
+    is-module "^1.0.0"
+    resolve "^1.22.1"
 
-"@types/body-parser@*":
-  version "1.19.2"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
-  integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
-  dependencies:
-    "@types/connect" "*"
-    "@types/node" "*"
-
-"@types/cacheable-request@^6.0.1":
+"@rollup/plugin-replace@^6.0.2":
   version "6.0.2"
-  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
-  integrity sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz#2f565d312d681e4570ff376c55c5c08eb6f1908d"
+  integrity sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==
   dependencies:
-    "@types/http-cache-semantics" "*"
-    "@types/keyv" "*"
-    "@types/node" "*"
-    "@types/responselike" "*"
+    "@rollup/pluginutils" "^5.0.1"
+    magic-string "^0.30.3"
 
-"@types/connect@*":
-  version "3.4.35"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
-  integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
+"@rollup/plugin-terser@^0.4.4":
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz#15dffdb3f73f121aa4fbb37e7ca6be9aeea91962"
+  integrity sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==
   dependencies:
-    "@types/node" "*"
+    serialize-javascript "^6.0.1"
+    smob "^1.0.0"
+    terser "^5.17.4"
 
-"@types/estree@*":
-  version "0.0.51"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
-  integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
-
-"@types/express-serve-static-core@^4.17.18":
-  version "4.17.28"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8"
-  integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==
+"@rollup/pluginutils@^5.0.1":
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a"
+  integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==
   dependencies:
-    "@types/node" "*"
-    "@types/qs" "*"
-    "@types/range-parser" "*"
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^4.0.2"
 
-"@types/express@^4.0.30":
-  version "4.17.13"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
-  integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
-  dependencies:
-    "@types/body-parser" "*"
-    "@types/express-serve-static-core" "^4.17.18"
-    "@types/qs" "*"
-    "@types/serve-static" "*"
+"@rollup/rollup-android-arm-eabi@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz#c228d00a41f0dbd6fb8b7ea819bbfbf1c1157a10"
+  integrity sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==
 
-"@types/freeport@^1.0.19":
-  version "1.0.22"
-  resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.22.tgz#dbe627a20cb30c17c8aaaba09332e1d14cc2281f"
-  integrity sha512-UGg4s5PDPXZXkkrHarU1l6WDbULxN3g7xUEtdbNf9HQhU/JnCj1G1/xZHZmQjC0uWqN1LlB0R0xOlk3k5svgTQ==
+"@rollup/rollup-android-arm64@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz#e2b38d0c912169fd55d7e38d723aada208d37256"
+  integrity sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==
 
-"@types/http-cache-semantics@*":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
-  integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
+"@rollup/rollup-darwin-arm64@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz#1fddb3690f2ae33df16d334c613377f05abe4878"
+  integrity sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==
 
-"@types/json-buffer@~3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/json-buffer/-/json-buffer-3.0.0.tgz#85c1ff0f0948fc159810d4b5be35bf8c20875f64"
-  integrity sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==
+"@rollup/rollup-darwin-x64@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz#818298d11c8109e1112590165142f14be24b396d"
+  integrity sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==
 
-"@types/keyv@*":
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
-  integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==
-  dependencies:
-    "@types/node" "*"
+"@rollup/rollup-freebsd-arm64@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz#91a28dc527d5bed7f9ecf0e054297b3012e19618"
+  integrity sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==
 
-"@types/launchpad@^0.6.0":
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.1.tgz#9a5f285128598f5e0cb4a5db3e458e1b7cf07f35"
-  integrity sha512-kQ1a7PwzJelwwOIw1SABmW5OsbCRPvdjps0J84MahGsEKzN89StrPyrWCMWfwpONR3ZqSxDeblxS+8WznIBEGw==
+"@rollup/rollup-freebsd-x64@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz#28acadefa76b5c7bede1576e065b51d335c62c62"
+  integrity sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz#819691464179cbcd9a9f9d3dc7617954840c6186"
+  integrity sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==
+
+"@rollup/rollup-linux-arm-musleabihf@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz#d149207039e4189e267e8724050388effc80d704"
+  integrity sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==
+
+"@rollup/rollup-linux-arm64-gnu@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz#fa72ebddb729c3c6d88973242f1a2153c83e86ec"
+  integrity sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==
+
+"@rollup/rollup-linux-arm64-musl@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz#2054216e34469ab8765588ebf343d531fc3c9228"
+  integrity sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz#818de242291841afbfc483a84f11e9c7a11959bc"
+  integrity sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz#0bb4cb8fc4a2c635f68c1208c924b2145eb647cb"
+  integrity sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==
+
+"@rollup/rollup-linux-riscv64-gnu@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz#4b3b8e541b7b13e447ae07774217d98c06f6926d"
+  integrity sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==
+
+"@rollup/rollup-linux-riscv64-musl@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz#e065405e67d8bd64a7d0126c931bd9f03910817f"
+  integrity sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==
+
+"@rollup/rollup-linux-s390x-gnu@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz#dda3265bbbfe16a5d0089168fd07f5ebb2a866fe"
+  integrity sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==
+
+"@rollup/rollup-linux-x64-gnu@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz#90993269b8b995b4067b7b9d72ff1c360ef90a17"
+  integrity sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==
+
+"@rollup/rollup-linux-x64-musl@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz#fdf5b09fd121eb8d977ebb0fda142c7c0167b8de"
+  integrity sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==
+
+"@rollup/rollup-win32-arm64-msvc@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz#6397e1e012db64dfecfed0774cb9fcf89503d716"
+  integrity sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==
+
+"@rollup/rollup-win32-ia32-msvc@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz#df0991464a52a35506103fe18d29913bf8798a0c"
+  integrity sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==
+
+"@rollup/rollup-win32-x64-msvc@4.40.2":
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz#8dae04d01a2cbd84d6297d99356674c6b993f0fc"
+  integrity sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==
+
+"@types/estree@*", "@types/estree@1.0.7", "@types/estree@^1.0.0":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
+  integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
 
 "@types/long@^4.0.0":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
   integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
 
-"@types/mime@^1":
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
-  integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
+"@types/node@*", "@types/node@^22.15.2":
+  version "22.15.18"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.18.tgz#2f8240f7e932f571c2d45f555ba0b6c3f7a75963"
+  integrity sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==
+  dependencies:
+    undici-types "~6.21.0"
 
-"@types/node@*":
-  version "17.0.38"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947"
-  integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==
-
-"@types/node@^10.1.0", "@types/node@^10.17.12":
+"@types/node@^10.1.0":
   version "10.17.60"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
   integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
@@ -275,17 +308,11 @@
     "@types/parse5-sax-parser" "*"
 
 "@types/parse5-sax-parser@*":
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/@types/parse5-sax-parser/-/parse5-sax-parser-5.0.2.tgz#4cdca0f8bc0ce71b17e27b96e7ca9b5f79e861ff"
-  integrity sha512-EQtGoduLbdMmS4N27g6wcXdCCJ70dWYemfogWuumYg+JmzRqwYvTRAbGOYFortSHtS/qRzRCFwcP3ixy62RsdA==
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz#6b6fd46ef05c3c58392fc4d915f07969f7ce1a90"
+  integrity sha512-wR3hG33tMsLOdqtcJT0XuGi1Ik3dTP+/ZWN1AckYmEagO/SWoL99GPyv21ZI/FAHOtYWB02TEV3+Y5zqkC4L+Q==
   dependencies:
-    "@types/node" "*"
-    "@types/parse5" "*"
-
-"@types/parse5@*":
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
-  integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
+    parse5-sax-parser "*"
 
 "@types/parse5@^2.2.34":
   version "2.2.34"
@@ -301,251 +328,40 @@
   dependencies:
     "@types/node" "*"
 
-"@types/qs@*":
-  version "6.9.7"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
-  integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
+"@types/resolve@1.20.2":
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
+  integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
 
-"@types/range-parser@*":
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
-  integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
-
-"@types/resolve@0.0.8":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
-  integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/responselike@*", "@types/responselike@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
-  integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/serve-static@*":
-  version "1.13.10"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
-  integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
-  dependencies:
-    "@types/mime" "^1"
-    "@types/node" "*"
-
-"@types/which@^1.3.1":
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"
-  integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==
-
-acorn@^8.8.2:
-  version "8.10.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
-  integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
-
-agent-base@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
-  integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
-  dependencies:
-    es6-promisify "^5.0.0"
-
-ansi-styles@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
-  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
-  dependencies:
-    color-convert "^1.9.0"
-
-ast-matcher@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ast-matcher/-/ast-matcher-1.1.1.tgz#95a6dc72318319507024fff438b7839e4e280813"
-  integrity sha512-wQPAp09kPFRQsOijM2Blfg4lH6B9MIhIUrhFtDdhD/1JFhPmfg2/+WAjViVYl3N7EwleHI+q/enTHjaDrv+wEw==
-
-async@^2.0.1:
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
-  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
-  dependencies:
-    lodash "^4.17.14"
-
-balanced-match@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
-  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
-  integrity sha512-hURVuTTGLOppKhjSe9lZy4NCjnvaIAF/juwazv4WtHwsk5rxKrU1WbxN+XtwKDSvkrNbIIaTBQd9wUsSwruZUg==
-
-base64-js@^1.3.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
-  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-bl@^4.0.3:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
-  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
-  dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
-
-brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
-  dependencies:
-    balanced-match "^1.0.0"
-    concat-map "0.0.1"
-
-browserstack@^1.2.0:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3"
-  integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==
-  dependencies:
-    https-proxy-agent "^2.2.1"
-
-buffer-crc32@~0.2.3:
-  version "0.2.13"
-  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
-  integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
+acorn@^8.14.0:
+  version "8.14.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
+  integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
 
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
-buffer@^5.5.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
-  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.1.13"
-
-builtin-modules@^3.1.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
-  integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
-
-cacheable-lookup@^5.0.3:
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
-  integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
-
-cacheable-request@^7.0.2:
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27"
-  integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==
-  dependencies:
-    clone-response "^1.0.2"
-    get-stream "^5.1.0"
-    http-cache-semantics "^4.0.0"
-    keyv "^4.0.0"
-    lowercase-keys "^2.0.0"
-    normalize-url "^6.0.1"
-    responselike "^2.0.0"
-
-chalk@^2.3.0, chalk@^2.4.2:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
-  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
-  dependencies:
-    ansi-styles "^3.2.1"
-    escape-string-regexp "^1.0.5"
-    supports-color "^5.3.0"
-
-cleankill@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/cleankill/-/cleankill-2.0.0.tgz#59830dfc8b411d53dc72ad09d45a78ea33161a91"
-  integrity sha512-qj/ZY1wjON/36bsk3cF5WtXnrxUgWqc5PCN78LsOpjIk0Dka0lPqbhu9FVk4Yy4N3VuDA8VhlcgBLWC5L+tGHg==
-
-clone-response@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
-  integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==
-  dependencies:
-    mimic-response "^1.0.0"
-
 clone@^2.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
   integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
 
-color-convert@^1.9.0:
-  version "1.9.3"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
-  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
-  dependencies:
-    color-name "1.1.3"
-
-color-name@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-
-commander@^2.20.0, commander@^2.20.3:
+commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-compress-brotli@^1.3.8:
-  version "1.3.8"
-  resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.8.tgz#0c0a60c97a989145314ec381e84e26682e7b38db"
-  integrity sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==
-  dependencies:
-    "@types/json-buffer" "~3.0.0"
-    json-buffer "~3.0.1"
+commondir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+  integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
 
-concat-map@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-
-cross-spawn@^7.0.3:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
-  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
-  dependencies:
-    path-key "^3.1.0"
-    shebang-command "^2.0.0"
-    which "^2.0.1"
-
-debug@^2.2.0:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
-  dependencies:
-    ms "2.0.0"
-
-debug@^3.1.0:
-  version "3.2.7"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
-  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
-  dependencies:
-    ms "^2.1.1"
-
-debug@^4.3.1:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
-  dependencies:
-    ms "2.1.2"
-
-decompress-response@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
-  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
-  dependencies:
-    mimic-response "^3.1.0"
-
-defer-to-connect@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
-  integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==
+deepmerge@^4.2.2:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
 
 dom5@^3.0.1:
   version "3.0.1"
@@ -556,340 +372,73 @@
     clone "^2.1.0"
     parse5 "^4.0.0"
 
-end-of-stream@^1.1.0, end-of-stream@^1.4.1:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
-  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
-  dependencies:
-    once "^1.4.0"
+entities@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.0.tgz#09c9e29cb79b0a6459a9b9db9efb418ac5bb8e51"
+  integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==
 
-es6-promise@^4.0.3:
-  version "4.2.8"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
-  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
-
-es6-promisify@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
-  integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==
-  dependencies:
-    es6-promise "^4.0.3"
-
-escape-string-regexp@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
-  integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
-
-escape-string-regexp@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
-  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
-estree-walker@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
-  integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
-
-estree-walker@^2.0.1:
+estree-walker@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
   integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 
-fd-slicer@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
-  integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
-  dependencies:
-    pend "~1.2.0"
-
-freeport@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/freeport/-/freeport-1.0.5.tgz#255e8ab84170c33ba85d990e821ae5f4a1a9bc5d"
-  integrity sha512-1+iRfba5tXzQAF83Tvvw5ZuhqDzyACfM+v13SZkdq8xKdaj/WR0Bke4sw9HsO1nU143+Hn0JxIleHEct+xbz9A==
-
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
-fs.realpath@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+fdir@^6.2.0:
+  version "6.4.4"
+  resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9"
+  integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==
 
 fsevents@~2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
-  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
 
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-stream@^5.1.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
-  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
-  dependencies:
-    pump "^3.0.0"
-
-glob@^7.1.3:
-  version "7.2.3"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
-  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.1.1"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
 
 google-protobuf@^3.6.1:
-  version "3.20.1"
-  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.20.1.tgz#1b255c2b59bcda7c399df46c65206aa3c7a0ce8b"
-  integrity sha512-XMf1+O32FjYIV3CYu6Tuh5PNbfNEU5Xu22X+Xkdb/DUexFlCzhvv7d5Iirm4AOwn8lv4al1YvIhzGrg2j9Zfzw==
+  version "3.21.4"
+  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.4.tgz#2f933e8b6e5e9f8edde66b7be0024b68f77da6c9"
+  integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==
 
-got@^11.8.2:
-  version "11.8.5"
-  resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046"
-  integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==
+hasown@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
   dependencies:
-    "@sindresorhus/is" "^4.0.0"
-    "@szmarczak/http-timer" "^4.0.5"
-    "@types/cacheable-request" "^6.0.1"
-    "@types/responselike" "^1.0.0"
-    cacheable-lookup "^5.0.3"
-    cacheable-request "^7.0.2"
-    decompress-response "^6.0.0"
-    http2-wrapper "^1.0.0-beta.5.2"
-    lowercase-keys "^2.0.0"
-    p-cancelable "^2.0.0"
-    responselike "^2.0.0"
+    function-bind "^1.1.2"
 
-has-flag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
-  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
-
-has-flag@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
-  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
-  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+is-core-module@^2.16.0:
+  version "2.16.1"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
+  integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
   dependencies:
-    function-bind "^1.1.1"
-
-http-cache-semantics@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
-  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
-
-http2-wrapper@^1.0.0-beta.5.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
-  integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==
-  dependencies:
-    quick-lru "^5.1.1"
-    resolve-alpn "^1.0.0"
-
-https-proxy-agent@^2.2.1:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
-  integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
-  dependencies:
-    agent-base "^4.3.0"
-    debug "^3.1.0"
-
-ieee754@^1.1.13:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
-  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-inflight@^1.0.4:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
-  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
-  dependencies:
-    once "^1.3.0"
-    wrappy "1"
-
-inherits@2, inherits@^2.0.3, inherits@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
-  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-is-core-module@^2.8.1:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
-  integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
-  dependencies:
-    has "^1.0.3"
+    hasown "^2.0.2"
 
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
   integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
 
-is-reference@^1.1.2:
+is-reference@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
   integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
   dependencies:
     "@types/estree" "*"
 
-isexe@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
-  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-
-jest-worker@^26.2.1:
-  version "26.6.2"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
-  integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
-  dependencies:
-    "@types/node" "*"
-    merge-stream "^2.0.0"
-    supports-color "^7.0.0"
-
-js-tokens@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
-  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-json-buffer@3.0.1, json-buffer@~3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
-  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
-
-keyv@^4.0.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.3.0.tgz#b4352e0e4fe7c94111947d6738a6d3fe7903027c"
-  integrity sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==
-  dependencies:
-    compress-brotli "^1.3.8"
-    json-buffer "3.0.1"
-
-"launchpad@git+https://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be", "launchpad@git://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be":
-  version "0.7.5"
-  resolved "git+https://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be"
-  dependencies:
-    async "^2.0.1"
-    browserstack "^1.2.0"
-    debug "^2.2.0"
-    mkdirp "^0.5.1"
-    plist "^2.0.1"
-    q "^1.4.1"
-    rimraf "^3.0.0"
-    underscore "^1.8.3"
-
-lodash.mapvalues@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
-  integrity sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==
-
-lodash.merge@^4.6.2:
-  version "4.6.2"
-  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
-  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
-
-lodash@4.17.21, lodash@^4.17.14:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
 long@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
   integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
 
-lowercase-keys@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
-  integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
-
-magic-string@^0.25.2, magic-string@^0.25.7:
-  version "0.25.9"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
-  integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
+magic-string@^0.30.3:
+  version "0.30.17"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
+  integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
   dependencies:
-    sourcemap-codec "^1.4.8"
-
-merge-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
-  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
-
-mimic-response@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
-  integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
-
-mimic-response@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
-  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
-
-minimatch@^3.1.1:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
-  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimist@^1.2.5, minimist@^1.2.6:
-  version "1.2.6"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
-  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
-
-mkdirp@^0.5.1:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
-  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
-  dependencies:
-    minimist "^1.2.6"
-
-mkdirp@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
-  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
-ms@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-  integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
-
-ms@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
-  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-ms@^2.1.1:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
-  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-normalize-url@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
-  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
-
-once@^1.3.0, once@^1.3.1, once@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
-  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
-  dependencies:
-    wrappy "1"
-
-p-cancelable@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
-  integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
+    "@jridgewell/sourcemap-codec" "^1.5.0"
 
 parse5-html-rewriting-stream@^5.1.1:
   version "5.1.1"
@@ -899,6 +448,13 @@
     parse5 "^5.1.1"
     parse5-sax-parser "^5.1.1"
 
+parse5-sax-parser@*:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz#4c05064254f0488676aca75fb39ca069ec96dee5"
+  integrity sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==
+  dependencies:
+    parse5 "^7.0.0"
+
 parse5-sax-parser@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-5.1.1.tgz#02834a9d08b23ea2d99584841c38be09d5247a15"
@@ -916,44 +472,22 @@
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
   integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
-path-is-absolute@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
-
-path-key@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
-  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+parse5@^7.0.0:
+  version "7.3.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
+  integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
+  dependencies:
+    entities "^6.0.0"
 
 path-parse@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
-pend@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
-  integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
-
-picomatch@^2.2.2:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
-  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-
-plist@^2.0.1:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
-  integrity sha512-yirJ+8SSb8o7pkfyNv+fTzUP0GbK52HMvh0MjMycCxvpL8rHiAfKhXU/3R5znSJnrGakV0WNZhr8yTR4//PjyA==
-  dependencies:
-    base64-js "1.2.0"
-    xmlbuilder "8.2.2"
-    xmldom "0.1.x"
-
-progress@2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
-  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+picomatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
+  integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
 
 protobufjs@6.8.8:
   version "6.8.8"
@@ -974,24 +508,6 @@
     "@types/node" "^10.1.0"
     long "^4.0.0"
 
-pump@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
-  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
-  dependencies:
-    end-of-stream "^1.1.0"
-    once "^1.3.1"
-
-q@^1.4.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
-  integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
-
-quick-lru@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
-  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
-
 randombytes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -999,145 +515,65 @@
   dependencies:
     safe-buffer "^5.1.0"
 
-readable-stream@^3.1.1, readable-stream@^3.4.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
-  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+resolve@^1.22.1:
+  version "1.22.10"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
+  integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
   dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
-
-resolve-alpn@^1.0.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
-  integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==
-
-resolve@^1.11.0, resolve@^1.11.1:
-  version "1.22.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
-  integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
-  dependencies:
-    is-core-module "^2.8.1"
+    is-core-module "^2.16.0"
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
-responselike@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
-  integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
+rollup@^4.40.0:
+  version "4.40.2"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.2.tgz#778e88b7a197542682b3e318581f7697f55f0619"
+  integrity sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==
   dependencies:
-    lowercase-keys "^2.0.0"
-
-rimraf@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
-  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
-  dependencies:
-    glob "^7.1.3"
-
-rollup-plugin-commonjs@^10.1.0:
-  version "10.1.0"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz#417af3b54503878e084d127adf4d1caf8beb86fb"
-  integrity sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==
-  dependencies:
-    estree-walker "^0.6.1"
-    is-reference "^1.1.2"
-    magic-string "^0.25.2"
-    resolve "^1.11.0"
-    rollup-pluginutils "^2.8.1"
-
-rollup-plugin-define@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-define/-/rollup-plugin-define-1.0.1.tgz#45b027cec9d2e30df71118efa156170e3846fd3d"
-  integrity sha512-SM/CKFpLvWq5xBEf84ff/ooT3KodXPVITCkRliyNccuq8SZMpzthN/Bp7JkWScbGTX5lo1SF3cjxKKDjnnFCuA==
-  dependencies:
-    "@rollup/pluginutils" "^4.0.0"
-    ast-matcher "^1.1.1"
-    escape-string-regexp "^4.0.0"
-    magic-string "^0.25.7"
-
-rollup-plugin-node-resolve@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz#730f93d10ed202473b1fb54a5997a7db8c6d8523"
-  integrity sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==
-  dependencies:
-    "@types/resolve" "0.0.8"
-    builtin-modules "^3.1.0"
-    is-module "^1.0.0"
-    resolve "^1.11.1"
-    rollup-pluginutils "^2.8.1"
-
-rollup-plugin-terser@^7.0.2:
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
-  integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
-  dependencies:
-    "@babel/code-frame" "^7.10.4"
-    jest-worker "^26.2.1"
-    serialize-javascript "^4.0.0"
-    terser "^5.0.0"
-
-rollup-pluginutils@^2.8.1:
-  version "2.8.2"
-  resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
-  integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
-  dependencies:
-    estree-walker "^0.6.1"
-
-rollup@^2.79.1:
-  version "2.79.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
-  integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
+    "@types/estree" "1.0.7"
   optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.40.2"
+    "@rollup/rollup-android-arm64" "4.40.2"
+    "@rollup/rollup-darwin-arm64" "4.40.2"
+    "@rollup/rollup-darwin-x64" "4.40.2"
+    "@rollup/rollup-freebsd-arm64" "4.40.2"
+    "@rollup/rollup-freebsd-x64" "4.40.2"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.40.2"
+    "@rollup/rollup-linux-arm-musleabihf" "4.40.2"
+    "@rollup/rollup-linux-arm64-gnu" "4.40.2"
+    "@rollup/rollup-linux-arm64-musl" "4.40.2"
+    "@rollup/rollup-linux-loongarch64-gnu" "4.40.2"
+    "@rollup/rollup-linux-powerpc64le-gnu" "4.40.2"
+    "@rollup/rollup-linux-riscv64-gnu" "4.40.2"
+    "@rollup/rollup-linux-riscv64-musl" "4.40.2"
+    "@rollup/rollup-linux-s390x-gnu" "4.40.2"
+    "@rollup/rollup-linux-x64-gnu" "4.40.2"
+    "@rollup/rollup-linux-x64-musl" "4.40.2"
+    "@rollup/rollup-win32-arm64-msvc" "4.40.2"
+    "@rollup/rollup-win32-ia32-msvc" "4.40.2"
+    "@rollup/rollup-win32-x64-msvc" "4.40.2"
     fsevents "~2.3.2"
 
-safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+safe-buffer@^5.1.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-selenium-standalone@^6.7.0:
-  version "6.24.0"
-  resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.24.0.tgz#cca7c1c36bfa3429078a8e6a1a4fd373f641a7c8"
-  integrity sha512-Dun2XgNAgCfJNrrSzuv7Z7Wj7QTvBKpqx0VXFz7bW9T9FUe5ytzgzoCEEshwDVMh0Dv6sCgdZg7VDhM/q2yPPQ==
-  dependencies:
-    commander "^2.20.3"
-    cross-spawn "^7.0.3"
-    debug "^4.3.1"
-    got "^11.8.2"
-    lodash.mapvalues "^4.6.0"
-    lodash.merge "^4.6.2"
-    minimist "^1.2.5"
-    mkdirp "^1.0.4"
-    progress "2.0.3"
-    tar-stream "2.2.0"
-    which "^2.0.2"
-    yauzl "^2.10.0"
-
 semver@5.6.0:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
   integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
 
-serialize-javascript@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
-  integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
+serialize-javascript@^6.0.1:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
+  integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
   dependencies:
     randombytes "^2.1.0"
 
-shebang-command@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
-  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
-  dependencies:
-    shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
-  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+smob@^1.0.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab"
+  integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==
 
 source-map-support@0.5.9:
   version "0.5.9"
@@ -1160,72 +596,21 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
-source-map@~0.7.2:
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
-  integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
-
-sourcemap-codec@^1.4.8:
-  version "1.4.8"
-  resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
-  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-
-string_decoder@^1.1.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
-  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
-  dependencies:
-    safe-buffer "~5.2.0"
-
-supports-color@^5.3.0:
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
-  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
-  dependencies:
-    has-flag "^3.0.0"
-
-supports-color@^7.0.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
-  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
-  dependencies:
-    has-flag "^4.0.0"
-
 supports-preserve-symlinks-flag@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
-tar-stream@2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
-  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
-  dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-terser@^5.0.0:
-  version "5.22.0"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.22.0.tgz#4f18103f84c5c9437aafb7a14918273310a8a49d"
-  integrity sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==
+terser@^5.17.4, terser@~5.39.0:
+  version "5.39.2"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.2.tgz#5a1626030724a672e2e5b5c9cd9070308c20e8f9"
+  integrity sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==
   dependencies:
     "@jridgewell/source-map" "^0.3.3"
-    acorn "^8.8.2"
+    acorn "^8.14.0"
     commander "^2.20.0"
     source-map-support "~0.5.20"
 
-terser@~5.8.0:
-  version "5.8.0"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.8.0.tgz#c6d352f91aed85cc6171ccb5e84655b77521d947"
-  integrity sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==
-  dependencies:
-    commander "^2.20.0"
-    source-map "~0.7.2"
-    source-map-support "~0.5.20"
-
 tslib@^1.8.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -1238,70 +623,12 @@
   dependencies:
     tslib "^1.8.1"
 
-typescript@^4.9.5:
-  version "4.9.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
-  integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
+typescript@^5.8.3:
+  version "5.8.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
+  integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
 
-underscore@^1.8.3:
-  version "1.13.4"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee"
-  integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==
-
-util-deprecate@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
-
-wct-local@2.1.6:
-  version "2.1.6"
-  resolved "https://registry.yarnpkg.com/wct-local/-/wct-local-2.1.6.tgz#2d099c52996e77265d16e03a5d6d897b77ea9967"
-  integrity sha512-jvTzgOIIfJ43H3DXUfruHPTQ/TJ269SDk4R2CfCpU13EYbwxn3U1B6L5NHYRFu/cgdJmOHraGrn/wREHH6xeXQ==
-  dependencies:
-    "@types/express" "^4.0.30"
-    "@types/freeport" "^1.0.19"
-    "@types/launchpad" "^0.6.0"
-    "@types/which" "^1.3.1"
-    chalk "^2.3.0"
-    cleankill "^2.0.0"
-    freeport "^1.0.4"
-    launchpad "git://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be"
-    selenium-standalone "^6.7.0"
-    which "^1.0.8"
-
-which@^1.0.8:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
-  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
-  dependencies:
-    isexe "^2.0.0"
-
-which@^2.0.1, which@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
-  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
-  dependencies:
-    isexe "^2.0.0"
-
-wrappy@1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
-  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-xmlbuilder@8.2.2:
-  version "8.2.2"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
-  integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=
-
-xmldom@0.1.x:
-  version "0.1.31"
-  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
-  integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
-
-yauzl@^2.10.0:
-  version "2.10.0"
-  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
-  integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
-  dependencies:
-    buffer-crc32 "~0.2.3"
-    fd-slicer "~1.1.0"
+undici-types@~6.21.0:
+  version "6.21.0"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
+  integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 717b58d..f8e2591 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -13,11 +13,7 @@
 
 AUTO_VALUE_VERSION = "1.11.0"
 
-GUAVA_VERSION = "33.4.0-jre"
-
-GUAVA_BIN_SHA1 = "03fcc0a259f724c7de54a6a55ea7e26d3d5c0cac"
-
-GUAVA_TESTLIB_BIN_SHA1 = "e849ea71846b5ca96387d543c7ac862f18fe2513"
+GUAVA_VERSION = "33.4.8-jre"
 
 GUAVA_DOC_URL = "https://guava.dev/releases/" + GUAVA_VERSION + "/api/docs/"
 
@@ -137,30 +133,30 @@
         sha1 = "4c0093ffbe0d6a90253e47277ce6dc4f759aff7b",
     )
 
-    SSHD_VERS = "2.15.0"
+    SSHD_VERS = "2.16.0"
 
     maven_jar(
         name = "sshd-osgi",
         artifact = "org.apache.sshd:sshd-osgi:" + SSHD_VERS,
-        sha1 = "aa76898fe47eab7da0878dd60e6f3be5631e076c",
+        sha1 = "87cab2aaa6e06c5d48d746e90f0b3635f8c06419",
     )
 
     maven_jar(
         name = "sshd-sftp",
         artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
-        sha1 = "2e226055ed060c64ed76256a9c45de6d0109eef8",
+        sha1 = "09d9e7024535fb4a3f74367ba7e0a2f5093af638",
     )
 
     maven_jar(
         name = "mina-core",
-        artifact = "org.apache.mina:mina-core:2.0.27",
-        sha1 = "d5d353d971b0fb17ae0271f6f2921585f64e1535",
+        artifact = "org.apache.mina:mina-core:2.2.4",
+        sha1 = "f76b231c8a332640a4b1deef5262c603b088be02",
     )
 
     maven_jar(
         name = "sshd-mina",
         artifact = "org.apache.sshd:sshd-mina:" + SSHD_VERS,
-        sha1 = "f0495bc8ad7b6aea017007528d76ed630d011575",
+        sha1 = "9247372c4b7fc88d69d4e1bd7de281b3b74f1b3f",
     )
 
     maven_jar(
@@ -262,13 +258,13 @@
     maven_jar(
         name = "guava",
         artifact = "com.google.guava:guava:" + GUAVA_VERSION,
-        sha1 = GUAVA_BIN_SHA1,
+        sha1 = "e70a3268e6cd3e7d458aa15787ce6811c34e96ae",
     )
 
     maven_jar(
         name = "guava-testlib",
         artifact = "com.google.guava:guava-testlib:" + GUAVA_VERSION,
-        sha1 = GUAVA_TESTLIB_BIN_SHA1,
+        sha1 = "7443cfac765d74b8a31bbe6c49357715e32f714c",
     )
 
     GUICE_VERS = "6.0.0"
@@ -355,42 +351,42 @@
         sha1 = "4b88990178086ffdd482246b35a5a48b4d26896c",
     )
 
-    LUCENE_VERS = "10.1.0"
+    LUCENE_VERS = "10.2.2"
 
     maven_jar(
         name = "lucene-core",
         artifact = "org.apache.lucene:lucene-core:" + LUCENE_VERS,
-        sha1 = "65d7670de89a72433ef374b332da679a484d3a1e",
+        sha1 = "336a9c4b24e5704bd5fd71af794cce80f479a3ae",
     )
 
     maven_jar(
         name = "lucene-analyzers-common",
         artifact = "org.apache.lucene:lucene-analysis-common:" + LUCENE_VERS,
-        sha1 = "ddbc824a311d49a54f5808d5a01d5c52424c48b8",
+        sha1 = "2c35eb96330d96b6ffb61856ce2cd886a5656c81",
     )
 
     maven_jar(
         name = "lucene-backward-codecs",
         artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS,
-        sha1 = "fbbebd58f1505cc70d73dbdbb8196bfc29b9cf08",
+        sha1 = "848ccaaadbcc97c84c09ad808fe4354af00449d9",
     )
 
     maven_jar(
         name = "lucene-misc",
         artifact = "org.apache.lucene:lucene-misc:" + LUCENE_VERS,
-        sha1 = "ae1104521d00501e18e3c18c2b326f15589cc873",
+        sha1 = "047de3cefc3aa78ba11593d72c60f5b17a611c73",
     )
 
     maven_jar(
         name = "lucene-queryparser",
         artifact = "org.apache.lucene:lucene-queryparser:" + LUCENE_VERS,
-        sha1 = "2774ab95128b0615568a4861b9a56f24511f774a",
+        sha1 = "bb94dc5a00f01ccc7dc6804388bc7fe9f0070c75",
     )
 
     maven_jar(
         name = "h2",
-        artifact = "com.h2database:h2:2.3.232",
-        sha1 = "4fcc05d966ccdb2812ae8b9a718f69226c0cf4e2",
+        artifact = "com.h2database:h2:2.4.240",
+        sha1 = "686180ad33981ad943fdc0ab381e619b2c2fdfe5",
     )
 
     # JGit's transitive dependencies
diff --git a/version.bzl b/version.bzl
index a487c22..66ceefa 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.12.9-SNAPSHOT"
+GERRIT_VERSION = "3.13.8-SNAPSHOT"
diff --git a/web-dev-server.config.mjs b/web-dev-server.config.mjs
index 53a240e..8820ecd 100644
--- a/web-dev-server.config.mjs
+++ b/web-dev-server.config.mjs
@@ -25,9 +25,14 @@
     // polygerrit-ui/app/rules.bzl
     async (context, next) => {
 
-      if ( context.url.includes("/bower_components/webcomponentsjs/webcomponents-lite.js") ) {
-        context.response.redirect("/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js");
-
+      if ( context.url.includes("/bower_components/webcomponentsjs/webcomponents-loader.js") ) {
+        context.response.redirect("/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js");
+      } else if (context.url.includes("/bower_components/webcomponentsjs/bundles/")) {
+        const bundlePath = context.url.replace(
+          "/bower_components/webcomponentsjs/bundles/",
+          "/node_modules/@webcomponents/webcomponentsjs/bundles/"
+        );
+        context.response.redirect(bundlePath);
       } else if ( context.url.startsWith( "/fonts/" ) ) {
         const fontFile = path.join( "lib/fonts", path.basename(context.url) );
         context.body = fs.createReadStream( fontFile );
diff --git a/yarn.lock b/yarn.lock
index 8a5d986..e3c2364 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,42 +2,24 @@
 # yarn lockfile v1
 
 
-"@75lb/deep-merge@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@75lb/deep-merge/-/deep-merge-1.1.1.tgz#3b06155b90d34f5f8cc2107d796f1853ba02fd6d"
-  integrity sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==
-  dependencies:
-    lodash.assignwith "^4.2.0"
-    typical "^7.1.1"
-
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.11":
-  version "7.23.5"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
-  integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
+  version "7.27.1"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
+  integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
   dependencies:
-    "@babel/highlight" "^7.23.4"
-    chalk "^2.4.2"
-
-"@babel/helper-validator-identifier@^7.22.20":
-  version "7.22.20"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
-  integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-
-"@babel/highlight@^7.23.4":
-  version "7.23.4"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
-  integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.22.20"
-    chalk "^2.4.2"
+    "@babel/helper-validator-identifier" "^7.27.1"
     js-tokens "^4.0.0"
+    picocolors "^1.1.1"
+
+"@babel/helper-validator-identifier@^7.27.1":
+  version "7.27.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
+  integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
 
 "@babel/runtime@^7.10.2":
-  version "7.23.7"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.7.tgz#dd7c88deeb218a0f8bd34d5db1aa242e0f203193"
-  integrity sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==
-  dependencies:
-    regenerator-runtime "^0.14.0"
+  version "7.28.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.3.tgz#75c5034b55ba868121668be5d5bb31cc64e6e61a"
+  integrity sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==
 
 "@bazel/concatjs@^5.8.1":
   version "5.8.1"
@@ -77,180 +59,274 @@
   dependencies:
     google-protobuf "^3.6.1"
 
-"@es-joy/jsdoccomment@~0.39.4":
-  version "0.39.4"
-  resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz#6b8a62e9b3077027837728818d3c4389a898b392"
-  integrity sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==
+"@es-joy/jsdoccomment@~0.50.2":
+  version "0.50.2"
+  resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz#707768f0cb62abe0703d51aa9086986d230a5d5c"
+  integrity sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==
   dependencies:
-    comment-parser "1.3.1"
-    esquery "^1.5.0"
-    jsdoc-type-pratt-parser "~4.0.0"
+    "@types/estree" "^1.0.6"
+    "@typescript-eslint/types" "^8.11.0"
+    comment-parser "1.4.1"
+    esquery "^1.6.0"
+    jsdoc-type-pratt-parser "~4.1.0"
 
-"@esbuild/android-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
-  integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
+"@esbuild/aix-ppc64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
+  integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
 
-"@esbuild/android-arm@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
-  integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
+"@esbuild/android-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
+  integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
 
-"@esbuild/android-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
-  integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
+"@esbuild/android-arm@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
+  integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
 
-"@esbuild/darwin-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
-  integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
+"@esbuild/android-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
+  integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
 
-"@esbuild/darwin-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
-  integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
+"@esbuild/darwin-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae"
+  integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
 
-"@esbuild/freebsd-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
-  integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
+"@esbuild/darwin-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
+  integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
 
-"@esbuild/freebsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
-  integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
+"@esbuild/freebsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
+  integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
 
-"@esbuild/linux-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
-  integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
+"@esbuild/freebsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
+  integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
 
-"@esbuild/linux-arm@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
-  integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
+"@esbuild/linux-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
+  integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
 
-"@esbuild/linux-ia32@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
-  integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
+"@esbuild/linux-arm@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
+  integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
 
-"@esbuild/linux-loong64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
-  integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
+"@esbuild/linux-ia32@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
+  integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
 
-"@esbuild/linux-mips64el@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
-  integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
+"@esbuild/linux-loong64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
+  integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
 
-"@esbuild/linux-ppc64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
-  integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
+"@esbuild/linux-mips64el@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
+  integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
 
-"@esbuild/linux-riscv64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
-  integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
+"@esbuild/linux-ppc64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
+  integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
 
-"@esbuild/linux-s390x@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
-  integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
+"@esbuild/linux-riscv64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
+  integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
 
-"@esbuild/linux-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
-  integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
+"@esbuild/linux-s390x@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
+  integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
 
-"@esbuild/netbsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
-  integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
+"@esbuild/linux-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f"
+  integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
 
-"@esbuild/openbsd-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
-  integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
+"@esbuild/netbsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
+  integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
 
-"@esbuild/sunos-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
-  integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
+"@esbuild/netbsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
+  integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
 
-"@esbuild/win32-arm64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
-  integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
+"@esbuild/openbsd-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
+  integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
 
-"@esbuild/win32-ia32@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
-  integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
+"@esbuild/openbsd-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
+  integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
 
-"@esbuild/win32-x64@0.17.19":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
-  integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
+"@esbuild/openharmony-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
+  integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
 
-"@eslint-community/eslint-utils@^4.2.0":
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56"
-  integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==
+"@esbuild/sunos-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
+  integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
+
+"@esbuild/win32-arm64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
+  integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
+
+"@esbuild/win32-ia32@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
+  integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
+
+"@esbuild/win32-x64@0.25.9":
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
+  integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
+
+"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.5.0", "@eslint-community/eslint-utils@^4.7.0":
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
+  integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
   dependencies:
     eslint-visitor-keys "^3.4.3"
 
-"@eslint-community/regexpp@^4.4.0":
-  version "4.10.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
-  integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
-
-"@eslint-community/regexpp@^4.6.1":
+"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0", "@eslint-community/regexpp@^4.12.1":
   version "4.12.1"
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
   integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
 
-"@eslint/eslintrc@^2.1.4":
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
-  integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
+"@eslint/config-array@^0.21.0":
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636"
+  integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==
+  dependencies:
+    "@eslint/object-schema" "^2.1.6"
+    debug "^4.3.1"
+    minimatch "^3.1.2"
+
+"@eslint/config-helpers@^0.3.1":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.1.tgz#d316e47905bd0a1a931fa50e669b9af4104d1617"
+  integrity sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==
+
+"@eslint/core@^0.15.2":
+  version "0.15.2"
+  resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.2.tgz#59386327d7862cc3603ebc7c78159d2dcc4a868f"
+  integrity sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==
+  dependencies:
+    "@types/json-schema" "^7.0.15"
+
+"@eslint/eslintrc@^3.3.1":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
+  integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.6.0"
-    globals "^13.19.0"
+    espree "^10.0.1"
+    globals "^14.0.0"
     ignore "^5.2.0"
     import-fresh "^3.2.1"
     js-yaml "^4.1.0"
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@8.57.1":
-  version "8.57.1"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
-  integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
+"@eslint/js@9.34.0":
+  version "9.34.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.34.0.tgz#fc423168b9d10e08dea9088d083788ec6442996b"
+  integrity sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==
 
-"@humanwhocodes/config-array@^0.13.0":
-  version "0.13.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
-  integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
+"@eslint/object-schema@^2.1.6":
+  version "2.1.6"
+  resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
+  integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
+
+"@eslint/plugin-kit@^0.3.5":
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz#fd8764f0ee79c8ddab4da65460c641cefee017c5"
+  integrity sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==
   dependencies:
-    "@humanwhocodes/object-schema" "^2.0.3"
-    debug "^4.3.1"
-    minimatch "^3.0.5"
+    "@eslint/core" "^0.15.2"
+    levn "^0.4.1"
+
+"@humanfs/core@^0.19.1":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
+  integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
+
+"@humanfs/node@^0.16.6":
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e"
+  integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==
+  dependencies:
+    "@humanfs/core" "^0.19.1"
+    "@humanwhocodes/retry" "^0.3.0"
 
 "@humanwhocodes/module-importer@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
   integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
 
-"@humanwhocodes/object-schema@^2.0.3":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
-  integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
+"@humanwhocodes/retry@^0.3.0":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
+  integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
+
+"@humanwhocodes/retry@^0.4.2":
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
+  integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
+
+"@jridgewell/gen-mapping@^0.3.5":
+  version "0.3.13"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
+  integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.5.0"
+    "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+  integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/source-map@^0.3.3":
+  version "0.3.11"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba"
+  integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.5"
+    "@jridgewell/trace-mapping" "^0.3.25"
+
+"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+  integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+  version "0.3.30"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99"
+  integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.1.0"
+    "@jridgewell/sourcemap-codec" "^1.4.14"
 
 "@koa/cors@^5.0.0":
   version "5.0.0"
@@ -264,14 +340,6 @@
   resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8"
   integrity sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==
 
-"@mrmlnc/readdir-enhanced@^2.2.1":
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
-  integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
-  dependencies:
-    call-me-maybe "^1.0.1"
-    glob-to-regexp "^0.3.0"
-
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -285,12 +353,7 @@
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
 
-"@nodelib/fs.stat@^1.1.2":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
-  integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
-
-"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
+"@nodelib/fs.walk@^1.2.3":
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
   integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -298,6 +361,16 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
+"@pkgr/core@^0.1.0":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.2.tgz#1cf95080bb7072fafaa3cb13b442fab4695c3893"
+  integrity sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==
+
+"@pkgr/core@^0.2.9":
+  version "0.2.9"
+  resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b"
+  integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
+
 "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -351,26 +424,125 @@
   resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
   integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
 
-"@rollup/plugin-node-resolve@^13.0.4":
-  version "13.3.0"
-  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c"
-  integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==
+"@rollup/plugin-node-resolve@^15.0.1":
+  version "15.3.1"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78"
+  integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==
   dependencies:
-    "@rollup/pluginutils" "^3.1.0"
-    "@types/resolve" "1.17.1"
+    "@rollup/pluginutils" "^5.0.1"
+    "@types/resolve" "1.20.2"
     deepmerge "^4.2.2"
-    is-builtin-module "^3.1.0"
     is-module "^1.0.0"
-    resolve "^1.19.0"
+    resolve "^1.22.1"
 
-"@rollup/pluginutils@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
-  integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
+"@rollup/pluginutils@^5.0.1":
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz#eac25ca5b0bdda4ba735ddaca5fbf26bd435f602"
+  integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==
   dependencies:
-    "@types/estree" "0.0.39"
-    estree-walker "^1.0.1"
-    picomatch "^2.2.2"
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^4.0.2"
+
+"@rollup/rollup-android-arm-eabi@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz#ba432433f5e7b419dba2be407d1d59fea6b8de48"
+  integrity sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==
+
+"@rollup/rollup-android-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz#4e05c86e0fb9af6eaf52fc298dcdec577477e35c"
+  integrity sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==
+
+"@rollup/rollup-darwin-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz#788fad425b4129875639e0c14b6441c5f3b69d46"
+  integrity sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==
+
+"@rollup/rollup-darwin-x64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz#d44e05bee55b781d7c2cf535d9f9169787c3599d"
+  integrity sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==
+
+"@rollup/rollup-freebsd-arm64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz#107786b4d604495224c3543bfd2cae33ddf76500"
+  integrity sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==
+
+"@rollup/rollup-freebsd-x64@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz#54e105c3da27f31084ca6913fed603627755abde"
+  integrity sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz#725c23e0766b5d9368180bc2c427a51e31d0e147"
+  integrity sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==
+
+"@rollup/rollup-linux-arm-musleabihf@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz#6946b0d2f132f2baf5657945b81565d8abd51cc0"
+  integrity sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==
+
+"@rollup/rollup-linux-arm64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz#83510a6d03e748619241a17f5a879418a963c5ed"
+  integrity sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==
+
+"@rollup/rollup-linux-arm64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz#085b98d44c10908626dd40f26bf924433bbd8471"
+  integrity sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz#13e0a4808e9f7924f2cc8c133603f627c7a00543"
+  integrity sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==
+
+"@rollup/rollup-linux-ppc64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz#aeee4e47fc9ca5d6687e686fea4696202af6b2f4"
+  integrity sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==
+
+"@rollup/rollup-linux-riscv64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz#603e4591643f1d7851a96d096cf7fcd273f7b0e1"
+  integrity sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==
+
+"@rollup/rollup-linux-riscv64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz#f8fd9b01f1888e1816d5a398789d430511286c00"
+  integrity sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==
+
+"@rollup/rollup-linux-s390x-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz#37a1fd372d9b93d2b75b2f37c482ecf52f52849b"
+  integrity sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==
+
+"@rollup/rollup-linux-x64-gnu@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz#131e66dbf7e71cb2a389acc45319bd4c990e093a"
+  integrity sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==
+
+"@rollup/rollup-linux-x64-musl@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz#b7245a5ea57db9679e8bf3032c25a5d2c5f54056"
+  integrity sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==
+
+"@rollup/rollup-win32-arm64-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz#768a128bb5da3c5472c3c56aec77507d28bc7209"
+  integrity sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==
+
+"@rollup/rollup-win32-ia32-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz#ce3f3b2eebe585340631498666718f00983a6a62"
+  integrity sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==
+
+"@rollup/rollup-win32-x64-msvc@4.49.0":
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz#c2a0e3b81262a7e9dd12ce18b350a97558dd50bc"
+  integrity sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==
 
 "@rtsao/scc@^1.1.0":
   version "1.1.0"
@@ -385,9 +557,9 @@
     "@types/node" "*"
 
 "@types/body-parser@*":
-  version "1.19.5"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
-  integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
+  version "1.19.6"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
+  integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==
   dependencies:
     "@types/connect" "*"
     "@types/node" "*"
@@ -405,29 +577,29 @@
     "@types/node" "*"
 
 "@types/content-disposition@*":
-  version "0.5.8"
-  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.8.tgz#6742a5971f490dc41e59d277eee71361fea0b537"
-  integrity sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==
+  version "0.5.9"
+  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.9.tgz#00ca14939432869de829a4ccf6fd380fa9181750"
+  integrity sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==
 
 "@types/cookies@*":
-  version "0.7.10"
-  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.10.tgz#c4881dca4dd913420c488508d192496c46eb4fd0"
-  integrity sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.1.tgz#083924ca1266e34ff240ca4d4fd6732ee5b81886"
+  integrity sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==
   dependencies:
     "@types/connect" "*"
     "@types/express" "*"
     "@types/keygrip" "*"
     "@types/node" "*"
 
-"@types/estree@0.0.39":
-  version "0.0.39"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
-  integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
+"@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+  integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
 
-"@types/express-serve-static-core@^4.17.33":
-  version "4.17.41"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
-  integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
+"@types/express-serve-static-core@^5.0.0":
+  version "5.0.7"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6"
+  integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==
   dependencies:
     "@types/node" "*"
     "@types/qs" "*"
@@ -435,26 +607,25 @@
     "@types/send" "*"
 
 "@types/express@*":
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
-  integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956"
+  integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==
   dependencies:
     "@types/body-parser" "*"
-    "@types/express-serve-static-core" "^4.17.33"
-    "@types/qs" "*"
+    "@types/express-serve-static-core" "^5.0.0"
     "@types/serve-static" "*"
 
 "@types/http-assert@*":
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf"
-  integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==
+  version "1.5.6"
+  resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.6.tgz#b6b657c38a2350d21ce213139f33b03b2b5fa431"
+  integrity sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==
 
-"@types/http-errors@*":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
-  integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
+"@types/http-errors@*", "@types/http-errors@^2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
+  integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
 
-"@types/json-schema@^7.0.9":
+"@types/json-schema@^7.0.15":
   version "7.0.15"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -476,10 +647,24 @@
   dependencies:
     "@types/koa" "*"
 
-"@types/koa@*", "@types/koa@^2.11.6":
-  version "2.13.12"
-  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.12.tgz#70d87a9061a81909e0ee11ca50168416e8d3e795"
-  integrity sha512-vAo1KuDSYWFDB4Cs80CHvfmzSQWeUb909aQib0C0aFx4sw0K9UZFz2m5jaEP+b3X1+yr904iQiruS0hXi31jbw==
+"@types/koa@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-3.0.0.tgz#5df2b3b30da902f01b6ec78ef56403e6eb62fa6e"
+  integrity sha512-MOcVYdVYmkSutVHZZPh8j3+dAjLyR5Tl59CN0eKgpkE1h/LBSmPAsQQuWs+bKu7WtGNn+hKfJH9Gzml+PulmDg==
+  dependencies:
+    "@types/accepts" "*"
+    "@types/content-disposition" "*"
+    "@types/cookies" "*"
+    "@types/http-assert" "*"
+    "@types/http-errors" "^2"
+    "@types/keygrip" "*"
+    "@types/koa-compose" "*"
+    "@types/node" "*"
+
+"@types/koa@^2.11.6":
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.15.0.tgz#eca43d76f527c803b491731f95df575636e7b6f2"
+  integrity sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==
   dependencies:
     "@types/accepts" "*"
     "@types/content-disposition" "*"
@@ -495,11 +680,6 @@
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
   integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
 
-"@types/mime@*":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
-  integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
-
 "@types/mime@^1":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
@@ -511,11 +691,11 @@
   integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
 
 "@types/node@*":
-  version "20.10.6"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
-  integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
+  version "24.3.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec"
+  integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
   dependencies:
-    undici-types "~5.26.4"
+    undici-types "~7.10.0"
 
 "@types/node@^10.1.0":
   version "10.17.60"
@@ -538,43 +718,36 @@
   integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
 
 "@types/qs@*":
-  version "6.9.11"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda"
-  integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==
+  version "6.14.0"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
+  integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==
 
 "@types/range-parser@*":
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
   integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
 
-"@types/resolve@1.17.1":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
-  integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/semver@^7.3.12":
-  version "7.5.6"
-  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
-  integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
+"@types/resolve@1.20.2":
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
+  integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
 
 "@types/send@*":
-  version "0.17.4"
-  resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
-  integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
+  version "0.17.5"
+  resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74"
+  integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==
   dependencies:
     "@types/mime" "^1"
     "@types/node" "*"
 
 "@types/serve-static@*":
-  version "1.15.5"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
-  integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
+  version "1.15.8"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877"
+  integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==
   dependencies:
     "@types/http-errors" "*"
-    "@types/mime" "*"
     "@types/node" "*"
+    "@types/send" "*"
 
 "@types/ws@^7.4.0":
   version "7.4.7"
@@ -583,111 +756,128 @@
   dependencies:
     "@types/node" "*"
 
-"@typescript-eslint/eslint-plugin@^4.2.0", "@typescript-eslint/eslint-plugin@^5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
-  integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
+"@typescript-eslint/eslint-plugin@5.62.0", "@typescript-eslint/eslint-plugin@^8.32.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz#42209e2ce3e2274de0f5f9b75c777deedacaa558"
+  integrity sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==
   dependencies:
-    "@eslint-community/regexpp" "^4.4.0"
-    "@typescript-eslint/scope-manager" "5.62.0"
-    "@typescript-eslint/type-utils" "5.62.0"
-    "@typescript-eslint/utils" "5.62.0"
-    debug "^4.3.4"
+    "@eslint-community/regexpp" "^4.10.0"
+    "@typescript-eslint/scope-manager" "8.41.0"
+    "@typescript-eslint/type-utils" "8.41.0"
+    "@typescript-eslint/utils" "8.41.0"
+    "@typescript-eslint/visitor-keys" "8.41.0"
     graphemer "^1.4.0"
-    ignore "^5.2.0"
-    natural-compare-lite "^1.4.0"
-    semver "^7.3.7"
-    tsutils "^3.21.0"
+    ignore "^7.0.0"
+    natural-compare "^1.4.0"
+    ts-api-utils "^2.1.0"
 
-"@typescript-eslint/parser@^4.2.0", "@typescript-eslint/parser@^5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
-  integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
+"@typescript-eslint/parser@5.62.0", "@typescript-eslint/parser@^8.32.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.41.0.tgz#677f5b2b3fa947ee1eac4129220c051b1990d898"
+  integrity sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.62.0"
-    "@typescript-eslint/types" "5.62.0"
-    "@typescript-eslint/typescript-estree" "5.62.0"
+    "@typescript-eslint/scope-manager" "8.41.0"
+    "@typescript-eslint/types" "8.41.0"
+    "@typescript-eslint/typescript-estree" "8.41.0"
+    "@typescript-eslint/visitor-keys" "8.41.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"
-  integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
+"@typescript-eslint/project-service@8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.41.0.tgz#08ebf882d413a038926e73fda36e00c3dba84882"
+  integrity sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==
   dependencies:
-    "@typescript-eslint/types" "5.62.0"
-    "@typescript-eslint/visitor-keys" "5.62.0"
-
-"@typescript-eslint/type-utils@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
-  integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
-  dependencies:
-    "@typescript-eslint/typescript-estree" "5.62.0"
-    "@typescript-eslint/utils" "5.62.0"
+    "@typescript-eslint/tsconfig-utils" "^8.41.0"
+    "@typescript-eslint/types" "^8.41.0"
     debug "^4.3.4"
-    tsutils "^3.21.0"
 
-"@typescript-eslint/types@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
-  integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
-
-"@typescript-eslint/typescript-estree@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
-  integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
+"@typescript-eslint/scope-manager@8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz#c8aba12129cb9cead1f1727f58e6a0fcebeecdb5"
+  integrity sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==
   dependencies:
-    "@typescript-eslint/types" "5.62.0"
-    "@typescript-eslint/visitor-keys" "5.62.0"
+    "@typescript-eslint/types" "8.41.0"
+    "@typescript-eslint/visitor-keys" "8.41.0"
+
+"@typescript-eslint/tsconfig-utils@8.41.0", "@typescript-eslint/tsconfig-utils@^8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz#134dee36eb16cdd78095a20bca0516d10b5dda75"
+  integrity sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==
+
+"@typescript-eslint/type-utils@8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz#68d401e38fccf239925447e97bdbd048a9891ae5"
+  integrity sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==
+  dependencies:
+    "@typescript-eslint/types" "8.41.0"
+    "@typescript-eslint/typescript-estree" "8.41.0"
+    "@typescript-eslint/utils" "8.41.0"
     debug "^4.3.4"
-    globby "^11.1.0"
+    ts-api-utils "^2.1.0"
+
+"@typescript-eslint/types@8.41.0", "@typescript-eslint/types@^8.11.0", "@typescript-eslint/types@^8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.41.0.tgz#9935afeaae65e535abcbcee95383fa649c64d16d"
+  integrity sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==
+
+"@typescript-eslint/typescript-estree@8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz#7c9cff8b4334ce96f14e9689692e8cf426ce4d59"
+  integrity sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==
+  dependencies:
+    "@typescript-eslint/project-service" "8.41.0"
+    "@typescript-eslint/tsconfig-utils" "8.41.0"
+    "@typescript-eslint/types" "8.41.0"
+    "@typescript-eslint/visitor-keys" "8.41.0"
+    debug "^4.3.4"
+    fast-glob "^3.3.2"
     is-glob "^4.0.3"
-    semver "^7.3.7"
-    tsutils "^3.21.0"
+    minimatch "^9.0.4"
+    semver "^7.6.0"
+    ts-api-utils "^2.1.0"
 
-"@typescript-eslint/utils@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
-  integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
+"@typescript-eslint/utils@8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.41.0.tgz#17cb3b766c1626311004ea41ffd8c27eb226b953"
+  integrity sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==
   dependencies:
-    "@eslint-community/eslint-utils" "^4.2.0"
-    "@types/json-schema" "^7.0.9"
-    "@types/semver" "^7.3.12"
-    "@typescript-eslint/scope-manager" "5.62.0"
-    "@typescript-eslint/types" "5.62.0"
-    "@typescript-eslint/typescript-estree" "5.62.0"
-    eslint-scope "^5.1.1"
-    semver "^7.3.7"
+    "@eslint-community/eslint-utils" "^4.7.0"
+    "@typescript-eslint/scope-manager" "8.41.0"
+    "@typescript-eslint/types" "8.41.0"
+    "@typescript-eslint/typescript-estree" "8.41.0"
 
-"@typescript-eslint/visitor-keys@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
-  integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
+"@typescript-eslint/visitor-keys@8.41.0":
+  version "8.41.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz#16eb99b55d207f6688002a2cf425e039579aa9a9"
+  integrity sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==
   dependencies:
-    "@typescript-eslint/types" "5.62.0"
-    eslint-visitor-keys "^3.3.0"
+    "@typescript-eslint/types" "8.41.0"
+    eslint-visitor-keys "^4.2.1"
 
-"@ungap/structured-clone@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
-  integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+"@vscode/l10n@^0.0.18":
+  version "0.0.18"
+  resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95"
+  integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==
 
-"@web/config-loader@^0.1.3":
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.1.3.tgz#8325ea54f75ef2ee7166783e64e66936db25bff7"
-  integrity sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==
-  dependencies:
-    semver "^7.3.4"
+"@vscode/web-custom-data@^0.4.2":
+  version "0.4.13"
+  resolved "https://registry.yarnpkg.com/@vscode/web-custom-data/-/web-custom-data-0.4.13.tgz#46d5f084fab6b8d4b47429ba5e22c42d2097ec8b"
+  integrity sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==
 
-"@web/dev-server-core@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.4.1.tgz#803faff45281ee296d0dda02dfdd905c330db4d8"
-  integrity sha512-KdYwejXZwIZvb6tYMCqU7yBiEOPfKLQ3V9ezqqEz8DA9V9R3oQWaowckvCpFB9IxxPfS/P8/59OkdzGKQjcIUw==
+"@web/config-loader@^0.3.0":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.3.3.tgz#13d4852c479d1f3adbc1f24d79c7e6eeedeb6e5d"
+  integrity sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==
+
+"@web/dev-server-core@^0.7.2", "@web/dev-server-core@^0.7.4":
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.7.5.tgz#b283d46eb2c0384e848311ec9da25d06bbbc47df"
+  integrity sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==
   dependencies:
     "@types/koa" "^2.11.6"
     "@types/ws" "^7.4.0"
-    "@web/parse5-utils" "^1.3.1"
-    chokidar "^3.4.3"
+    "@web/parse5-utils" "^2.1.0"
+    chokidar "^4.0.1"
     clone "^2.1.2"
     es-module-lexer "^1.0.0"
     get-stream "^6.0.0"
@@ -697,59 +887,59 @@
     koa-etag "^4.0.0"
     koa-send "^5.0.1"
     koa-static "^5.0.0"
-    lru-cache "^6.0.0"
+    lru-cache "^8.0.4"
     mime-types "^2.1.27"
     parse5 "^6.0.1"
     picomatch "^2.2.2"
-    ws "^7.4.2"
+    ws "^7.5.10"
 
-"@web/dev-server-esbuild@^0.3.6":
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-0.3.6.tgz#838100894937443b96bfc4266c7795d27ed4afac"
-  integrity sha512-VDcZOzvmbg/z/8Q54hHqFwt9U4cacQJZxgS8YXAvyFuG85HAJ/Q55P7Tr++1NlRS8wQEos6QK2ERUWNjEVOhqQ==
+"@web/dev-server-esbuild@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.4.tgz#7ac39eb4350da39fb3f7c5ce5c0fd423a8511346"
+  integrity sha512-ia1LxBwwRiQBYhJ7/RtLenHyPjzle3SvTw3jOZaeGv8UGXVPOkQV8fR05caOtW/DPPZaZovNAybzRKVnNiYIZg==
   dependencies:
     "@mdn/browser-compat-data" "^4.0.0"
-    "@web/dev-server-core" "^0.4.1"
-    esbuild "^0.16 || ^0.17"
+    "@web/dev-server-core" "^0.7.4"
+    esbuild "^0.25.0"
     parse5 "^6.0.1"
     ua-parser-js "^1.0.33"
 
-"@web/dev-server-rollup@^0.4.1":
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.4.1.tgz#3c6606bac8e497498b5b47bf9e0c544c321b38ef"
-  integrity sha512-Ebsv7Ovd9MufeH3exvikBJ7GmrZA5OmHnOgaiHcwMJ2eQBJA5/I+/CbRjsLX97ICj/ZwZG//p2ITRz8W3UfSqg==
+"@web/dev-server-rollup@^0.6.1":
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz#d0a4f69e4a659d2b79f172e86236cea6c872e81c"
+  integrity sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==
   dependencies:
-    "@rollup/plugin-node-resolve" "^13.0.4"
-    "@web/dev-server-core" "^0.4.1"
+    "@rollup/plugin-node-resolve" "^15.0.1"
+    "@web/dev-server-core" "^0.7.2"
     nanocolors "^0.2.1"
     parse5 "^6.0.1"
-    rollup "^2.67.0"
-    whatwg-url "^11.0.0"
+    rollup "^4.4.0"
+    whatwg-url "^14.0.0"
 
-"@web/dev-server@^0.1.38":
-  version "0.1.38"
-  resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.1.38.tgz#d755092d66aeb923c546237a6c460439ea3ddd29"
-  integrity sha512-WUq7Zi8KeJ5/UZmmpZ+kzUpUlFlMP/rcreJKYg9Lxiz998KYl4G5Rv24akX0piTuqXG7r6h+zszg8V/hdzjCoA==
+"@web/dev-server@^0.4.6":
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.4.6.tgz#18a4421d474a9bd1bfc90419a7a798ed0b5538c5"
+  integrity sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==
   dependencies:
     "@babel/code-frame" "^7.12.11"
     "@types/command-line-args" "^5.0.0"
-    "@web/config-loader" "^0.1.3"
-    "@web/dev-server-core" "^0.4.1"
-    "@web/dev-server-rollup" "^0.4.1"
+    "@web/config-loader" "^0.3.0"
+    "@web/dev-server-core" "^0.7.2"
+    "@web/dev-server-rollup" "^0.6.1"
     camelcase "^6.2.0"
     command-line-args "^5.1.1"
     command-line-usage "^7.0.1"
     debounce "^1.2.0"
     deepmerge "^4.2.2"
-    ip "^1.1.5"
+    internal-ip "^6.2.0"
     nanocolors "^0.2.1"
     open "^8.0.2"
     portfinder "^1.0.32"
 
-"@web/parse5-utils@^1.3.1":
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-1.3.1.tgz#6727be4d7875a9ecb96a5b3003bd271da763f8b4"
-  integrity sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==
+"@web/parse5-utils@^2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-2.1.0.tgz#3d33aca62c66773492f2fba89d23a45f8b57ba4a"
+  integrity sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==
   dependencies:
     "@types/parse5" "^6.0.1"
     parse5 "^6.0.1"
@@ -767,10 +957,10 @@
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^8.9.0:
-  version "8.14.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
-  integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
+acorn@^8.14.0, acorn@^8.15.0:
+  version "8.15.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
+  integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
 
 ajv@^6.12.4:
   version "6.12.6"
@@ -808,14 +998,6 @@
   dependencies:
     color-convert "^2.0.1"
 
-anymatch@~3.1.2:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
-  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
-  dependencies:
-    normalize-path "^3.0.0"
-    picomatch "^2.0.4"
-
 are-docs-informative@^0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963"
@@ -826,21 +1008,6 @@
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
-arr-diff@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
-  integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==
-
-arr-flatten@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
-  integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
-
-arr-union@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-  integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==
-
 array-back@^3.0.1, array-back@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
@@ -851,103 +1018,88 @@
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157"
   integrity sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==
 
-array-buffer-byte-length@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f"
-  integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==
+array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b"
+  integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==
   dependencies:
-    call-bind "^1.0.5"
-    is-array-buffer "^3.0.4"
+    call-bound "^1.0.3"
+    is-array-buffer "^3.0.5"
 
-array-includes@^3.1.8:
-  version "3.1.8"
-  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d"
-  integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==
+array-includes@^3.1.9:
+  version "3.1.9"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a"
+  integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==
   dependencies:
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
+    call-bound "^1.0.4"
     define-properties "^1.2.1"
-    es-abstract "^1.23.2"
-    es-object-atoms "^1.0.0"
-    get-intrinsic "^1.2.4"
-    is-string "^1.0.7"
+    es-abstract "^1.24.0"
+    es-object-atoms "^1.1.1"
+    get-intrinsic "^1.3.0"
+    is-string "^1.1.1"
+    math-intrinsics "^1.1.0"
 
-array-union@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
-  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
-
-array-unique@^0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
-  integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==
-
-array.prototype.findlastindex@^1.2.5:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d"
-  integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==
+array.prototype.findlastindex@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564"
+  integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==
   dependencies:
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
+    call-bound "^1.0.4"
     define-properties "^1.2.1"
-    es-abstract "^1.23.2"
+    es-abstract "^1.23.9"
     es-errors "^1.3.0"
-    es-object-atoms "^1.0.0"
+    es-object-atoms "^1.1.1"
+    es-shim-unscopables "^1.1.0"
+
+array.prototype.flat@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5"
+  integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==
+  dependencies:
+    call-bind "^1.0.8"
+    define-properties "^1.2.1"
+    es-abstract "^1.23.5"
     es-shim-unscopables "^1.0.2"
 
-array.prototype.flat@^1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18"
-  integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==
+array.prototype.flatmap@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b"
+  integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==
   dependencies:
-    call-bind "^1.0.2"
-    define-properties "^1.2.0"
-    es-abstract "^1.22.1"
-    es-shim-unscopables "^1.0.0"
+    call-bind "^1.0.8"
+    define-properties "^1.2.1"
+    es-abstract "^1.23.5"
+    es-shim-unscopables "^1.0.2"
 
-array.prototype.flatmap@^1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527"
-  integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==
-  dependencies:
-    call-bind "^1.0.2"
-    define-properties "^1.2.0"
-    es-abstract "^1.22.1"
-    es-shim-unscopables "^1.0.0"
-
-arraybuffer.prototype.slice@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6"
-  integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==
+arraybuffer.prototype.slice@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c"
+  integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==
   dependencies:
     array-buffer-byte-length "^1.0.1"
-    call-bind "^1.0.5"
+    call-bind "^1.0.8"
     define-properties "^1.2.1"
-    es-abstract "^1.22.3"
-    es-errors "^1.2.1"
-    get-intrinsic "^1.2.3"
+    es-abstract "^1.23.5"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.6"
     is-array-buffer "^3.0.4"
-    is-shared-array-buffer "^1.0.2"
 
 arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
 
-assign-symbols@^1.0.0:
+async-function@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
-  integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==
+  resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b"
+  integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==
 
-async@^2.6.4:
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
-  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
-  dependencies:
-    lodash "^4.17.14"
-
-atob@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
-  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+async@^3.2.6:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+  integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
 
 available-typed-arrays@^1.0.7:
   version "1.0.7"
@@ -961,79 +1113,39 @@
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base@^0.11.1:
-  version "0.11.2"
-  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
-  integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
-  dependencies:
-    cache-base "^1.0.1"
-    class-utils "^0.3.5"
-    component-emitter "^1.2.1"
-    define-property "^1.0.0"
-    isobject "^3.0.1"
-    mixin-deep "^1.2.0"
-    pascalcase "^0.1.1"
-
-binary-extensions@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
-  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
 brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
+  integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
   dependencies:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-braces@^2.3.1:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
-  integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+brace-expansion@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
+  integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
   dependencies:
-    arr-flatten "^1.1.0"
-    array-unique "^0.3.2"
-    extend-shallow "^2.0.1"
-    fill-range "^4.0.0"
-    isobject "^3.0.1"
-    repeat-element "^1.1.2"
-    snapdragon "^0.8.1"
-    snapdragon-node "^2.0.1"
-    split-string "^3.0.2"
-    to-regex "^3.0.1"
+    balanced-match "^1.0.0"
 
-braces@^3.0.2, braces@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
-  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+braces@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
   dependencies:
-    fill-range "^7.0.1"
+    fill-range "^7.1.1"
 
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
-builtin-modules@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
-  integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
-
-cache-base@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
-  integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+builtins@^5.0.1:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.1.0.tgz#6d85eeb360c4ebc166c3fdef922a15aa7316a5e8"
+  integrity sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==
   dependencies:
-    collection-visit "^1.0.0"
-    component-emitter "^1.2.1"
-    get-value "^2.0.6"
-    has-value "^1.0.0"
-    isobject "^3.0.1"
-    set-value "^2.0.0"
-    to-object-path "^0.3.0"
-    union-value "^1.0.0"
-    unset-value "^1.0.0"
+    semver "^7.0.0"
 
 cache-content-type@^1.0.0:
   version "1.0.1"
@@ -1043,21 +1155,31 @@
     mime-types "^2.1.18"
     ylru "^1.2.0"
 
-call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
-  integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
+call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+  integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
   dependencies:
-    es-define-property "^1.0.0"
     es-errors "^1.3.0"
     function-bind "^1.1.2"
-    get-intrinsic "^1.2.4"
-    set-function-length "^1.2.1"
 
-call-me-maybe@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa"
-  integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==
+call-bind@^1.0.7, call-bind@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
+  integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
+  dependencies:
+    call-bind-apply-helpers "^1.0.0"
+    es-define-property "^1.0.0"
+    get-intrinsic "^1.2.4"
+    set-function-length "^1.2.2"
+
+call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+  integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
+  dependencies:
+    call-bind-apply-helpers "^1.0.2"
+    get-intrinsic "^1.3.0"
 
 callsites@^3.0.0:
   version "3.1.0"
@@ -1073,7 +1195,7 @@
     map-obj "^4.0.0"
     quick-lru "^4.0.1"
 
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -1112,30 +1234,12 @@
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
-chokidar@^3.4.3:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
-  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+chokidar@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
+  integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
   dependencies:
-    anymatch "~3.1.2"
-    braces "~3.0.2"
-    glob-parent "~5.1.2"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.6.0"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-class-utils@^0.3.5:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
-  integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
-  dependencies:
-    arr-union "^3.1.0"
-    define-property "^0.2.5"
-    isobject "^3.0.0"
-    static-extend "^0.1.1"
+    readdirp "^4.0.1"
 
 cli-cursor@^3.1.0:
   version "3.1.0"
@@ -1149,14 +1253,14 @@
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
   integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
 
-cliui@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
-  integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+cliui@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
+  integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
   dependencies:
     string-width "^4.2.0"
-    strip-ansi "^6.0.0"
-    wrap-ansi "^6.2.0"
+    strip-ansi "^6.0.1"
+    wrap-ansi "^7.0.0"
 
 clone@^2.1.2:
   version "2.1.2"
@@ -1168,14 +1272,6 @@
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
   integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
 
-collection-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
-  integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==
-  dependencies:
-    map-visit "^1.0.0"
-    object-visit "^1.0.0"
-
 color-convert@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -1200,7 +1296,7 @@
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-command-line-args@^5.1.1, command-line-args@^5.2.1:
+command-line-args@^5.1.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e"
   integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==
@@ -1210,14 +1306,14 @@
     lodash.camelcase "^4.3.0"
     typical "^4.0.0"
 
-command-line-usage@^7.0.0, command-line-usage@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-7.0.1.tgz#e540afef4a4f3bc501b124ffde33956309100655"
-  integrity sha512-NCyznE//MuTjwi3y84QVUGEOT+P5oto1e1Pk/jFPVdPPfsG03qpTIl3yw6etR+v73d0lXsoojRpvbru2sqePxQ==
+command-line-usage@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-7.0.3.tgz#6bce992354f6af10ecea2b631bfdf0c8b3bfaea3"
+  integrity sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==
   dependencies:
     array-back "^6.2.2"
     chalk-template "^0.4.0"
-    table-layout "^3.0.0"
+    table-layout "^4.1.0"
     typical "^7.1.1"
 
 commander@^2.20.0:
@@ -1225,15 +1321,10 @@
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-comment-parser@1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b"
-  integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
-
-component-emitter@^1.2.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
-  integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
+comment-parser@1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc"
+  integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==
 
 concat-map@0.0.1:
   version "0.0.1"
@@ -1260,15 +1351,10 @@
     depd "~2.0.0"
     keygrip "~1.1.0"
 
-copy-descriptor@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-  integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==
-
 cross-spawn@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
-  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
+  integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
   dependencies:
     nice-try "^1.0.4"
     path-key "^2.0.1"
@@ -1276,39 +1362,39 @@
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^7.0.2, cross-spawn@^7.0.3:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
-  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+cross-spawn@^7.0.3, cross-spawn@^7.0.6:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
   dependencies:
     path-key "^3.1.0"
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-data-view-buffer@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
-  integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==
+data-view-buffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570"
+  integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==
   dependencies:
-    call-bind "^1.0.6"
+    call-bound "^1.0.3"
     es-errors "^1.3.0"
-    is-data-view "^1.0.1"
+    is-data-view "^1.0.2"
 
-data-view-byte-length@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2"
-  integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==
+data-view-byte-length@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735"
+  integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==
   dependencies:
-    call-bind "^1.0.7"
+    call-bound "^1.0.3"
     es-errors "^1.3.0"
-    is-data-view "^1.0.1"
+    is-data-view "^1.0.2"
 
-data-view-byte-offset@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a"
-  integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==
+data-view-byte-offset@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191"
+  integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==
   dependencies:
-    call-bind "^1.0.6"
+    call-bound "^1.0.2"
     es-errors "^1.3.0"
     is-data-view "^1.0.1"
 
@@ -1317,13 +1403,6 @@
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
   integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
 
-debug@^2.2.0, debug@^2.3.3:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
-  dependencies:
-    ms "2.0.0"
-
 debug@^3.1.0, debug@^3.2.7:
   version "3.2.7"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@@ -1331,17 +1410,10 @@
   dependencies:
     ms "^2.1.1"
 
-debug@^4.1.1, debug@^4.3.4:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
-  dependencies:
-    ms "2.1.2"
-
-debug@^4.3.1, debug@^4.3.2:
-  version "4.3.7"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
-  integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.1:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+  integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
   dependencies:
     ms "^2.1.3"
 
@@ -1358,11 +1430,6 @@
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
 
-decode-uri-component@^0.2.0:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
-  integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
-
 deep-equal@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -1378,6 +1445,13 @@
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
   integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
 
+default-gateway@^6.0.0:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71"
+  integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==
+  dependencies:
+    execa "^5.0.0"
+
 define-data-property@^1.0.1, define-data-property@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
@@ -1392,7 +1466,7 @@
   resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
   integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
 
-define-properties@^1.2.0, define-properties@^1.2.1:
+define-properties@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
   integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
@@ -1401,28 +1475,6 @@
     has-property-descriptors "^1.0.0"
     object-keys "^1.1.1"
 
-define-property@^0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
-  integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==
-  dependencies:
-    is-descriptor "^0.1.0"
-
-define-property@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
-  integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==
-  dependencies:
-    is-descriptor "^1.0.0"
-
-define-property@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
-  integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
-  dependencies:
-    is-descriptor "^1.0.2"
-    isobject "^3.0.1"
-
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -1452,13 +1504,6 @@
     leven "^3.1.0"
     lodash.deburr "^4.1.0"
 
-dir-glob@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
-  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
-  dependencies:
-    path-type "^4.0.0"
-
 doctrine@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -1466,13 +1511,6 @@
   dependencies:
     esutils "^2.0.2"
 
-doctrine@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
-  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
-  dependencies:
-    esutils "^2.0.2"
-
 dom-serializer@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
@@ -1494,15 +1532,24 @@
   dependencies:
     domelementtype "^2.3.0"
 
-domutils@^3.0.1:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
-  integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
+domutils@^3.2.1:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
+  integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==
   dependencies:
     dom-serializer "^2.0.0"
     domelementtype "^2.3.0"
     domhandler "^5.0.3"
 
+dunder-proto@^1.0.0, dunder-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    es-errors "^1.3.0"
+    gopd "^1.2.0"
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -1518,11 +1565,24 @@
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
 
-entities@^4.2.0, entities@^4.4.0:
+enhanced-resolve@^5.17.1:
+  version "5.18.3"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44"
+  integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+entities@^4.2.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
   integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
 
+entities@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
+  integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
+
 error-ex@^1.3.1:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -1530,134 +1590,150 @@
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2:
-  version "1.23.3"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0"
-  integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==
+es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0:
+  version "1.24.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328"
+  integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==
   dependencies:
-    array-buffer-byte-length "^1.0.1"
-    arraybuffer.prototype.slice "^1.0.3"
+    array-buffer-byte-length "^1.0.2"
+    arraybuffer.prototype.slice "^1.0.4"
     available-typed-arrays "^1.0.7"
-    call-bind "^1.0.7"
-    data-view-buffer "^1.0.1"
-    data-view-byte-length "^1.0.1"
-    data-view-byte-offset "^1.0.0"
-    es-define-property "^1.0.0"
+    call-bind "^1.0.8"
+    call-bound "^1.0.4"
+    data-view-buffer "^1.0.2"
+    data-view-byte-length "^1.0.2"
+    data-view-byte-offset "^1.0.1"
+    es-define-property "^1.0.1"
     es-errors "^1.3.0"
-    es-object-atoms "^1.0.0"
-    es-set-tostringtag "^2.0.3"
-    es-to-primitive "^1.2.1"
-    function.prototype.name "^1.1.6"
-    get-intrinsic "^1.2.4"
-    get-symbol-description "^1.0.2"
-    globalthis "^1.0.3"
-    gopd "^1.0.1"
+    es-object-atoms "^1.1.1"
+    es-set-tostringtag "^2.1.0"
+    es-to-primitive "^1.3.0"
+    function.prototype.name "^1.1.8"
+    get-intrinsic "^1.3.0"
+    get-proto "^1.0.1"
+    get-symbol-description "^1.1.0"
+    globalthis "^1.0.4"
+    gopd "^1.2.0"
     has-property-descriptors "^1.0.2"
-    has-proto "^1.0.3"
-    has-symbols "^1.0.3"
+    has-proto "^1.2.0"
+    has-symbols "^1.1.0"
     hasown "^2.0.2"
-    internal-slot "^1.0.7"
-    is-array-buffer "^3.0.4"
+    internal-slot "^1.1.0"
+    is-array-buffer "^3.0.5"
     is-callable "^1.2.7"
-    is-data-view "^1.0.1"
+    is-data-view "^1.0.2"
     is-negative-zero "^2.0.3"
-    is-regex "^1.1.4"
-    is-shared-array-buffer "^1.0.3"
-    is-string "^1.0.7"
-    is-typed-array "^1.1.13"
-    is-weakref "^1.0.2"
-    object-inspect "^1.13.1"
+    is-regex "^1.2.1"
+    is-set "^2.0.3"
+    is-shared-array-buffer "^1.0.4"
+    is-string "^1.1.1"
+    is-typed-array "^1.1.15"
+    is-weakref "^1.1.1"
+    math-intrinsics "^1.1.0"
+    object-inspect "^1.13.4"
     object-keys "^1.1.1"
-    object.assign "^4.1.5"
-    regexp.prototype.flags "^1.5.2"
-    safe-array-concat "^1.1.2"
-    safe-regex-test "^1.0.3"
-    string.prototype.trim "^1.2.9"
-    string.prototype.trimend "^1.0.8"
+    object.assign "^4.1.7"
+    own-keys "^1.0.1"
+    regexp.prototype.flags "^1.5.4"
+    safe-array-concat "^1.1.3"
+    safe-push-apply "^1.0.0"
+    safe-regex-test "^1.1.0"
+    set-proto "^1.0.0"
+    stop-iteration-iterator "^1.1.0"
+    string.prototype.trim "^1.2.10"
+    string.prototype.trimend "^1.0.9"
     string.prototype.trimstart "^1.0.8"
-    typed-array-buffer "^1.0.2"
-    typed-array-byte-length "^1.0.1"
-    typed-array-byte-offset "^1.0.2"
-    typed-array-length "^1.0.6"
-    unbox-primitive "^1.0.2"
-    which-typed-array "^1.1.15"
+    typed-array-buffer "^1.0.3"
+    typed-array-byte-length "^1.0.3"
+    typed-array-byte-offset "^1.0.4"
+    typed-array-length "^1.0.7"
+    unbox-primitive "^1.1.0"
+    which-typed-array "^1.1.19"
 
-es-define-property@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
-  integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
-  dependencies:
-    get-intrinsic "^1.2.4"
+es-define-property@^1.0.0, es-define-property@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
 
-es-errors@^1.2.1, es-errors@^1.3.0:
+es-errors@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
   integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
 
 es-module-lexer@^1.0.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
-  integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
+  integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
 
-es-object-atoms@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
-  integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+  integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
   dependencies:
     es-errors "^1.3.0"
 
-es-set-tostringtag@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777"
-  integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==
+es-set-tostringtag@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+  integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
   dependencies:
-    get-intrinsic "^1.2.4"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.6"
     has-tostringtag "^1.0.2"
-    hasown "^2.0.1"
+    hasown "^2.0.2"
 
-es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763"
-  integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==
+es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5"
+  integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==
   dependencies:
-    hasown "^2.0.0"
+    hasown "^2.0.2"
 
-es-to-primitive@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
-  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+es-to-primitive@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18"
+  integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==
   dependencies:
-    is-callable "^1.1.4"
-    is-date-object "^1.0.1"
-    is-symbol "^1.0.2"
+    is-callable "^1.2.7"
+    is-date-object "^1.0.5"
+    is-symbol "^1.0.4"
 
-"esbuild@^0.16 || ^0.17":
-  version "0.17.19"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
-  integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
+esbuild@^0.25.0:
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976"
+  integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==
   optionalDependencies:
-    "@esbuild/android-arm" "0.17.19"
-    "@esbuild/android-arm64" "0.17.19"
-    "@esbuild/android-x64" "0.17.19"
-    "@esbuild/darwin-arm64" "0.17.19"
-    "@esbuild/darwin-x64" "0.17.19"
-    "@esbuild/freebsd-arm64" "0.17.19"
-    "@esbuild/freebsd-x64" "0.17.19"
-    "@esbuild/linux-arm" "0.17.19"
-    "@esbuild/linux-arm64" "0.17.19"
-    "@esbuild/linux-ia32" "0.17.19"
-    "@esbuild/linux-loong64" "0.17.19"
-    "@esbuild/linux-mips64el" "0.17.19"
-    "@esbuild/linux-ppc64" "0.17.19"
-    "@esbuild/linux-riscv64" "0.17.19"
-    "@esbuild/linux-s390x" "0.17.19"
-    "@esbuild/linux-x64" "0.17.19"
-    "@esbuild/netbsd-x64" "0.17.19"
-    "@esbuild/openbsd-x64" "0.17.19"
-    "@esbuild/sunos-x64" "0.17.19"
-    "@esbuild/win32-arm64" "0.17.19"
-    "@esbuild/win32-ia32" "0.17.19"
-    "@esbuild/win32-x64" "0.17.19"
+    "@esbuild/aix-ppc64" "0.25.9"
+    "@esbuild/android-arm" "0.25.9"
+    "@esbuild/android-arm64" "0.25.9"
+    "@esbuild/android-x64" "0.25.9"
+    "@esbuild/darwin-arm64" "0.25.9"
+    "@esbuild/darwin-x64" "0.25.9"
+    "@esbuild/freebsd-arm64" "0.25.9"
+    "@esbuild/freebsd-x64" "0.25.9"
+    "@esbuild/linux-arm" "0.25.9"
+    "@esbuild/linux-arm64" "0.25.9"
+    "@esbuild/linux-ia32" "0.25.9"
+    "@esbuild/linux-loong64" "0.25.9"
+    "@esbuild/linux-mips64el" "0.25.9"
+    "@esbuild/linux-ppc64" "0.25.9"
+    "@esbuild/linux-riscv64" "0.25.9"
+    "@esbuild/linux-s390x" "0.25.9"
+    "@esbuild/linux-x64" "0.25.9"
+    "@esbuild/netbsd-arm64" "0.25.9"
+    "@esbuild/netbsd-x64" "0.25.9"
+    "@esbuild/openbsd-arm64" "0.25.9"
+    "@esbuild/openbsd-x64" "0.25.9"
+    "@esbuild/openharmony-arm64" "0.25.9"
+    "@esbuild/sunos-x64" "0.25.9"
+    "@esbuild/win32-arm64" "0.25.9"
+    "@esbuild/win32-ia32" "0.25.9"
+    "@esbuild/win32-x64" "0.25.9"
+
+escalade@^3.1.1:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+  integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
 
 escape-html@^1.0.3:
   version "1.0.3"
@@ -1674,15 +1750,22 @@
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
+eslint-compat-utils@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz#7fc92b776d185a70c4070d03fd26fde3d59652e4"
+  integrity sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==
+  dependencies:
+    semver "^7.5.4"
+
 eslint-config-google@^0.14.0:
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
   integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
 
-eslint-config-prettier@^7.0.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9"
-  integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==
+eslint-config-prettier@9.1.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
+  integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
 
 eslint-import-resolver-node@^0.3.9:
   version "0.3.9"
@@ -1693,66 +1776,86 @@
     is-core-module "^2.13.0"
     resolve "^1.22.4"
 
-eslint-module-utils@^2.12.0:
-  version "2.12.0"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b"
-  integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==
+eslint-module-utils@^2.12.1:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff"
+  integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==
   dependencies:
     debug "^3.2.7"
 
-eslint-plugin-es@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
-  integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
+eslint-plugin-es-x@8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-8.0.0.tgz#66835093aae1cdf7e1639a17036ca179369ac456"
+  integrity sha512-kPIagK5FxZBDwVxZXCsxmZUjU2aYGeTs4/wfAauI2FAThsbeLgred5b+6S0x7Hhx04GPzrB4j0h60bnsyLpzEA==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.1.2"
+    "@eslint-community/regexpp" "^4.11.0"
+    eslint-compat-utils "^0.5.1"
+
+eslint-plugin-es-x@^7.8.0:
+  version "7.8.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz#a207aa08da37a7923f2a9599e6d3eb73f3f92b74"
+  integrity sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.1.2"
+    "@eslint-community/regexpp" "^4.11.0"
+    eslint-compat-utils "^0.5.1"
+
+eslint-plugin-es@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9"
+  integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==
   dependencies:
     eslint-utils "^2.0.0"
     regexpp "^3.0.0"
 
-eslint-plugin-html@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-7.1.0.tgz#aec2a3772b40ccf51a5be4f972f07600539d3b3e"
-  integrity sha512-fNLRraV/e6j8e3XYOC9xgND4j+U7b1Rq+OygMlLcMg+wI/IpVbF+ubQa3R78EjKB9njT6TQOlcK5rFKBVVtdfg==
+eslint-plugin-html@^8.1.3:
+  version "8.1.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-8.1.3.tgz#67168911914ccd68526d7c0f4111b81bc62036d6"
+  integrity sha512-cnCdO7yb/jrvgSJJAfRkGDOwLu1AOvNdw8WCD6nh/2C4RnxuI4tz6QjMEAmmSiHSeugq/fXcIO8yBpIBQrMZCg==
   dependencies:
-    htmlparser2 "^8.0.1"
+    htmlparser2 "^10.0.0"
 
 eslint-plugin-import@^2.31.0:
-  version "2.31.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7"
-  integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==
+  version "2.32.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980"
+  integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==
   dependencies:
     "@rtsao/scc" "^1.1.0"
-    array-includes "^3.1.8"
-    array.prototype.findlastindex "^1.2.5"
-    array.prototype.flat "^1.3.2"
-    array.prototype.flatmap "^1.3.2"
+    array-includes "^3.1.9"
+    array.prototype.findlastindex "^1.2.6"
+    array.prototype.flat "^1.3.3"
+    array.prototype.flatmap "^1.3.3"
     debug "^3.2.7"
     doctrine "^2.1.0"
     eslint-import-resolver-node "^0.3.9"
-    eslint-module-utils "^2.12.0"
+    eslint-module-utils "^2.12.1"
     hasown "^2.0.2"
-    is-core-module "^2.15.1"
+    is-core-module "^2.16.1"
     is-glob "^4.0.3"
     minimatch "^3.1.2"
     object.fromentries "^2.0.8"
     object.groupby "^1.0.3"
-    object.values "^1.2.0"
+    object.values "^1.2.1"
     semver "^6.3.1"
-    string.prototype.trimend "^1.0.8"
+    string.prototype.trimend "^1.0.9"
     tsconfig-paths "^3.15.0"
 
-eslint-plugin-jsdoc@^44.2.7:
-  version "44.2.7"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-44.2.7.tgz#5ecdb46ddfca209ecd58fff972a4eb74b8dde599"
-  integrity sha512-PcAJO7Wh4xIHPT+StBRpEbWgwCpIrYk75zL31RMbduVVHpgiy3Y8aXQ6pdbRJOq0fxHuepWSEAve8ZrPWTSKRg==
+eslint-plugin-jsdoc@^50.6.11:
+  version "50.8.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.8.0.tgz#a8d192ccca26df368a2fbaff17c9dddefacd773f"
+  integrity sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg==
   dependencies:
-    "@es-joy/jsdoccomment" "~0.39.4"
+    "@es-joy/jsdoccomment" "~0.50.2"
     are-docs-informative "^0.0.2"
-    comment-parser "1.3.1"
-    debug "^4.3.4"
+    comment-parser "1.4.1"
+    debug "^4.4.1"
     escape-string-regexp "^4.0.0"
-    esquery "^1.5.0"
-    semver "^7.5.1"
-    spdx-expression-parse "^3.0.1"
+    espree "^10.3.0"
+    esquery "^1.6.0"
+    parse-imports-exports "^0.2.4"
+    semver "^7.7.2"
+    spdx-expression-parse "^4.0.0"
 
 eslint-plugin-lit@^1.15.0:
   version "1.15.0"
@@ -1763,49 +1866,60 @@
     parse5-htmlparser2-tree-adapter "^6.0.1"
     requireindex "^1.2.0"
 
-eslint-plugin-node@^11.1.0:
-  version "11.1.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
-  integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
+eslint-plugin-n@15.7.0:
+  version "15.7.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz#e29221d8f5174f84d18f2eb94765f2eeea033b90"
+  integrity sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==
   dependencies:
-    eslint-plugin-es "^3.0.0"
-    eslint-utils "^2.0.0"
+    builtins "^5.0.1"
+    eslint-plugin-es "^4.1.0"
+    eslint-utils "^3.0.0"
     ignore "^5.1.1"
-    minimatch "^3.0.4"
-    resolve "^1.10.1"
-    semver "^6.1.0"
+    is-core-module "^2.11.0"
+    minimatch "^3.1.2"
+    resolve "^1.22.1"
+    semver "^7.3.8"
 
-eslint-plugin-prettier@^3.1.4:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5"
-  integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==
+eslint-plugin-n@^17.17.0:
+  version "17.21.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz#a07592c28390ac742bf52acae89048c997a7b91c"
+  integrity sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.5.0"
+    enhanced-resolve "^5.17.1"
+    eslint-plugin-es-x "^7.8.0"
+    get-tsconfig "^4.8.1"
+    globals "^15.11.0"
+    globrex "^0.1.2"
+    ignore "^5.3.2"
+    semver "^7.6.3"
+    ts-declaration-location "^1.0.6"
+
+eslint-plugin-prettier@5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95"
+  integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==
   dependencies:
     prettier-linter-helpers "^1.0.0"
+    synckit "^0.9.1"
 
-eslint-plugin-prettier@^4.2.1:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
-  integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
+eslint-plugin-prettier@^5.4.0:
+  version "5.5.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz#9d61c4ea11de5af704d4edf108c82ccfa7f2e61c"
+  integrity sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==
   dependencies:
     prettier-linter-helpers "^1.0.0"
+    synckit "^0.11.7"
 
 eslint-plugin-regex@^1.10.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-regex/-/eslint-plugin-regex-1.10.0.tgz#d182cedbeb89eb03cd8e53f750f6f92e14fa6f9c"
   integrity sha512-C8/qYKkkbIb0epxKzaz4aw7oVAOmm19fJpR/moUrUToq/vc4xW4sEKMlTQqH6EtNGpvLjYsbbZRlWNWwQGeTSA==
 
-eslint-scope@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
-  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
-  dependencies:
-    esrecurse "^4.3.0"
-    estraverse "^4.1.1"
-
-eslint-scope@^7.2.2:
-  version "7.2.2"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
-  integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
+eslint-scope@^8.4.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
+  integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"
@@ -1817,83 +1931,90 @@
   dependencies:
     eslint-visitor-keys "^1.1.0"
 
+eslint-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
+  integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
+  dependencies:
+    eslint-visitor-keys "^2.0.0"
+
 eslint-visitor-keys@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
   integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
 
-eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
+eslint-visitor-keys@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
+  integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
+
+eslint-visitor-keys@^3.4.3:
   version "3.4.3"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
 
-eslint@^7.10.0, eslint@^8.57.1:
-  version "8.57.1"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
-  integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
+eslint-visitor-keys@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
+  integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
+
+eslint@8.57.1, eslint@^9.26.0:
+  version "9.34.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.34.0.tgz#0ea1f2c1b5d1671db8f01aa6b8ce722302016f7b"
+  integrity sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
-    "@eslint-community/regexpp" "^4.6.1"
-    "@eslint/eslintrc" "^2.1.4"
-    "@eslint/js" "8.57.1"
-    "@humanwhocodes/config-array" "^0.13.0"
+    "@eslint-community/regexpp" "^4.12.1"
+    "@eslint/config-array" "^0.21.0"
+    "@eslint/config-helpers" "^0.3.1"
+    "@eslint/core" "^0.15.2"
+    "@eslint/eslintrc" "^3.3.1"
+    "@eslint/js" "9.34.0"
+    "@eslint/plugin-kit" "^0.3.5"
+    "@humanfs/node" "^0.16.6"
     "@humanwhocodes/module-importer" "^1.0.1"
-    "@nodelib/fs.walk" "^1.2.8"
-    "@ungap/structured-clone" "^1.2.0"
+    "@humanwhocodes/retry" "^0.4.2"
+    "@types/estree" "^1.0.6"
+    "@types/json-schema" "^7.0.15"
     ajv "^6.12.4"
     chalk "^4.0.0"
-    cross-spawn "^7.0.2"
+    cross-spawn "^7.0.6"
     debug "^4.3.2"
-    doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^7.2.2"
-    eslint-visitor-keys "^3.4.3"
-    espree "^9.6.1"
-    esquery "^1.4.2"
+    eslint-scope "^8.4.0"
+    eslint-visitor-keys "^4.2.1"
+    espree "^10.4.0"
+    esquery "^1.5.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
-    file-entry-cache "^6.0.1"
+    file-entry-cache "^8.0.0"
     find-up "^5.0.0"
     glob-parent "^6.0.2"
-    globals "^13.19.0"
-    graphemer "^1.4.0"
     ignore "^5.2.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
-    is-path-inside "^3.0.3"
-    js-yaml "^4.1.0"
     json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.4.1"
     lodash.merge "^4.6.2"
     minimatch "^3.1.2"
     natural-compare "^1.4.0"
     optionator "^0.9.3"
-    strip-ansi "^6.0.1"
-    text-table "^0.2.0"
 
-espree@^9.6.0, espree@^9.6.1:
-  version "9.6.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
-  integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+espree@^10.0.1, espree@^10.3.0, espree@^10.4.0:
+  version "10.4.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
+  integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
   dependencies:
-    acorn "^8.9.0"
+    acorn "^8.15.0"
     acorn-jsx "^5.3.2"
-    eslint-visitor-keys "^3.4.1"
+    eslint-visitor-keys "^4.2.1"
 
-esquery@^1.4.2:
+esquery@^1.5.0, esquery@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
   integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
   dependencies:
     estraverse "^5.1.0"
 
-esquery@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
-  integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
-  dependencies:
-    estraverse "^5.1.0"
-
 esrecurse@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
@@ -1901,20 +2022,15 @@
   dependencies:
     estraverse "^5.2.0"
 
-estraverse@^4.1.1:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
-  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
-
 estraverse@^5.1.0, estraverse@^5.2.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
 
-estree-walker@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
-  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 
 esutils@^2.0.2:
   version "2.0.3"
@@ -1941,34 +2057,6 @@
     signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
-expand-brackets@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
-  integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==
-  dependencies:
-    debug "^2.3.3"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    posix-character-classes "^0.1.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-extend-shallow@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
-  integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==
-  dependencies:
-    is-extendable "^0.1.0"
-
-extend-shallow@^3.0.0, extend-shallow@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
-  integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==
-  dependencies:
-    assign-symbols "^1.0.0"
-    is-extendable "^1.0.1"
-
 external-editor@^3.0.3:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
@@ -1978,20 +2066,6 @@
     iconv-lite "^0.4.24"
     tmp "^0.0.33"
 
-extglob@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
-  integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
-  dependencies:
-    array-unique "^0.3.2"
-    define-property "^1.0.0"
-    expand-brackets "^2.1.4"
-    extend-shallow "^2.0.1"
-    fragment-cache "^0.2.1"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -2002,28 +2076,16 @@
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
   integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
 
-fast-glob@^2.2.6:
-  version "2.2.7"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
-  integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
-  dependencies:
-    "@mrmlnc/readdir-enhanced" "^2.2.1"
-    "@nodelib/fs.stat" "^1.1.2"
-    glob-parent "^3.1.0"
-    is-glob "^4.0.0"
-    merge2 "^1.2.3"
-    micromatch "^3.1.10"
-
-fast-glob@^3.2.2, fast-glob@^3.2.9:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
-  integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
+fast-glob@^3.2.11, fast-glob@^3.2.2, fast-glob@^3.3.2:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+  integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.walk" "^1.2.3"
     glob-parent "^5.1.2"
     merge2 "^1.3.0"
-    micromatch "^4.0.4"
+    micromatch "^4.0.8"
 
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
@@ -2036,9 +2098,9 @@
   integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
 
 fastq@^1.6.0:
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
-  integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+  integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
   dependencies:
     reusify "^1.0.4"
 
@@ -2049,27 +2111,17 @@
   dependencies:
     escape-string-regexp "^1.0.5"
 
-file-entry-cache@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
-  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+file-entry-cache@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
+  integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
   dependencies:
-    flat-cache "^3.0.4"
+    flat-cache "^4.0.0"
 
-fill-range@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
-  integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-    to-regex-range "^2.1.0"
-
-fill-range@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
-  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+fill-range@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
   dependencies:
     to-regex-range "^5.0.1"
 
@@ -2096,38 +2148,25 @@
     locate-path "^6.0.0"
     path-exists "^4.0.0"
 
-flat-cache@^3.0.4:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
-  integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
+flat-cache@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
+  integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
   dependencies:
     flatted "^3.2.9"
-    keyv "^4.5.3"
-    rimraf "^3.0.2"
+    keyv "^4.5.4"
 
 flatted@^3.2.9:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
-  integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
+  integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
 
-for-each@^0.3.3:
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
-  integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+for-each@^0.3.3, for-each@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
+  integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
   dependencies:
-    is-callable "^1.1.3"
-
-for-in@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
-  integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==
-
-fragment-cache@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
-  integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==
-  dependencies:
-    map-cache "^0.2.2"
+    is-callable "^1.2.7"
 
 fresh@~0.5.2:
   version "0.5.2"
@@ -2149,65 +2188,74 @@
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
   integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
 
-function.prototype.name@^1.1.6:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd"
-  integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==
+function.prototype.name@^1.1.6, function.prototype.name@^1.1.8:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78"
+  integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==
   dependencies:
-    call-bind "^1.0.2"
-    define-properties "^1.2.0"
-    es-abstract "^1.22.1"
+    call-bind "^1.0.8"
+    call-bound "^1.0.3"
+    define-properties "^1.2.1"
     functions-have-names "^1.2.3"
+    hasown "^2.0.2"
+    is-callable "^1.2.7"
 
 functions-have-names@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
   integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
 
-get-caller-file@^2.0.1:
+get-caller-file@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
-  integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
+get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+  integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
   dependencies:
+    call-bind-apply-helpers "^1.0.2"
+    es-define-property "^1.0.1"
     es-errors "^1.3.0"
+    es-object-atoms "^1.1.1"
     function-bind "^1.1.2"
-    has-proto "^1.0.1"
-    has-symbols "^1.0.3"
-    hasown "^2.0.0"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
+    has-symbols "^1.1.0"
+    hasown "^2.0.2"
+    math-intrinsics "^1.1.0"
+
+get-proto@^1.0.0, get-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+  integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+  dependencies:
+    dunder-proto "^1.0.1"
+    es-object-atoms "^1.0.0"
 
 get-stream@^6.0.0:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
   integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
-get-symbol-description@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
-  integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==
+get-symbol-description@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee"
+  integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==
   dependencies:
-    call-bind "^1.0.5"
+    call-bound "^1.0.3"
     es-errors "^1.3.0"
-    get-intrinsic "^1.2.4"
+    get-intrinsic "^1.2.6"
 
-get-value@^2.0.3, get-value@^2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
-  integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==
-
-glob-parent@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
-  integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==
+get-tsconfig@^4.8.1:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e"
+  integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==
   dependencies:
-    is-glob "^3.1.0"
-    path-dirname "^1.0.0"
+    resolve-pkg-maps "^1.0.0"
 
-glob-parent@^5.1.2, glob-parent@~5.1.2:
+glob-parent@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -2221,11 +2269,6 @@
   dependencies:
     is-glob "^4.0.3"
 
-glob-to-regexp@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
-  integrity sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==
-
 glob@^7.1.3:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -2238,14 +2281,17 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-globals@^13.19.0:
-  version "13.24.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
-  integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
-  dependencies:
-    type-fest "^0.20.2"
+globals@^14.0.0:
+  version "14.0.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
 
-globalthis@^1.0.3:
+globals@^15.11.0:
+  version "15.15.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
+  integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
+
+globalthis@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
   integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
@@ -2253,31 +2299,22 @@
     define-properties "^1.2.1"
     gopd "^1.0.1"
 
-globby@^11.1.0:
-  version "11.1.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
-  integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
-  dependencies:
-    array-union "^2.1.0"
-    dir-glob "^3.0.1"
-    fast-glob "^3.2.9"
-    ignore "^5.2.0"
-    merge2 "^1.4.1"
-    slash "^3.0.0"
+globrex@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
+  integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
 
 google-protobuf@^3.6.1:
-  version "3.21.2"
-  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4"
-  integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==
+  version "3.21.4"
+  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.4.tgz#2f933e8b6e5e9f8edde66b7be0024b68f77da6c9"
+  integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==
 
-gopd@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
-  integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
-  dependencies:
-    get-intrinsic "^1.1.3"
+gopd@^1.0.1, gopd@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
 
-graceful-fs@^4.1.2:
+graceful-fs@^4.1.2, graceful-fs@^4.2.4:
   version "4.2.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
   integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@@ -2287,36 +2324,36 @@
   resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
   integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
 
-gts@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/gts/-/gts-3.1.1.tgz#c7347cf8f8ea32577909659b22bf698ac5ca8082"
-  integrity sha512-Jw44aBbzMnd1vtZs7tZt3LMstKQukCBg7N4CKVGzviIQ45Cz5b9lxDJGXVKj/9ySuGv6TYEeijZJGbiiVcM27w==
+gts@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/gts/-/gts-6.0.2.tgz#f7ff83fd786de92384740da4409f954261a0a62c"
+  integrity sha512-lp9+eDzzm6TYqiBpgGY00EInxBHFTJiU5brsVp11qXCJEw7Q6WNNngja0spZeqSFWSquaRuHQUuWxdZLaxnKmw==
   dependencies:
-    "@typescript-eslint/eslint-plugin" "^4.2.0"
-    "@typescript-eslint/parser" "^4.2.0"
-    chalk "^4.1.0"
-    eslint "^7.10.0"
-    eslint-config-prettier "^7.0.0"
-    eslint-plugin-node "^11.1.0"
-    eslint-plugin-prettier "^3.1.4"
+    "@typescript-eslint/eslint-plugin" "5.62.0"
+    "@typescript-eslint/parser" "5.62.0"
+    chalk "^4.1.2"
+    eslint "8.57.1"
+    eslint-config-prettier "9.1.0"
+    eslint-plugin-n "15.7.0"
+    eslint-plugin-prettier "5.2.1"
     execa "^5.0.0"
     inquirer "^7.3.3"
     json5 "^2.1.3"
     meow "^9.0.0"
     ncp "^2.0.0"
-    prettier "^2.1.2"
-    rimraf "^3.0.2"
-    write-file-atomic "^3.0.3"
+    prettier "3.3.3"
+    rimraf "3.0.2"
+    write-file-atomic "^4.0.0"
 
 hard-rejection@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
   integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
 
-has-bigints@^1.0.1, has-bigints@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
-  integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+has-bigints@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
+  integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==
 
 has-flag@^3.0.0:
   version "3.0.0"
@@ -2335,55 +2372,26 @@
   dependencies:
     es-define-property "^1.0.0"
 
-has-proto@^1.0.1, has-proto@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
-  integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
+has-proto@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5"
+  integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==
+  dependencies:
+    dunder-proto "^1.0.0"
 
-has-symbols@^1.0.2, has-symbols@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
-  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
 
-has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
+has-tostringtag@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
   integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
   dependencies:
     has-symbols "^1.0.3"
 
-has-value@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
-  integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==
-  dependencies:
-    get-value "^2.0.3"
-    has-values "^0.1.4"
-    isobject "^2.0.0"
-
-has-value@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
-  integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==
-  dependencies:
-    get-value "^2.0.6"
-    has-values "^1.0.0"
-    isobject "^3.0.0"
-
-has-values@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
-  integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==
-
-has-values@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
-  integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==
-  dependencies:
-    is-number "^3.0.0"
-    kind-of "^4.0.0"
-
-hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
+hasown@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
   integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
@@ -2402,15 +2410,15 @@
   dependencies:
     lru-cache "^6.0.0"
 
-htmlparser2@^8.0.1:
-  version "8.0.2"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
-  integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
+htmlparser2@^10.0.0:
+  version "10.0.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.0.0.tgz#77ad249037b66bf8cc99c6e286ef73b83aeb621d"
+  integrity sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==
   dependencies:
     domelementtype "^2.3.0"
     domhandler "^5.0.3"
-    domutils "^3.0.1"
-    entities "^4.4.0"
+    domutils "^3.2.1"
+    entities "^6.0.0"
 
 http-assert@^1.3.0:
   version "1.5.0"
@@ -2453,20 +2461,20 @@
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ignore@^5.1.1:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
-  integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
-
-ignore@^5.2.0:
+ignore@^5.1.1, ignore@^5.2.0, ignore@^5.3.2:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
   integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
 
+ignore@^7.0.0:
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
+  integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
+
 import-fresh@^3.2.1:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
-  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
+  integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
   dependencies:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
@@ -2518,178 +2526,155 @@
     strip-ansi "^6.0.0"
     through "^2.3.6"
 
-internal-slot@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
-  integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==
+internal-ip@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-6.2.0.tgz#d5541e79716e406b74ac6b07b856ef18dc1621c1"
+  integrity sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==
+  dependencies:
+    default-gateway "^6.0.0"
+    ipaddr.js "^1.9.1"
+    is-ip "^3.1.0"
+    p-event "^4.2.0"
+
+internal-slot@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
+  integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==
   dependencies:
     es-errors "^1.3.0"
-    hasown "^2.0.0"
-    side-channel "^1.0.4"
+    hasown "^2.0.2"
+    side-channel "^1.1.0"
 
-ip@^1.1.5:
-  version "1.1.8"
-  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
-  integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==
+ip-regex@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
 
-is-accessor-descriptor@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz#3223b10628354644b86260db29b3e693f5ceedd4"
-  integrity sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==
+ipaddr.js@^1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280"
+  integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==
   dependencies:
-    hasown "^2.0.0"
-
-is-array-buffer@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
-  integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==
-  dependencies:
-    call-bind "^1.0.2"
-    get-intrinsic "^1.2.1"
+    call-bind "^1.0.8"
+    call-bound "^1.0.3"
+    get-intrinsic "^1.2.6"
 
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
   integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
 
-is-bigint@^1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
-  integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+is-async-function@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523"
+  integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==
   dependencies:
-    has-bigints "^1.0.1"
+    async-function "^1.0.0"
+    call-bound "^1.0.3"
+    get-proto "^1.0.1"
+    has-tostringtag "^1.0.2"
+    safe-regex-test "^1.1.0"
 
-is-binary-path@~2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
-  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+is-bigint@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672"
+  integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==
   dependencies:
-    binary-extensions "^2.0.0"
+    has-bigints "^1.0.2"
 
-is-boolean-object@^1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
-  integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+is-boolean-object@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e"
+  integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==
   dependencies:
-    call-bind "^1.0.2"
-    has-tostringtag "^1.0.0"
+    call-bound "^1.0.3"
+    has-tostringtag "^1.0.2"
 
-is-buffer@^1.1.5:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
-  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-
-is-builtin-module@^3.1.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
-  integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==
-  dependencies:
-    builtin-modules "^3.3.0"
-
-is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
+is-callable@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
   integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
 
-is-core-module@^2.13.0, is-core-module@^2.15.1:
-  version "2.15.1"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
-  integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
+is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1, is-core-module@^2.5.0:
+  version "2.16.1"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
+  integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
   dependencies:
     hasown "^2.0.2"
 
-is-core-module@^2.5.0:
-  version "2.13.1"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
-  integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
+is-data-view@^1.0.1, is-data-view@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e"
+  integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==
   dependencies:
-    hasown "^2.0.0"
-
-is-data-descriptor@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz#2109164426166d32ea38c405c1e0945d9e6a4eeb"
-  integrity sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==
-  dependencies:
-    hasown "^2.0.0"
-
-is-data-view@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f"
-  integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==
-  dependencies:
+    call-bound "^1.0.2"
+    get-intrinsic "^1.2.6"
     is-typed-array "^1.1.13"
 
-is-date-object@^1.0.1:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
-  integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+is-date-object@^1.0.5, is-date-object@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7"
+  integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==
   dependencies:
-    has-tostringtag "^1.0.0"
-
-is-descriptor@^0.1.0:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.7.tgz#2727eb61fd789dcd5bdf0ed4569f551d2fe3be33"
-  integrity sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==
-  dependencies:
-    is-accessor-descriptor "^1.0.1"
-    is-data-descriptor "^1.0.1"
-
-is-descriptor@^1.0.0, is-descriptor@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.3.tgz#92d27cb3cd311c4977a4db47df457234a13cb306"
-  integrity sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==
-  dependencies:
-    is-accessor-descriptor "^1.0.1"
-    is-data-descriptor "^1.0.1"
+    call-bound "^1.0.2"
+    has-tostringtag "^1.0.2"
 
 is-docker@^2.0.0, is-docker@^2.1.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
   integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
 
-is-extendable@^0.1.0, is-extendable@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
-  integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
-
-is-extendable@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
-  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
-  dependencies:
-    is-plain-object "^2.0.4"
-
-is-extglob@^2.1.0, is-extglob@^2.1.1:
+is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
   integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
 
+is-finalizationregistry@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90"
+  integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==
+  dependencies:
+    call-bound "^1.0.3"
+
 is-fullwidth-code-point@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
-is-generator-function@^1.0.7:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
-  integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+is-generator-function@^1.0.10, is-generator-function@^1.0.7:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca"
+  integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==
   dependencies:
-    has-tostringtag "^1.0.0"
+    call-bound "^1.0.3"
+    get-proto "^1.0.0"
+    has-tostringtag "^1.0.2"
+    safe-regex-test "^1.1.0"
 
-is-glob@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
-  integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==
-  dependencies:
-    is-extglob "^2.1.0"
-
-is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
   integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
   dependencies:
     is-extglob "^2.1.1"
 
+is-ip@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
+  integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
+  dependencies:
+    ip-regex "^4.0.0"
+
+is-map@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
+  integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
+
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@@ -2700,99 +2685,94 @@
   resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747"
   integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==
 
-is-number-object@^1.0.4:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
-  integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==
+is-number-object@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541"
+  integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==
   dependencies:
-    has-tostringtag "^1.0.0"
-
-is-number@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
-  integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==
-  dependencies:
-    kind-of "^3.0.2"
+    call-bound "^1.0.3"
+    has-tostringtag "^1.0.2"
 
 is-number@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-path-inside@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
-  integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
-
 is-plain-obj@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==
 
-is-plain-object@^2.0.3, is-plain-object@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
-  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+is-regex@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
+  integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==
   dependencies:
-    isobject "^3.0.1"
+    call-bound "^1.0.2"
+    gopd "^1.2.0"
+    has-tostringtag "^1.0.2"
+    hasown "^2.0.2"
 
-is-regex@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
-  integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
-  dependencies:
-    call-bind "^1.0.2"
-    has-tostringtag "^1.0.0"
+is-set@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d"
+  integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==
 
-is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688"
-  integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==
+is-shared-array-buffer@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f"
+  integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==
   dependencies:
-    call-bind "^1.0.7"
+    call-bound "^1.0.3"
 
 is-stream@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
   integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
-is-string@^1.0.5, is-string@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
-  integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+is-string@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9"
+  integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==
   dependencies:
-    has-tostringtag "^1.0.0"
+    call-bound "^1.0.3"
+    has-tostringtag "^1.0.2"
 
-is-symbol@^1.0.2, is-symbol@^1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
-  integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+is-symbol@^1.0.4, is-symbol@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634"
+  integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==
   dependencies:
-    has-symbols "^1.0.2"
+    call-bound "^1.0.2"
+    has-symbols "^1.1.0"
+    safe-regex-test "^1.1.0"
 
-is-typed-array@^1.1.13:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229"
-  integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==
+is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15:
+  version "1.1.15"
+  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b"
+  integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==
   dependencies:
-    which-typed-array "^1.1.14"
+    which-typed-array "^1.1.16"
 
-is-typedarray@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
-  integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
+is-weakmap@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd"
+  integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==
 
-is-weakref@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
-  integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+is-weakref@^1.0.2, is-weakref@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293"
+  integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==
   dependencies:
-    call-bind "^1.0.2"
+    call-bound "^1.0.3"
 
-is-windows@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
-  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+is-weakset@^2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca"
+  integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==
+  dependencies:
+    call-bound "^1.0.3"
+    get-intrinsic "^1.2.6"
 
 is-wsl@^2.2.0:
   version "2.2.0"
@@ -2801,38 +2781,21 @@
   dependencies:
     is-docker "^2.0.0"
 
-isarray@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-  integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
-
 isarray@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
   integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
 
 isbinaryfile@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.0.tgz#034b7e54989dab8986598cbcea41f66663c65234"
-  integrity sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.5.tgz#5a29c56b97ef337394c3ec4cc304066042ae81e8"
+  integrity sha512-vh9MWXjhhblwrHlt/yutrubDuBD01kKFscyVndE2/VEeEU5aAizrzuWAsEaCjo997k+IluhN6C4jgxfS69SCIw==
 
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 
-isobject@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
-  integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==
-  dependencies:
-    isarray "1.0.0"
-
-isobject@^3.0.0, isobject@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
-  integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
-
 js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -2845,10 +2808,10 @@
   dependencies:
     argparse "^2.0.1"
 
-jsdoc-type-pratt-parser@~4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114"
-  integrity sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==
+jsdoc-type-pratt-parser@~4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113"
+  integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==
 
 json-buffer@3.0.1:
   version "3.0.1"
@@ -2894,28 +2857,14 @@
   dependencies:
     tsscmp "1.0.6"
 
-keyv@^4.5.3:
+keyv@^4.5.4:
   version "4.5.4"
   resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
   integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
   dependencies:
     json-buffer "3.0.1"
 
-kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
-  integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==
-  dependencies:
-    is-buffer "^1.1.5"
-
-kind-of@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
-  integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==
-  dependencies:
-    is-buffer "^1.1.5"
-
-kind-of@^6.0.2, kind-of@^6.0.3:
+kind-of@^6.0.3:
   version "6.0.3"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
@@ -2958,9 +2907,9 @@
     koa-send "^5.0.0"
 
 koa@^2.13.0:
-  version "2.15.0"
-  resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.0.tgz#d24ae1b0ff378bf12eb3df584ab4204e4c12ac2b"
-  integrity sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==
+  version "2.16.2"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.16.2.tgz#1292a3b415a9b9f3dd6872c1835229630a4ecae3"
+  integrity sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==
   dependencies:
     accepts "^1.3.5"
     cache-content-type "^1.0.0"
@@ -3004,19 +2953,20 @@
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
   integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
 
-lit-analyzer@1.2.1, lit-analyzer@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/lit-analyzer/-/lit-analyzer-1.2.1.tgz#725331a4019ae870dd631d4dd709d39a237161ea"
-  integrity sha512-OEARBhDidyaQENavLbzpTKbEmu5rnAI+SdYsH4ia1BlGlLiqQXoym7uH1MaRPtwtUPbkhUfT4OBDZ+74VHc3Cg==
+lit-analyzer@^2.0.1, lit-analyzer@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/lit-analyzer/-/lit-analyzer-2.0.3.tgz#15560ccfb0e1b66fc5521330fc73502b1678621e"
+  integrity sha512-XiAjnwVipNrKav7r3CSEZpWt+mwYxrhPRVC7h8knDmn/HWTzzWJvPe+mwBcL2brn4xhItAMzZhFC8tzzqHKmiQ==
   dependencies:
+    "@vscode/web-custom-data" "^0.4.2"
     chalk "^2.4.2"
     didyoumean2 "4.1.0"
-    fast-glob "^2.2.6"
+    fast-glob "^3.2.11"
     parse5 "5.1.0"
-    ts-simple-type "~1.0.5"
+    ts-simple-type "~2.0.0-next.0"
     vscode-css-languageservice "4.3.0"
     vscode-html-languageservice "3.1.0"
-    web-component-analyzer "~1.1.1"
+    web-component-analyzer "^2.0.0"
 
 load-json-file@^4.0.0:
   version "4.0.0"
@@ -3042,11 +2992,6 @@
   dependencies:
     p-locate "^5.0.0"
 
-lodash.assignwith@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb"
-  integrity sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==
-
 lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -3062,7 +3007,7 @@
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash@^4.17.14, lodash@^4.17.19:
+lodash@^4.17.19:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -3079,10 +3024,10 @@
   dependencies:
     yallist "^4.0.0"
 
-map-cache@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
-  integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==
+lru-cache@^8.0.4:
+  version "8.0.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
+  integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
 
 map-obj@^1.0.0:
   version "1.0.1"
@@ -3094,12 +3039,10 @@
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
   integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
 
-map-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
-  integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==
-  dependencies:
-    object-visit "^1.0.0"
+math-intrinsics@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
 
 media-typer@0.3.0:
   version "0.3.0"
@@ -3134,36 +3077,17 @@
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
-merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1:
+merge2@^1.3.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-micromatch@^3.1.10:
-  version "3.1.10"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
-  integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+micromatch@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+  integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
   dependencies:
-    arr-diff "^4.0.0"
-    array-unique "^0.3.2"
-    braces "^2.3.1"
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    extglob "^2.0.4"
-    fragment-cache "^0.2.1"
-    kind-of "^6.0.2"
-    nanomatch "^1.2.9"
-    object.pick "^1.3.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.2"
-
-micromatch@^4.0.4:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
-  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
-  dependencies:
-    braces "^3.0.2"
+    braces "^3.0.3"
     picomatch "^2.3.1"
 
 mime-db@1.52.0:
@@ -3188,13 +3112,20 @@
   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
-minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
+minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
   dependencies:
     brace-expansion "^1.1.7"
 
+minimatch@^9.0.4:
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimist-options@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
@@ -3209,31 +3140,6 @@
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
   integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
 
-mixin-deep@^1.2.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
-  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
-  dependencies:
-    for-in "^1.0.2"
-    is-extendable "^1.0.1"
-
-mkdirp@^0.5.6:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
-  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
-  dependencies:
-    minimist "^1.2.6"
-
-ms@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-  integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
-
-ms@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
-  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
 ms@^2.1.1, ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -3249,28 +3155,6 @@
   resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.2.13.tgz#dfd1ed0bfab05e9fe540eb6874525f0a1684099b"
   integrity sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==
 
-nanomatch@^1.2.9:
-  version "1.2.13"
-  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
-  integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
-  dependencies:
-    arr-diff "^4.0.0"
-    array-unique "^0.3.2"
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    fragment-cache "^0.2.1"
-    is-windows "^1.0.2"
-    kind-of "^6.0.2"
-    object.pick "^1.3.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-natural-compare-lite@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
-  integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
-
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -3311,11 +3195,6 @@
     semver "^7.3.4"
     validate-npm-package-license "^3.0.1"
 
-normalize-path@^3.0.0, normalize-path@~3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
-  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
 npm-run-all@^4.1.5:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
@@ -3338,40 +3217,26 @@
   dependencies:
     path-key "^3.0.0"
 
-object-copy@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
-  integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==
-  dependencies:
-    copy-descriptor "^0.1.0"
-    define-property "^0.2.5"
-    kind-of "^3.0.3"
-
-object-inspect@^1.13.1:
-  version "1.13.2"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
-  integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
+object-inspect@^1.13.3, object-inspect@^1.13.4:
+  version "1.13.4"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
+  integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
 
 object-keys@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object-visit@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
-  integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==
+object.assign@^4.1.7:
+  version "4.1.7"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d"
+  integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==
   dependencies:
-    isobject "^3.0.0"
-
-object.assign@^4.1.5:
-  version "4.1.5"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
-  integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==
-  dependencies:
-    call-bind "^1.0.5"
+    call-bind "^1.0.8"
+    call-bound "^1.0.3"
     define-properties "^1.2.1"
-    has-symbols "^1.0.3"
+    es-object-atoms "^1.0.0"
+    has-symbols "^1.1.0"
     object-keys "^1.1.1"
 
 object.fromentries@^2.0.8:
@@ -3393,19 +3258,13 @@
     define-properties "^1.2.1"
     es-abstract "^1.23.2"
 
-object.pick@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
-  integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==
+object.values@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216"
+  integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==
   dependencies:
-    isobject "^3.0.1"
-
-object.values@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
-  integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==
-  dependencies:
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
+    call-bound "^1.0.3"
     define-properties "^1.2.1"
     es-object-atoms "^1.0.0"
 
@@ -3461,6 +3320,27 @@
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
 
+own-keys@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358"
+  integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==
+  dependencies:
+    get-intrinsic "^1.2.6"
+    object-keys "^1.1.1"
+    safe-push-apply "^1.0.0"
+
+p-event@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
+  integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
+  dependencies:
+    p-timeout "^3.1.0"
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
 p-limit@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
@@ -3489,6 +3369,13 @@
   dependencies:
     p-limit "^3.0.2"
 
+p-timeout@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+  integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+  dependencies:
+    p-finally "^1.0.0"
+
 p-try@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
@@ -3501,6 +3388,13 @@
   dependencies:
     callsites "^3.0.0"
 
+parse-imports-exports@^0.2.4:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz#e3fb3b5e264cfb55c25b5dfcbe7f410f8dc4e7af"
+  integrity sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==
+  dependencies:
+    parse-statements "1.0.11"
+
 parse-json@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -3519,6 +3413,11 @@
     json-parse-even-better-errors "^2.3.0"
     lines-and-columns "^1.1.6"
 
+parse-statements@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/parse-statements/-/parse-statements-1.0.11.tgz#8787c5d383ae5746568571614be72b0689584344"
+  integrity sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==
+
 parse5-htmlparser2-tree-adapter@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
@@ -3541,16 +3440,6 @@
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
   integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
 
-pascalcase@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-  integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==
-
-path-dirname@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
-  integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==
-
 path-exists@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -3583,16 +3472,21 @@
   dependencies:
     pify "^3.0.0"
 
-path-type@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
-  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+picocolors@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
 
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
+picomatch@^2.2.2, picomatch@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 
+picomatch@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
+  integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+
 pidtree@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
@@ -3604,23 +3498,17 @@
   integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
 
 portfinder@^1.0.32:
-  version "1.0.32"
-  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
-  integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
+  version "1.0.37"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.37.tgz#92b754ef89a11801c8efe4b0e5cd845b0064c212"
+  integrity sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==
   dependencies:
-    async "^2.6.4"
-    debug "^3.2.7"
-    mkdirp "^0.5.6"
-
-posix-character-classes@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
-  integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==
+    async "^3.2.6"
+    debug "^4.3.6"
 
 possible-typed-array-names@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
-  integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
+  integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
 
 prelude-ls@^1.2.1:
   version "1.2.1"
@@ -3634,7 +3522,12 @@
   dependencies:
     fast-diff "^1.1.2"
 
-prettier@^2.1.2, prettier@^2.8.8:
+prettier@3.3.3:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
+  integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
+
+prettier@^2.8.8:
   version "2.8.8"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
   integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
@@ -3658,7 +3551,7 @@
     "@types/node" "^10.1.0"
     long "^4.0.0"
 
-punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.1.0, punycode@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
   integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@@ -3701,12 +3594,10 @@
     parse-json "^5.0.0"
     type-fest "^0.6.0"
 
-readdirp@~3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
-  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
-  dependencies:
-    picomatch "^2.2.1"
+readdirp@^4.0.1:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
+  integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
 
 redent@^3.0.0:
   version "3.0.0"
@@ -3716,27 +3607,30 @@
     indent-string "^4.0.0"
     strip-indent "^3.0.0"
 
-regenerator-runtime@^0.14.0:
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
-  integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
-
-regex-not@^1.0.0, regex-not@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
-  integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"
+  integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==
   dependencies:
-    extend-shallow "^3.0.2"
-    safe-regex "^1.1.0"
+    call-bind "^1.0.8"
+    define-properties "^1.2.1"
+    es-abstract "^1.23.9"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.0.0"
+    get-intrinsic "^1.2.7"
+    get-proto "^1.0.1"
+    which-builtin-type "^1.2.1"
 
-regexp.prototype.flags@^1.5.2:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42"
-  integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==
+regexp.prototype.flags@^1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19"
+  integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==
   dependencies:
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
     define-properties "^1.2.1"
     es-errors "^1.3.0"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
     set-function-name "^2.0.2"
 
 regexpp@^3.0.0:
@@ -3744,26 +3638,11 @@
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
   integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
-repeat-element@^1.1.2:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
-  integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
-
-repeat-string@^1.6.1:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
-  integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
   integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
 
-require-main-filename@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
-  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-
 requireindex@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
@@ -3782,17 +3661,17 @@
     http-errors "~1.6.2"
     path-is-absolute "1.0.1"
 
-resolve-url@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
-  integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
+resolve-pkg-maps@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
+  integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
 
-resolve@^1.10.0, resolve@^1.10.1, resolve@^1.19.0, resolve@^1.22.4:
-  version "1.22.8"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
-  integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
+resolve@^1.10.0, resolve@^1.22.1, resolve@^1.22.4:
+  version "1.22.10"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
+  integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
   dependencies:
-    is-core-module "^2.13.0"
+    is-core-module "^2.16.0"
     path-parse "^1.0.7"
     supports-preserve-symlinks-flag "^1.0.0"
 
@@ -3804,35 +3683,45 @@
     onetime "^5.1.0"
     signal-exit "^3.0.2"
 
-ret@~0.1.10:
-  version "0.1.15"
-  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
-  integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
-
 reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+  integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
 
-rimraf@^3.0.2:
+rimraf@3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
   dependencies:
     glob "^7.1.3"
 
-rollup@^2.67.0:
-  version "2.79.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
-  integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
+rollup@^4.4.0, rollup@^4.49.0:
+  version "4.49.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.49.0.tgz#9751ad9d06a47a4496c3c5c238b27b1422c8b0eb"
+  integrity sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==
+  dependencies:
+    "@types/estree" "1.0.8"
   optionalDependencies:
-    fsevents "~2.3.2"
-
-rollup@^2.79.2:
-  version "2.79.2"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090"
-  integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
-  optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.49.0"
+    "@rollup/rollup-android-arm64" "4.49.0"
+    "@rollup/rollup-darwin-arm64" "4.49.0"
+    "@rollup/rollup-darwin-x64" "4.49.0"
+    "@rollup/rollup-freebsd-arm64" "4.49.0"
+    "@rollup/rollup-freebsd-x64" "4.49.0"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.49.0"
+    "@rollup/rollup-linux-arm-musleabihf" "4.49.0"
+    "@rollup/rollup-linux-arm64-gnu" "4.49.0"
+    "@rollup/rollup-linux-arm64-musl" "4.49.0"
+    "@rollup/rollup-linux-loongarch64-gnu" "4.49.0"
+    "@rollup/rollup-linux-ppc64-gnu" "4.49.0"
+    "@rollup/rollup-linux-riscv64-gnu" "4.49.0"
+    "@rollup/rollup-linux-riscv64-musl" "4.49.0"
+    "@rollup/rollup-linux-s390x-gnu" "4.49.0"
+    "@rollup/rollup-linux-x64-gnu" "4.49.0"
+    "@rollup/rollup-linux-x64-musl" "4.49.0"
+    "@rollup/rollup-win32-arm64-msvc" "4.49.0"
+    "@rollup/rollup-win32-ia32-msvc" "4.49.0"
+    "@rollup/rollup-win32-x64-msvc" "4.49.0"
     fsevents "~2.3.2"
 
 run-async@^2.4.0:
@@ -3854,14 +3743,15 @@
   dependencies:
     tslib "^1.9.0"
 
-safe-array-concat@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb"
-  integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==
+safe-array-concat@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3"
+  integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==
   dependencies:
-    call-bind "^1.0.7"
-    get-intrinsic "^1.2.4"
-    has-symbols "^1.0.3"
+    call-bind "^1.0.8"
+    call-bound "^1.0.2"
+    get-intrinsic "^1.2.6"
+    has-symbols "^1.1.0"
     isarray "^2.0.5"
 
 safe-buffer@5.2.1:
@@ -3869,21 +3759,22 @@
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-safe-regex-test@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
-  integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==
+safe-push-apply@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5"
+  integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==
   dependencies:
-    call-bind "^1.0.6"
     es-errors "^1.3.0"
-    is-regex "^1.1.4"
+    isarray "^2.0.5"
 
-safe-regex@^1.1.0:
+safe-regex-test@^1.1.0:
   version "1.1.0"
-  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
-  integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==
+  resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1"
+  integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==
   dependencies:
-    ret "~0.1.10"
+    call-bound "^1.0.2"
+    es-errors "^1.3.0"
+    is-regex "^1.2.1"
 
 "safer-buffer@>= 2.1.2 < 3":
   version "2.1.2"
@@ -3900,24 +3791,17 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
   integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
 
-semver@^6.1.0, semver@^6.3.1:
+semver@^6.3.1:
   version "6.3.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
 
-semver@^7.3.4, semver@^7.3.7, semver@^7.5.1:
-  version "7.5.4"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
-  integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
-  dependencies:
-    lru-cache "^6.0.0"
+semver@^7.0.0, semver@^7.3.4, semver@^7.3.8, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.2:
+  version "7.7.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+  integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
 
-set-blocking@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
-  integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
-
-set-function-length@^1.2.1:
+set-function-length@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
   integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
@@ -3939,15 +3823,14 @@
     functions-have-names "^1.2.3"
     has-property-descriptors "^1.0.2"
 
-set-value@^2.0.0, set-value@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
-  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+set-proto@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e"
+  integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==
   dependencies:
-    extend-shallow "^2.0.1"
-    is-extendable "^0.1.1"
-    is-plain-object "^2.0.3"
-    split-string "^3.0.1"
+    dunder-proto "^1.0.1"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.0.0"
 
 setprototypeof@1.1.0:
   version "1.1.0"
@@ -3984,71 +3867,55 @@
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
 shell-quote@^1.6.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
-  integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
+  integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
 
-side-channel@^1.0.4:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
-  integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
+side-channel-list@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
+  integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
   dependencies:
-    call-bind "^1.0.7"
     es-errors "^1.3.0"
-    get-intrinsic "^1.2.4"
-    object-inspect "^1.13.1"
+    object-inspect "^1.13.3"
 
-signal-exit@^3.0.2, signal-exit@^3.0.3:
+side-channel-map@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
+  integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+  dependencies:
+    call-bound "^1.0.2"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.5"
+    object-inspect "^1.13.3"
+
+side-channel-weakmap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
+  integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
+  dependencies:
+    call-bound "^1.0.2"
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.5"
+    object-inspect "^1.13.3"
+    side-channel-map "^1.0.1"
+
+side-channel@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
+  integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
+  dependencies:
+    es-errors "^1.3.0"
+    object-inspect "^1.13.3"
+    side-channel-list "^1.0.0"
+    side-channel-map "^1.0.1"
+    side-channel-weakmap "^1.0.2"
+
+signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
   version "3.0.7"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
   integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
 
-slash@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
-  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-
-snapdragon-node@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
-  integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
-  dependencies:
-    define-property "^1.0.0"
-    isobject "^3.0.0"
-    snapdragon-util "^3.0.1"
-
-snapdragon-util@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
-  integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
-  dependencies:
-    kind-of "^3.2.0"
-
-snapdragon@^0.8.1:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
-  integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
-  dependencies:
-    base "^0.11.1"
-    debug "^2.2.0"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    map-cache "^0.2.2"
-    source-map "^0.5.6"
-    source-map-resolve "^0.5.0"
-    use "^3.1.0"
-
-source-map-resolve@^0.5.0:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
-  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
-  dependencies:
-    atob "^2.1.2"
-    decode-uri-component "^0.2.0"
-    resolve-url "^0.2.1"
-    source-map-url "^0.4.0"
-    urix "^0.1.0"
-
 source-map-support@0.5.9:
   version "0.5.9"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
@@ -4065,26 +3932,11 @@
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
-source-map-url@^0.4.0:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
-  integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
-
-source-map@^0.5.6:
-  version "0.5.7"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-  integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
-
 source-map@^0.6.0:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
-source-map@~0.7.2:
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
-  integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
-
 spdx-correct@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
@@ -4094,11 +3946,11 @@
     spdx-license-ids "^3.0.0"
 
 spdx-exceptions@^2.1.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
-  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66"
+  integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==
 
-spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1:
+spdx-expression-parse@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
   integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
@@ -4106,37 +3958,33 @@
     spdx-exceptions "^2.1.0"
     spdx-license-ids "^3.0.0"
 
+spdx-expression-parse@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794"
+  integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
 spdx-license-ids@^3.0.0:
-  version "3.0.16"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
-  integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
-
-split-string@^3.0.1, split-string@^3.0.2:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
-  integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
-  dependencies:
-    extend-shallow "^3.0.0"
-
-static-extend@^0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
-  integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==
-  dependencies:
-    define-property "^0.2.5"
-    object-copy "^0.1.0"
+  version "3.0.22"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz#abf5a08a6f5d7279559b669f47f0a43e8f3464ef"
+  integrity sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==
 
 "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
 
-stream-read-all@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/stream-read-all/-/stream-read-all-3.0.1.tgz#60762ae45e61d93ba0978cda7f3913790052ad96"
-  integrity sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==
+stop-iteration-iterator@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad"
+  integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==
+  dependencies:
+    es-errors "^1.3.0"
+    internal-slot "^1.1.0"
 
-string-width@^4.1.0, string-width@^4.2.0:
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -4146,30 +3994,35 @@
     strip-ansi "^6.0.1"
 
 string.prototype.padend@^3.0.0:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz#311ef3a4e3c557dd999cdf88fbdde223f2ac0f95"
-  integrity sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==
-  dependencies:
-    call-bind "^1.0.2"
-    define-properties "^1.2.0"
-    es-abstract "^1.22.1"
-
-string.prototype.trim@^1.2.9:
-  version "1.2.9"
-  resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4"
-  integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz#ba79cf8992609a91c872daa47c6bb144ee7f62a5"
+  integrity sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==
   dependencies:
     call-bind "^1.0.7"
     define-properties "^1.2.1"
-    es-abstract "^1.23.0"
+    es-abstract "^1.23.2"
     es-object-atoms "^1.0.0"
 
-string.prototype.trimend@^1.0.8:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229"
-  integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==
+string.prototype.trim@^1.2.10:
+  version "1.2.10"
+  resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81"
+  integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==
   dependencies:
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
+    call-bound "^1.0.2"
+    define-data-property "^1.1.4"
+    define-properties "^1.2.1"
+    es-abstract "^1.23.5"
+    es-object-atoms "^1.0.0"
+    has-property-descriptors "^1.0.2"
+
+string.prototype.trimend@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942"
+  integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==
+  dependencies:
+    call-bind "^1.0.8"
+    call-bound "^1.0.2"
     define-properties "^1.2.1"
     es-object-atoms "^1.0.0"
 
@@ -4230,32 +4083,43 @@
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
-table-layout@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-3.0.2.tgz#69c2be44388a5139b48c59cf21e73b488021769a"
-  integrity sha512-rpyNZYRw+/C+dYkcQ3Pr+rLxW4CfHpXjPDnG7lYhdRoUcZTUt+KEsX+94RGp/aVp/MQU35JCITv2T/beY4m+hw==
+synckit@^0.11.7:
+  version "0.11.11"
+  resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0"
+  integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==
   dependencies:
-    "@75lb/deep-merge" "^1.1.1"
+    "@pkgr/core" "^0.2.9"
+
+synckit@^0.9.1:
+  version "0.9.3"
+  resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.3.tgz#1cfd60d9e61f931e07fb7f56f474b5eb31b826a7"
+  integrity sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==
+  dependencies:
+    "@pkgr/core" "^0.1.0"
+    tslib "^2.6.2"
+
+table-layout@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-4.1.1.tgz#0f72965de1a5c0c1419c9ba21cae4e73a2f73a42"
+  integrity sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==
+  dependencies:
     array-back "^6.2.2"
-    command-line-args "^5.2.1"
-    command-line-usage "^7.0.0"
-    stream-read-all "^3.0.1"
-    typical "^7.1.1"
     wordwrapjs "^5.1.0"
 
-terser@~5.8.0:
-  version "5.8.0"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.8.0.tgz#c6d352f91aed85cc6171ccb5e84655b77521d947"
-  integrity sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==
-  dependencies:
-    commander "^2.20.0"
-    source-map "~0.7.2"
-    source-map-support "~0.5.20"
+tapable@^2.2.0:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.3.tgz#4b67b635b2d97578a06a2713d2f04800c237e99b"
+  integrity sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==
 
-text-table@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
-  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+terser@~5.39.0:
+  version "5.39.2"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.2.tgz#5a1626030724a672e2e5b5c9cd9070308c20e8f9"
+  integrity sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.3"
+    acorn "^8.14.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
 
 through@^2.3.6:
   version "2.3.8"
@@ -4269,21 +4133,6 @@
   dependencies:
     os-tmpdir "~1.0.2"
 
-to-object-path@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
-  integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==
-  dependencies:
-    kind-of "^3.0.2"
-
-to-regex-range@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
-  integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==
-  dependencies:
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-
 to-regex-range@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -4291,44 +4140,47 @@
   dependencies:
     is-number "^7.0.0"
 
-to-regex@^3.0.1, to-regex@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
-  integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
-  dependencies:
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    regex-not "^1.0.2"
-    safe-regex "^1.1.0"
-
 toidentifier@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
-tr46@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
-  integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
+tr46@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca"
+  integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
   dependencies:
-    punycode "^2.1.1"
+    punycode "^2.3.1"
 
 trim-newlines@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
   integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
-ts-lit-plugin@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ts-lit-plugin/-/ts-lit-plugin-1.2.1.tgz#7fca17a454645c14911917fa7f17ade582fa3056"
-  integrity sha512-k/Me+aT1N9ckC/KuJCAlAJgCHFezOxuOGOzBE0q42xnKbJnUMNl08WqWF6C7OKecCPHIMRk5Wj5o6MDsmt9+qA==
-  dependencies:
-    lit-analyzer "1.2.1"
+ts-api-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
+  integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
 
-ts-simple-type@~1.0.5:
+ts-declaration-location@^1.0.6:
   version "1.0.7"
-  resolved "https://registry.yarnpkg.com/ts-simple-type/-/ts-simple-type-1.0.7.tgz#03930af557528dd40eaa121913c7035a0baaacf8"
-  integrity sha512-zKmsCQs4dZaeSKjEA7pLFDv7FHHqAFLPd0Mr//OIJvu8M+4p4bgSFJwZSEBEg3ec9W7RzRz1vi8giiX0+mheBQ==
+  resolved "https://registry.yarnpkg.com/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz#d4068fe9975828b3b453b3ab112b4711d8267688"
+  integrity sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==
+  dependencies:
+    picomatch "^4.0.2"
+
+ts-lit-plugin@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/ts-lit-plugin/-/ts-lit-plugin-2.0.2.tgz#dbf28cf9c81cad626a7b1d9d880db6df4ce66259"
+  integrity sha512-DPXlVxhjWHxg8AyBLcfSYt2JXgpANV1ssxxwjY98o26gD8MzeiM68HFW9c2VeDd1CjoR3w7B/6/uKxwBQe+ioA==
+  dependencies:
+    lit-analyzer "^2.0.1"
+    web-component-analyzer "^2.0.0"
+
+ts-simple-type@2.0.0-next.0, ts-simple-type@~2.0.0-next.0:
+  version "2.0.0-next.0"
+  resolved "https://registry.yarnpkg.com/ts-simple-type/-/ts-simple-type-2.0.0-next.0.tgz#262a321ba2a17aa57f88ef31e30453beff5be246"
+  integrity sha512-A+hLX83gS+yH6DtzNAhzZbPfU+D9D8lHlTSd7GeoMRBjOt3GRylDqLTYbdmjA4biWvq2xSfpqfIDj2l0OA/BVg==
 
 tsconfig-paths@^3.15.0:
   version "3.15.0"
@@ -4345,12 +4197,17 @@
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
+tslib@^2.6.2:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
 tsscmp@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
   integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
 
-tsutils@3.21.0, tsutils@^3.21.0:
+tsutils@3.21.0:
   version "3.21.0"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
   integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
@@ -4369,11 +4226,6 @@
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
   integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
 
-type-fest@^0.20.2:
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
-  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
-
 type-fest@^0.21.3:
   version "0.21.3"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
@@ -4397,66 +4249,55 @@
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
-typed-array-buffer@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3"
-  integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==
+typed-array-buffer@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536"
+  integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==
   dependencies:
-    call-bind "^1.0.7"
+    call-bound "^1.0.3"
     es-errors "^1.3.0"
-    is-typed-array "^1.1.13"
+    is-typed-array "^1.1.14"
 
-typed-array-byte-length@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67"
-  integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==
+typed-array-byte-length@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce"
+  integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==
   dependencies:
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
     for-each "^0.3.3"
-    gopd "^1.0.1"
-    has-proto "^1.0.3"
-    is-typed-array "^1.1.13"
+    gopd "^1.2.0"
+    has-proto "^1.2.0"
+    is-typed-array "^1.1.14"
 
-typed-array-byte-offset@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063"
-  integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==
+typed-array-byte-offset@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355"
+  integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==
   dependencies:
     available-typed-arrays "^1.0.7"
-    call-bind "^1.0.7"
+    call-bind "^1.0.8"
     for-each "^0.3.3"
-    gopd "^1.0.1"
-    has-proto "^1.0.3"
-    is-typed-array "^1.1.13"
+    gopd "^1.2.0"
+    has-proto "^1.2.0"
+    is-typed-array "^1.1.15"
+    reflect.getprototypeof "^1.0.9"
 
-typed-array-length@^1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3"
-  integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==
+typed-array-length@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d"
+  integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==
   dependencies:
     call-bind "^1.0.7"
     for-each "^0.3.3"
     gopd "^1.0.1"
-    has-proto "^1.0.3"
     is-typed-array "^1.1.13"
     possible-typed-array-names "^1.0.0"
+    reflect.getprototypeof "^1.0.6"
 
-typedarray-to-buffer@^3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
-  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
-  dependencies:
-    is-typedarray "^1.0.0"
-
-typescript@^3.8.3:
-  version "3.9.10"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
-  integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
-
-typescript@^4.9.5:
-  version "4.9.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
-  integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
+typescript@5.8.2, typescript@~5.2.0:
+  version "5.8.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
+  integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
 
 typical@^4.0.0:
   version "4.0.0"
@@ -4464,47 +4305,29 @@
   integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
 
 typical@^7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/typical/-/typical-7.1.1.tgz#ba177ab7ab103b78534463ffa4c0c9754523ac1f"
-  integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==
+  version "7.3.0"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-7.3.0.tgz#930376be344228709f134613911fa22aa09617a4"
+  integrity sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==
 
 ua-parser-js@^1.0.33:
-  version "1.0.37"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
-  integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==
+  version "1.0.41"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.41.tgz#bd04dc9ec830fcf9e4fad35cf22dcedd2e3b4e9c"
+  integrity sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==
 
-unbox-primitive@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
-  integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
+unbox-primitive@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2"
+  integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==
   dependencies:
-    call-bind "^1.0.2"
+    call-bound "^1.0.3"
     has-bigints "^1.0.2"
-    has-symbols "^1.0.3"
-    which-boxed-primitive "^1.0.2"
+    has-symbols "^1.1.0"
+    which-boxed-primitive "^1.1.1"
 
-undici-types@~5.26.4:
-  version "5.26.5"
-  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
-  integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
-
-union-value@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
-  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
-  dependencies:
-    arr-union "^3.1.0"
-    get-value "^2.0.6"
-    is-extendable "^0.1.1"
-    set-value "^2.0.1"
-
-unset-value@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
-  integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==
-  dependencies:
-    has-value "^0.3.1"
-    isobject "^3.0.0"
+undici-types@~7.10.0:
+  version "7.10.0"
+  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350"
+  integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
 
 uri-js@^4.2.2:
   version "4.4.1"
@@ -4513,16 +4336,6 @@
   dependencies:
     punycode "^2.1.0"
 
-urix@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
-  integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==
-
-use@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
-  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
-
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -4536,94 +4349,115 @@
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
 
-vscode-css-languageservice@4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.0.tgz#40c797d664ab6188cace33cfbb19b037580a9318"
-  integrity sha512-BkQAMz4oVHjr0oOAz5PdeE72txlLQK7NIwzmclfr+b6fj6I8POwB+VoXvrZLTbWt9hWRgfvgiQRkh5JwrjPJ5A==
+vscode-css-languageservice@4.3.0, vscode-css-languageservice@^6.3.6:
+  version "6.3.7"
+  resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.7.tgz#110d11a87973fb52fc9e1a5072b480580673daab"
+  integrity sha512-5TmXHKllPzfkPhW4UE9sODV3E0bIOJPOk+EERKllf2SmAczjfTmYeq5txco+N3jpF8KIZ6loj/JptpHBQuVQRA==
   dependencies:
-    vscode-languageserver-textdocument "^1.0.1"
-    vscode-languageserver-types "3.16.0-next.2"
-    vscode-nls "^4.1.2"
-    vscode-uri "^2.1.2"
+    "@vscode/l10n" "^0.0.18"
+    vscode-languageserver-textdocument "^1.0.12"
+    vscode-languageserver-types "3.17.5"
+    vscode-uri "^3.1.0"
 
-vscode-html-languageservice@3.1.0:
+vscode-html-languageservice@3.1.0, vscode-html-languageservice@^5.5.0:
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.5.1.tgz#d52c578dd0198a3e651e12092f72a3398c4783bc"
+  integrity sha512-/ZdEtsZ3OiFSyL00kmmu7crFV9KwWR+MgpzjsxO60DQH7sIfHZM892C/E4iDd11EKocr+NYuvOA4Y7uc3QzLEA==
+  dependencies:
+    "@vscode/l10n" "^0.0.18"
+    vscode-languageserver-textdocument "^1.0.12"
+    vscode-languageserver-types "^3.17.5"
+    vscode-uri "^3.1.0"
+
+vscode-languageserver-textdocument@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631"
+  integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==
+
+vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.5:
+  version "3.17.5"
+  resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
+  integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
+
+vscode-uri@^3.1.0:
   version "3.1.0"
-  resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.1.0.tgz#265b53bda595e6947b16b0fb8c604e1e58685393"
-  integrity sha512-QAyRHI98bbEIBCqTzZVA0VblGU40na0txggongw5ZgTj9UVsVk5XbLT16O9OTcbqBGSqn0oWmFDNjK/XGIDcqg==
-  dependencies:
-    vscode-languageserver-textdocument "^1.0.1"
-    vscode-languageserver-types "3.16.0-next.2"
-    vscode-nls "^4.1.2"
-    vscode-uri "^2.1.2"
+  resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
+  integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
 
-vscode-languageserver-textdocument@^1.0.1:
-  version "1.0.11"
-  resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
-  integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
-
-vscode-languageserver-types@3.16.0-next.2:
-  version "3.16.0-next.2"
-  resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083"
-  integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==
-
-vscode-nls@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
-  integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==
-
-vscode-uri@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c"
-  integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==
-
-web-component-analyzer@~1.1.1:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/web-component-analyzer/-/web-component-analyzer-1.1.7.tgz#dad7f5a91f3b095c5a54f0f48d2dccb810c96b89"
-  integrity sha512-SqCqN4nU9fU+j0CKXJQ8E4cslLsaezhagY6xoi+hoNPPd55GzR6MY1r5jkoJUVu+g4Wy4uB+JglTt7au4vQ1uA==
+web-component-analyzer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/web-component-analyzer/-/web-component-analyzer-2.0.0.tgz#38a66055ae2340fceaea84f19f9ee3f15233ba28"
+  integrity sha512-UEvwfpD+XQw99sLKiH5B1T4QwpwNyWJxp59cnlRwFfhUW6JsQpw5jMeMwi7580sNou8YL3kYoS7BWLm+yJ/jVQ==
   dependencies:
     fast-glob "^3.2.2"
-    ts-simple-type "~1.0.5"
-    typescript "^3.8.3"
-    yargs "^15.3.1"
+    ts-simple-type "2.0.0-next.0"
+    typescript "~5.2.0"
+    yargs "^17.7.2"
 
 webidl-conversions@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
   integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
 
-whatwg-url@^11.0.0:
-  version "11.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
-  integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+whatwg-url@^14.0.0:
+  version "14.2.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663"
+  integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
   dependencies:
-    tr46 "^3.0.0"
+    tr46 "^5.1.0"
     webidl-conversions "^7.0.0"
 
-which-boxed-primitive@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
-  integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"
+  integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==
   dependencies:
-    is-bigint "^1.0.1"
-    is-boolean-object "^1.1.0"
-    is-number-object "^1.0.4"
-    is-string "^1.0.5"
-    is-symbol "^1.0.3"
+    is-bigint "^1.1.0"
+    is-boolean-object "^1.2.1"
+    is-number-object "^1.1.1"
+    is-string "^1.1.1"
+    is-symbol "^1.1.1"
 
-which-module@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
-  integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
+which-builtin-type@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e"
+  integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==
+  dependencies:
+    call-bound "^1.0.2"
+    function.prototype.name "^1.1.6"
+    has-tostringtag "^1.0.2"
+    is-async-function "^2.0.0"
+    is-date-object "^1.1.0"
+    is-finalizationregistry "^1.1.0"
+    is-generator-function "^1.0.10"
+    is-regex "^1.2.1"
+    is-weakref "^1.0.2"
+    isarray "^2.0.5"
+    which-boxed-primitive "^1.1.0"
+    which-collection "^1.0.2"
+    which-typed-array "^1.1.16"
 
-which-typed-array@^1.1.14, which-typed-array@^1.1.15:
-  version "1.1.15"
-  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
-  integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==
+which-collection@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0"
+  integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==
+  dependencies:
+    is-map "^2.0.3"
+    is-set "^2.0.3"
+    is-weakmap "^2.0.2"
+    is-weakset "^2.0.3"
+
+which-typed-array@^1.1.16, which-typed-array@^1.1.19:
+  version "1.1.19"
+  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956"
+  integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==
   dependencies:
     available-typed-arrays "^1.0.7"
-    call-bind "^1.0.7"
-    for-each "^0.3.3"
-    gopd "^1.0.1"
+    call-bind "^1.0.8"
+    call-bound "^1.0.4"
+    for-each "^0.3.5"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
     has-tostringtag "^1.0.2"
 
 which@^1.2.9:
@@ -4650,10 +4484,10 @@
   resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-5.1.0.tgz#4c4d20446dcc670b14fa115ef4f8fd9947af2b3a"
   integrity sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==
 
-wrap-ansi@^6.2.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
-  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
   dependencies:
     ansi-styles "^4.0.0"
     string-width "^4.1.0"
@@ -4664,65 +4498,56 @@
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
-write-file-atomic@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
-  integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+write-file-atomic@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd"
+  integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==
   dependencies:
     imurmurhash "^0.1.4"
-    is-typedarray "^1.0.0"
-    signal-exit "^3.0.2"
-    typedarray-to-buffer "^3.1.5"
+    signal-exit "^3.0.7"
 
-ws@^7.4.2:
-  version "7.5.9"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
-  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+ws@^7.5.10:
+  version "7.5.10"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
+  integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
 
-y18n@^4.0.0:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
-  integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+y18n@^5.0.5:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
 yallist@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
-yargs-parser@^18.1.2:
-  version "18.1.3"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
-  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
 yargs-parser@^20.2.3:
   version "20.2.9"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
   integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
 
-yargs@^15.3.1:
-  version "15.4.1"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
-  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+yargs-parser@^21.1.1:
+  version "21.1.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+  integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs@^17.7.2:
+  version "17.7.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
   dependencies:
-    cliui "^6.0.0"
-    decamelize "^1.2.0"
-    find-up "^4.1.0"
-    get-caller-file "^2.0.1"
+    cliui "^8.0.1"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
     require-directory "^2.1.1"
-    require-main-filename "^2.0.0"
-    set-blocking "^2.0.0"
-    string-width "^4.2.0"
-    which-module "^2.0.0"
-    y18n "^4.0.0"
-    yargs-parser "^18.1.2"
+    string-width "^4.2.3"
+    y18n "^5.0.5"
+    yargs-parser "^21.1.1"
 
 ylru@^1.2.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785"
-  integrity sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.4.0.tgz#0cf0aa57e9c24f8a2cbde0cc1ca2c9592ac4e0f6"
+  integrity sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==
 
 yocto-queue@^0.1.0:
   version "0.1.0"