Merge changes I19e00ed1,I534ec42f,I2aa1fe7f,I578092a0,If3404c03

* changes:
  Allow to schedule periodic project reindexing
  Deprecate index.scheduledIndexer in favor of scheduledIndexer.groups
  Allow to run periodic group reindexing also on primary
  Extract periodic index scheduler out of PeriodicGroupIndexer
  Let GerritIsReplicaProvider honor the --replica option
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 857725d..b7493a3 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -133,7 +133,7 @@
 
 ----
   [label "Verified"]
-      function = MaxWithBlock
+      function = NoBlock
       value = -1 Fails
       value = 0 No score
       value = +1 Verified
@@ -159,6 +159,23 @@
 +
 *Any +1 enables submit.*
 
+Set the function to "NoBlock" to enable configuring submit-requirements.
+All other possible label function values are deprecated. The default is still
+"MaxWithBlock" which doesn't allow using the more flexible submit-requirements.
+
+Add a submit-requirement for the "Verified" label to define which
+conditions are required to make the change submittable:
+
+----
+  [submit-requirement "Verified"]
+    submittableIf = label:Verified=MAX AND -label:Verified=MIN
+    applicableIf = -branch:refs/meta/config
+----
+
+See the
+link:config-submit-requirements.html#examples[submit-requirements
+documentation] for more details.
+
 For a change to be submittable, the change must have a `+1 Verified`
 in this label, and no `-1 Fails`.  Thus, `-1 Fails` can block a submit,
 while `+1 Verified` enables a submit.
@@ -568,12 +585,25 @@
 
 ----
   [label "Copyright-Check"]
-      function = MaxWithBlock
+      function = NoBlock
       value = -1 Do not have copyright
       value = 0 No score
       value = +1 Copyright clear
 ----
 
+Add a submit-requirement for the "Copyright-Check" label to define which
+score is required to make the change submittable:
+
+----
+  [submit-requirement "Copyright-Check"]
+    submittableIf = label:Copyright-Check=MAX AND -label:Copyright-Check=MIN
+    applicableIf = -branch:refs/meta/config
+----
+
+See the
+link:config-submit-requirements.html#examples[submit-requirements
+documentation] for more details.
+
 The new column will appear at the end of the table, and `-1 Do not have
 copyright` will block submit, while `+1 Copyright clear` is required to
 enable submit.
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 5ab1add..560c77f 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -473,6 +473,38 @@
 +
 `distinctvoters:[Code-Review,Trust,API-Review],count>2`
 
+[[operator_label_with_users_arg]]
+label:'<label><operator><value>,users=human_reviewers'::
++
+Extension of the link:user-search.html#labels[label] predicate that
+allows matching changes that have a matching vote from all human
+reviewers. Votes from service users (members of the
+link:access-control.html#service_users[Service Users] group) and the
+change owner are ignored.
++
+If link:config-project-config.html#reviewer.enableByEmail[reviewers by
+email] are present then "user=all_reviewers" doesn't match if the
+expected value is other than 0. Reviewers by email are reviewers that
+don't have a Gerrit account.  Without Gerrit account they cannot vote
+on the change, which means changes that have any such reviewers never
+match when a vote from all reviewers is expected.
++
+If a change has no human reviewers, this operator doesn't match
+(because a human review is required but no human reviewer is present).
++
+Examples:
+`label:Code-Review=MAX,users=human_reviewers`
++
+`label:Code-Review>=1,users=human_reviewers`
++
+The 'users' arg cannot be combined with other arguments ('count',
+'user', 'group').
++
+'label:Code-Review=MAX,users=human_reviewers' can be used to
+implement "Want-Code-Review-From-All" functionaly, see
+link#require-code-review-approvals-from-all-human-reviewers-example[examples
+below].
+
 [[operator_is_true]]
 is:true::
 +
@@ -557,7 +589,7 @@
 == Examples
 
 [[code-review-example]]
-=== Code-Review Example
+=== Require Code-Review approval from a non-uploader
 
 To define a submit requirement for code-review that requires a maximum vote for
 the “Code-Review” label from a non-uploader without a maximum negative vote:
@@ -571,7 +603,7 @@
 ----
 
 [[exempt-branch-example]]
-=== Exempt a branch Example
+=== Exempt a branch
 
 We could exempt a submit requirement from certain branches. For example,
 project owners might want to skip the 'Code-Style' requirement from the
@@ -602,7 +634,7 @@
 ----
 
 [[require-footer-example]]
-=== Require a footer Example
+=== Require a footer
 
 It's possible to use a submit requirement to require a footer to be present in
 the commit message.
@@ -614,6 +646,59 @@
   submittableIf = hasfooter:\"Bug\"
 ----
 
+[[require-code-review-approvals-from-all-human-reviewers-example]]
+=== Require Code-Review approvals from all human reviewers
+
+The following submit requirement requires a 'Code-Review' approval
+('Code-Review+1' or 'Code-Review+2') from all human reviewers of the
+change. Votes from service users (members of the
+link:access-control.html#service_users[Service Users] group) and the
+change owner are ignored.
+
+The 'applicableIf' condition makes this submit requirement show up in
+the UI only if it is not satisfied (to keep the submit requirement
+showing when it is satisfied omit the 'applicableIf' condition).
+
+If a change has no human reviewers, this submit requirement is
+unsatisfied (because a human review is required but no human reviewer
+is present).
+
+----
+[submit-requirement "Want-Code-Review-From-All"]
+  description = A 'Code-Review' vote is required from all human \
+                reviewers (service users that are reviewers are \
+                ignored).
+  applicableIf = -label:Code-Review>=1,users=human_reviewers
+  submittableIf = label:Code-Review>=1,users=human_reviewers
+----
+
+It is possible to configure the 'Want-Code-Review-From-All' submit
+requirement so that it only applies when a 'Want-Code-Review: all'
+footer is present in the commit message. This way users can enable
+this submit requirement on demand by including this footer into their
+commit messages.
+
+The 'applicableIf' condition checks for the 'Want-Code-Review: all'
+footer and makes this submit requirement show up in the UI only if it
+is not satisfied (to keep the submit requirement showing when it is
+satisfied omit the '-label:Code-Review>=1,users=human_reviewers'
+predicate from the 'applicableIf' condition).
+
+Note, the footer key cannot contain underscores (e.g. using
+'Want_Code_Review: all' as the footer does not work).
+
+----
+[submit-requirement "Want-Code-Review-From-All"]
+  description = A 'Code-Review' vote is required from all human \
+                reviewers (service users that are reviewers are \
+                ignored).
+  applicableIf = footer:\"Want-Code-Review: all\" -label:Code-Review>=1,users=human_reviewers
+  submittableIf = label:Code-Review>=1,users=human_reviewers
+----
+
+For more information about the "users=human_reviewers" arg see
+link:#operator_label_with_users_arg[above].
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
index 183c132..2946d44a 100644
--- a/Documentation/pgm-reindex.txt
+++ b/Documentation/pgm-reindex.txt
@@ -44,6 +44,16 @@
 	populated disk caches on large Gerrit sites, it is recommended that
 	bloom filters are disabled to improve performance.
 
+--reuse::
+	Reuse the change documents that already exist instead of
+	recreating the whole index from scratch. Each existing document in
+	the index will be checked and reindexed if found to be stale.
+
+	NOTE: Only supported when reindexing changes.
+
+	Use this option if offline reindexing is restarted or crashed.
+	Without this option a restart recreates the complete index
+	from scratch without reusing existing index documents.
 
 == CONTEXT
 The secondary index must be enabled. See
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0305aaa..c199d82 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2751,7 +2751,7 @@
   }
 ----
 
-[[set-work-in-pogress]]
+[[set-work-in-progress]]
 === Set Work-In-Progress
 --
 'POST /changes/link:#change-id[\{change-id\}]/wip'
@@ -3634,6 +3634,9 @@
 
 Rebases change edit on top of latest patch set.
 
+Optionally, input parameters may be specified in the request body as a
+link:#rebase-change-edit-input[RebaseChangeEditInput] entity.
+
 If one of the secondary emails associated with the user performing the operation was used as the
 committer email in the latest patch set, the same email will be used as the committer email in the
 new change edit commit; otherwise, the user's preferred email will be used.
@@ -3643,16 +3646,48 @@
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:rebase HTTP/1.0
 ----
 
-When change was rebased on top of latest patch set, response
-"`204 No Content`" is returned. When change edit is already
-based on top of the latest patch set, the response
-"`409 Conflict`" is returned.
+When the change was rebased on top of latest patch set, the response code is
+"`200 OK`" and the rebased change edit is returned as an
+link:#edit-info[EditInfo] entity.
 
 .Response
 ----
-  HTTP/1.1 204 No Content
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "commit": {
+      "parents": [
+        {
+          "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+        }
+      ],
+      "author": {
+        "name": "Shawn O. Pearce",
+        "email": "sop@google.com",
+        "date": "2012-04-24 18:08:08.000000000",
+        "tz": -420
+       },
+       "committer": {
+         "name": "Shawn O. Pearce",
+         "email": "sop@google.com",
+         "date": "2012-04-24 18:08:08.000000000",
+         "tz": -420
+       },
+       "subject": "Use an EventBus to manage star icons",
+       "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+    },
+    "base_patch_set_number": s,
+    "base_revision": "c35558e0925e6985c91f3a16921537d5e572b7a3",
+    "ref": "refs/users/01/1000001/edit-76482/2"
+  }
 ----
 
+When the change edit is already based on top of the latest patch set, the
+response code is "`409 Conflict`".
+
 [[delete-edit]]
 === Delete Change Edit
 --
@@ -5816,7 +5851,12 @@
 Applies a list of <<fix-replacement-info,FixReplacementInfo>> loaded from the
 <<apply-provided-fix-input,ApplyProvidedFixInput>> entity. The fixes are passed as part of the request body. The
 application of the fixes creates a new change edit. `Apply Provided Fix` can only be applied on the current
-patchset.
+patchset. To apply a fix that was suggested to a non-current patchset, set `originalPatchsetForFix` to a patchset
+number where the fix was suggested. When `originalPatchsetForFix` is set, gerrit generates a patch (using
+`fix-replacement-info` and `originalPatchsetForFix`) and then tries to apply it to the current patchset.
+If it is not possible to apply a patch, an error is returned (see below for the list of errors).
+If the provided fix modifies the commit message and the message was changed between `originalPatchsetForFix`
+and the current patchset, the request is rejected..
 
 If one of the secondary emails associated with the user performing the operation was used as the
 committer email in the current patch set, the same email will be used as the committer email in the
@@ -8019,6 +8059,12 @@
 |`files`                |optional|
 The files of the change edit as a map that maps the file names to
 link:#file-info[FileInfo] entities.
+|`contains_git_conflicts`  |optional, not set if `false`|
+Whether the change edit contains conflicts. +
+If `true`, some of the file contents of the change edit contain git conflict
+markers to indicate the conflicts. +
+Only set if this edit info is returned in response to a request that
+link:#rebase-edit[rebases the change edit] and conflicts are allowed.
 |===========================
 
 [[fetch-info]]
@@ -8477,6 +8523,21 @@
 |`end`        | Last index.
 |===========================
 
+[[rebase-change-edit-input]]
+=== RebaseChangeEditInput
+The `RebaseChangeEditInput` entity contains information for rebasing a change edit.
+
+[options="header",cols="1,^1,5"]
+|====================================
+|Field Name             ||Description
+|`allow_conflicts`      |optional, defaults to false|
+If `true`, the rebase also succeeds if there are conflicts. +
+If there are conflicts the file contents of the rebased patch set contain
+git conflict markers to indicate the conflicts. +
+Callers can find out whether there were conflicts by checking the
+`contains_git_conflicts` field in the returned link:#edit-info[EditInfo].
+|====================================
+
 [[rebase-input]]
 === RebaseInput
 The `RebaseInput` entity contains information for changing parent when rebasing.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 0cb407e..444cc23 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1514,8 +1514,9 @@
       "name": "accounts",
       "versions": {
         "13": {
-          "write": true,
-          "search": true
+          "is_write": true,
+          "is_search": true,
+          "num_docs": 3250
         }
       }
     },
@@ -1523,12 +1524,14 @@
       "name": "changes",
       "versions": {
         "83": {
-          "write": true,
-          "search": true
+          "is_write": true,
+          "is_search": true,
+          "num_docs": 250000
         },
         "84": {
-          "write": true,
-          "search": false
+          "is_write": true,
+          "is_search": false,
+          "num_docs": 150000
         }
       }
     },
@@ -1536,8 +1539,9 @@
       "name": "groups",
       "versions": {
         "10": {
-          "write": true,
-          "search": true
+          "is_write": true,
+          "is_search": true,
+          "num_docs": 500
         }
       }
     },
@@ -1545,8 +1549,9 @@
       "name": "projects",
       "versions": {
         "8": {
-          "write": true,
-          "search": true
+          "is_write": true,
+          "is_search": true,
+          "num_docs": 90
         }
       }
     }
@@ -1576,12 +1581,14 @@
     "name": "changes",
     "versions": {
       "83": {
-        "write": true,
-        "search": true
+        "is_write": true,
+        "is_search": true,
+        "num_docs": 250000
       },
       "84": {
-        "write": true,
-        "search": false
+        "is_write": true,
+        "is_search": false,
+        "num_docs": 150000
       }
     }
   }
@@ -1607,12 +1614,14 @@
   )]}'
   {
     "83": {
-      "write": true,
-      "search": true
+      "is_write": true,
+      "is_search": true,
+      "num_docs": 250000
     },
     "84": {
-      "write": true,
-      "search": false
+      "is_write": true,
+      "is_search": false,
+      "num_docs": 150000
     }
   }
 ----
@@ -1636,8 +1645,9 @@
 
   )]}'
   {
-    "write": true,
-    "search": false
+    "is_write": true,
+    "is_search": false,
+    "num_docs": 150000
   }
 ----
 
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 9825478..469fcdd 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -32,12 +32,20 @@
 changing the attention set:
 
 * If reviewers are added to a change, then they are added to the attention set.
-  * Exception: A reviewer adding themselves along with a comment or vote.
+  ** Exception: A reviewer adding themselves along with a comment or vote.
 * If an active change is submitted, abandoned or reset to "work in progress",
   then all users are removed from the attention set.
 * Replying (commenting, voting or just writing a change message) removes the
   replying user from the attention set. And it adds all participants of comment
-  conversations that the user is replying to.
+  conversations that the user is replying to. Specifically
+  ** If owner is replying and thread is resolved: all participants who have not
+  given Code-Review yet, are added to the attention set.
+  ** If owner is replying and thread is unresolved: all participants are added
+  to the attention set.
+  ** If non-owner is replying and thread is unresolved: only owner is added to
+  the attention set.
+  ** If non-owner is replying and thread is resolved: all participants who have
+  not given Code-Review yet, are added to the attention set.
 * If a *reviewer* replies, then the change owner (and uploader) are added to the
   attention set.
 * For merged and abandoned changes the owner is added only when a human creates
diff --git a/contrib/hooks/ref-updated_repack-geometric.sh b/contrib/hooks/ref-updated_repack-geometric.sh
new file mode 100755
index 0000000..0fe640d
--- /dev/null
+++ b/contrib/hooks/ref-updated_repack-geometric.sh
@@ -0,0 +1,117 @@
+#!/bin/bash -e
+#
+# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Best run from the Gerrit ref-updated hook
+#
+
+# Make a simple "least effort" attempt to run geometric repacking after every
+# known update which may have written git objects, all while avoiding overloading
+# a server with too much repacking work.
+
+# The least effort avoids running more than one git repack on the same repo at a
+# time, or while a git gc is already running on a repo (by using .git/gc.pid as
+# a lock). To avoid overloading the server, it also avoids running more than 3
+# git repacks total across all repos. If any of these conditions would be violated,
+# this script simply does nothing and exits. The intention is to avoid doing too
+# much work during a burst, assuming that future updates will likely be good enough
+# to service the repos which were missed.
+#
+# Since this is an event based approach to repository maintenance, it is
+# recommended that another time based GC approach, perhaps a more significant and
+# costly one, repacking refs, creating bitmaps... be used in parallel with this
+# script. This simple policy of "least effort" should keep most repos from
+# degrading much even with very infrequent time based GCs.
+#
+# Since this script uses gc.pid to lock the repo against other git gcs, it means
+# that this script could potentially starve any time based gc maintenance from
+# happening on busy repos. It is therefore advisable for any such time based gc
+# jobs to spin for a while attempting to run if the job cannot acquire the gc.pid
+# lock to help ensure that time based gc also gets a chance to run.
+#
+# In order to be able to skip repacking for each update happening during repacking,
+# this script returns immediately after starting repacking in the background. If
+# this script were to instead block during repacking, it would simply delay
+# repacking for those updates instead of having a consolidating effect. That being
+# said, a smarter script might consider tracking that some updates happened after
+# repacking started and ensure that it gets repacked once again (while still
+# consolidating many updates), but that would likely no longer qualify as least
+# effort.
+#
+
+[ -z "$GERRIT_SITE" ] && { echo "ERROR: GERRIT_SITE not set" ; exit 1 ; }
+[ -z "$GIT_DIR" ] && { echo "ERROR: GIT_DIR not set" ; exit 2 ; }
+
+# ---- Generic ----
+
+debug() { true || echo "---- debug: $@" ; }
+
+cleanup() { [ -n "$GC_LOCK" ] && rm -- "$GCLOCK" ; }
+
+exec_locked() { # <lock> <cmd> [<args>...]
+    local lock=$1 rtn=0
+    shift
+    if ( set -o noclobber ; echo $$ > "$lock" ) > /dev/null 2>&1 ; then
+        GC_LOCK=$lock
+        debug "locked $lock"
+        "$@" || rtn=$?
+        rm -- "$lock" && unset GC_LOCK
+        debug "unlocked $lock"
+        return $rtn
+    fi
+    debug "already locked $lock"
+    return 20
+}
+
+exec_acquired() { # <lock> <max> <cmd> [<args>...]
+    local semaphore=$1 max=$2 rtn=0 slot lock
+    shift 2
+    mkdir -p -- "$semaphore"
+    for slot in $(seq "$max") ; do
+        lock="$semaphore/$slot"
+        touch -- "$lock"
+        exec 3<> "$lock"
+        if flock -n 3 ; then
+            debug "acquired semaphore $slot"
+            "$@" || rtn=$?
+            flock -o 3
+            debug "released semaphore $slot"
+            return $rtn
+        fi
+    done
+    debug "semaphore loaded $semaphore"
+    return 30
+}
+
+# ---- Policy ----
+
+gc_lock() { # <cmd> [<args>...]
+    exec_locked "$LOCK" "$@"
+}
+
+gc_runner() { # <cmd> [<args>...]
+    exec_acquired "$SEMAPHORE" "$MAX_RUNNERS" "$@"
+}
+
+trap cleanup EXIT
+
+MAX_RUNNERS=3
+SEMAPHORE=$GERRIT_SITE/logs/git-geometric.semaphore
+LOCK=$GIT_DIR/gc.pid
+
+gc_runner gc_lock git repack -n -d --no-write-bitmap-index --geometric=2 &
+
diff --git a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
index 7660948..4af9a31 100644
--- a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
@@ -70,6 +70,11 @@
   }
 
   @Override
+  public int numDocs() {
+    throw new UnsupportedOperationException("AccountIndex is disabled");
+  }
+
+  @Override
   public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts) {
     throw new UnsupportedOperationException("AccountIndex is disabled");
   }
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index c028a8e..4748e31 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -77,6 +77,11 @@
   }
 
   @Override
+  public int numDocs() {
+    throw new UnsupportedOperationException("ChangeIndex is disabled");
+  }
+
+  @Override
   public DataSource<ChangeData> getSource(Predicate<ChangeData> p, QueryOptions opts)
       throws QueryParseException {
     throw new UnsupportedOperationException("ChangeIndex is disabled");
diff --git a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
index f2aad4a..ec1bcd4 100644
--- a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
@@ -75,6 +75,11 @@
   }
 
   @Override
+  public int numDocs() {
+    throw new UnsupportedOperationException("ProjectIndex is disabled");
+  }
+
+  @Override
   public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts) {
     throw new UnsupportedOperationException("ProjectIndex is disabled");
   }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
index 2fd8a07..6d99ded 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -91,6 +91,14 @@
   void rebase() throws RestApiException;
 
   /**
+   * Rebases the change edit on top of the latest patch set of this change.
+   *
+   * @param input params for rebasing the change edit
+   * @throws RestApiException if the change edit couldn't be rebased or a change edit wasn't present
+   */
+  EditInfo rebase(RebaseChangeEditInput input) throws RestApiException;
+
+  /**
    * Publishes the change edit using default settings. See {@link #publish(PublishChangeEditInput)}
    * for more details.
    *
@@ -238,6 +246,11 @@
     }
 
     @Override
+    public EditInfo rebase(RebaseChangeEditInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void publish() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/changes/RebaseChangeEditInput.java b/java/com/google/gerrit/extensions/api/changes/RebaseChangeEditInput.java
new file mode 100644
index 0000000..4eb0ebb
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/changes/RebaseChangeEditInput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+public class RebaseChangeEditInput {
+  /**
+   * Whether the rebase should succeed if there are conflicts.
+   *
+   * <p>If there are conflicts the file contents of the rebased change contain git conflict markers
+   * to indicate the conflicts.
+   */
+  public boolean allowConflicts;
+}
diff --git a/java/com/google/gerrit/extensions/common/ApplyProvidedFixInput.java b/java/com/google/gerrit/extensions/common/ApplyProvidedFixInput.java
index cd28d83..7d48fce 100644
--- a/java/com/google/gerrit/extensions/common/ApplyProvidedFixInput.java
+++ b/java/com/google/gerrit/extensions/common/ApplyProvidedFixInput.java
@@ -24,4 +24,6 @@
   public ApplyProvidedFixInput() {}
 
   public List<FixReplacementInfo> fixReplacementInfos;
+
+  public Integer originalPatchsetForFix;
 }
diff --git a/java/com/google/gerrit/extensions/common/EditInfo.java b/java/com/google/gerrit/extensions/common/EditInfo.java
index 0cd5af3..9d170de 100644
--- a/java/com/google/gerrit/extensions/common/EditInfo.java
+++ b/java/com/google/gerrit/extensions/common/EditInfo.java
@@ -23,4 +23,16 @@
   public String ref;
   public Map<String, FetchInfo> fetch;
   public Map<String, FileInfo> files;
+
+  /**
+   * Whether the change edit contains conflicts.
+   *
+   * <p>If {@code true}, some of the file contents of the change contain git conflict markers to
+   * indicate the conflicts.
+   *
+   * <p>Only set if this edit info is returned in response to a request that rebases the change edit
+   * (see {@link com.google.gerrit.server.restapi.change.RebaseChangeEdit}) and conflicts are
+   * allowed.
+   */
+  public Boolean containsGitConflicts;
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
index 0d1e82a..3cdbe44 100644
--- a/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/EditInfoSubject.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.commits;
 import static com.google.gerrit.truth.MapSubject.mapEntries;
 
+import com.google.common.truth.BooleanSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
@@ -62,4 +63,10 @@
     isNotNull();
     return check("files").about(mapEntries()).that(editInfo.files);
   }
+
+  public BooleanSubject containsGitConflicts() {
+    isNotNull();
+    return check("containsGitConflicts")
+        .that(editInfo.containsGitConflicts != null ? editInfo.containsGitConflicts : false);
+  }
 }
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 78c698a..c8dab81 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -96,7 +96,7 @@
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
-import com.google.gerrit.server.project.DefaultProjectNameLockManager.DefaultProjectNameLockManagerModule;
+import com.google.gerrit.server.project.DefaultLockManager.DefaultLockManagerModule;
 import com.google.gerrit.server.restapi.RestApiModule;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore.JdbcAccountPatchReviewStoreModule;
 import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
@@ -370,7 +370,7 @@
     modules.add(new AttentionSetOwnerAdderModule());
     modules.add(new ChangeCleanupRunnerModule());
     modules.add(new AccountDeactivatorModule());
-    modules.add(new DefaultProjectNameLockManagerModule());
+    modules.add(new DefaultLockManagerModule());
     modules.add(new ExternalIdCaseSensitivityMigrator.ExternalIdCaseSensitivityMigratorModule());
     return dbInjector.createChildInjector(
         ModuleOverloader.override(
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index a92dd18..bb21662 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
@@ -30,6 +31,7 @@
 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.gson.Gson;
@@ -43,6 +45,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.regex.Matcher;
 
 /** Helper for generating parts of {@code index.html}. */
 @UsedAt(Project.GOOGLE)
@@ -80,6 +83,29 @@
     return data.build();
   }
 
+  /**
+   * Returns the basePatchNum that was specified in the URL when present. If no basePatchNum is
+   * specified then it points to PARENT which is represented by 0
+   */
+  public static Integer computeBasePatchNum(@Nullable String requestedPath) {
+    if (requestedPath == null) {
+      return 0;
+    }
+    Matcher matcher = IndexPreloadingUtil.CHANGE_URL_PATTERN.matcher(requestedPath);
+    String basePatchNum = null;
+    if (matcher.matches()) {
+      basePatchNum = matcher.group("basePatchNum");
+    }
+    if (basePatchNum == null) {
+      return 0; // No match is found
+    }
+    Integer basePatchNumInt = Ints.tryParse(basePatchNum);
+    if (basePatchNumInt == null) {
+      return 0; // tryParse was unable to parse
+    }
+    return basePatchNumInt;
+  }
+
   /** Returns dynamic parameters of {@code index.html}. */
   public static ImmutableMap<String, Object> dynamicTemplateData(
       GerritApi gerritApi, String requestedURL, String canonicalURL)
@@ -99,11 +125,19 @@
 
     String requestedPath = IndexPreloadingUtil.getPath(requestedURL);
     IndexPreloadingUtil.RequestedPage page = IndexPreloadingUtil.parseRequestedPage(requestedPath);
+    Integer basePatchNum = computeBasePatchNum(requestedPath);
     switch (page) {
       case CHANGE:
       case DIFF:
-        data.put(
-            "defaultChangeDetailHex", ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS));
+        if (basePatchNum.equals(0)) {
+          data.put(
+              "defaultChangeDetailHex",
+              ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS));
+        } else {
+          data.put(
+              "defaultChangeDetailHex",
+              ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS));
+        }
         data.put(
             "changeRequestsPath",
             IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
@@ -111,7 +145,8 @@
         break;
       case PROFILE:
       case DASHBOARD:
-        // Dashboard is preloaded queries are added later when we check user is authenticated.
+        // Dashboard is preloaded queries are added later when we check user is
+        // authenticated.
       case PAGE_WITHOUT_PRELOADING:
         break;
     }
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index cc11638..43d0172 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -45,7 +45,8 @@
   }
 
   public static final String CHANGE_CANONICAL_PATH = "/c/(?<project>.+)/\\+/(?<changeNum>\\d+)";
-  public static final String BASE_PATCH_NUM_PATH_PART = "(/(-?\\d+|edit)(\\.\\.(\\d+|edit))?)";
+  public static final String BASE_PATCH_NUM_PATH_PART =
+      "(/(?<basePatchNum>-?\\d+|edit)(\\.\\.(\\d+|edit))?)";
   public static final Pattern CHANGE_URL_PATTERN =
       Pattern.compile(CHANGE_CANONICAL_PATH + BASE_PATCH_NUM_PATH_PART + "?" + "/?$");
   public static final Pattern DIFF_URL_PATTERN =
@@ -90,7 +91,22 @@
           ListChangesOption.SUBMIT_REQUIREMENTS,
           ListChangesOption.STAR);
 
-  public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS =
+  public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS_WITHOUT_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);
+
+  public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS_WITH_PARENTS =
       ImmutableSet.of(
           ListChangesOption.ALL_COMMITS,
           ListChangesOption.ALL_REVISIONS,
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index ec530c1..fdf806c 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -75,6 +75,9 @@
   /** Delete all documents from the index. */
   void deleteAll();
 
+  /** Return the number of documents in this index */
+  int numDocs();
+
   /**
    * Convert the given operator predicate into a source searching the index and returning only the
    * documents matching that predicate.
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 570290e..b06143e 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -119,6 +119,13 @@
     }
   }
 
+  @Override
+  public int numDocs() {
+    synchronized (indexedDocuments) {
+      return indexedDocuments.size();
+    }
+  }
+
   public int getQueryCount() {
     return queryCount;
   }
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index e00c394..85ffd93 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -326,6 +326,21 @@
     }
   }
 
+  @Override
+  public int numDocs() {
+    try {
+      IndexSearcher searcher = acquire();
+      try {
+        return searcher.getIndexReader().numDocs();
+      } finally {
+        release(searcher);
+      }
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log(e.getMessage());
+      throw new StorageException(e);
+    }
+  }
+
   public IndexWriter getWriter() {
     return writer;
   }
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index ffd25ba..e5f7787 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -265,6 +265,11 @@
   }
 
   @Override
+  public int numDocs() {
+    return openIndex.numDocs() + closedIndex.numDocs();
+  }
+
+  @Override
   public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
       throws QueryParseException {
     Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 823be14..4a47e5ad 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -63,6 +63,7 @@
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountDeactivator.AccountDeactivatorModule;
 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;
 import com.google.gerrit.server.account.storage.notedb.AccountNoteDbReadStorageModule;
 import com.google.gerrit.server.account.storage.notedb.AccountNoteDbWriteStorageModule;
@@ -101,6 +102,7 @@
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier.SignedTokenEmailTokenVerifierModule;
 import com.google.gerrit.server.mail.receive.MailReceiver.MailReceiverModule;
+import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
 import com.google.gerrit.server.mail.send.SmtpEmailSender.SmtpEmailSenderModule;
 import com.google.gerrit.server.mime.MimeUtil2Module;
 import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
@@ -110,7 +112,7 @@
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
-import com.google.gerrit.server.project.DefaultProjectNameLockManager.DefaultProjectNameLockManagerModule;
+import com.google.gerrit.server.project.DefaultLockManager.DefaultLockManagerModule;
 import com.google.gerrit.server.restapi.RestApiModule;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore.JdbcAccountPatchReviewStoreModule;
 import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
@@ -475,7 +477,10 @@
 
     modules.add(new AccountNoteDbWriteStorageModule());
     modules.add(new AccountNoteDbReadStorageModule());
+    modules.add(new ExternalIdCacheImpl.ExternalIdCacheModule());
+    modules.add(new ExternalIdCacheImpl.ExternalIdCacheBindingModule());
     modules.add(new RepoSequenceModule());
+    modules.add(new FromAddressGeneratorProvider.UserAddressGenModule());
     modules.add(new NoteDbDraftCommentsModule());
     modules.add(new NoteDbStarredChangesModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
@@ -559,7 +564,7 @@
       modules.add(new ChangeCleanupRunnerModule());
     }
     modules.add(new LocalMergeSuperSetComputationModule());
-    modules.add(new DefaultProjectNameLockManagerModule());
+    modules.add(new DefaultLockManagerModule());
 
     List<Module> libModules =
         LibModuleLoader.loadModules(cfgInjector, LibModuleType.SYS_MODULE_TYPE);
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index d393a89..7424407 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.LibModuleLoader;
 import com.google.gerrit.server.ModuleOverloader;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl;
 import com.google.gerrit.server.account.storage.notedb.AccountNoteDbReadStorageModule;
 import com.google.gerrit.server.account.storage.notedb.AccountNoteDbWriteStorageModule;
 import com.google.gerrit.server.cache.CacheDisplay;
@@ -243,6 +244,8 @@
         });
     modules.add(new AccountNoteDbWriteStorageModule());
     modules.add(new AccountNoteDbReadStorageModule());
+    modules.add(new ExternalIdCacheImpl.ExternalIdCacheModule());
+    modules.add(new ExternalIdCacheImpl.ExternalIdCacheBindingModule());
     modules.add(new RepoSequenceModule());
     modules.add(new NoteDbDraftCommentsModule());
     modules.add(new NoteDbStarredChangesModule());
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index f45f1be..f30efd4 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -42,7 +42,6 @@
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.ServiceUserClassifierImpl;
-import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheModule;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -99,6 +98,7 @@
 import com.google.gerrit.server.submitrequirement.predicate.DistinctVotersPredicate;
 import com.google.gerrit.server.submitrequirement.predicate.FileEditsPredicate;
 import com.google.gerrit.server.submitrequirement.predicate.HasSubmoduleUpdatePredicate;
+import com.google.gerrit.server.submitrequirement.predicate.SubmitRequirementLabelExtensionPredicate;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -181,7 +181,6 @@
     modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultMemoryCacheModule());
     modules.add(new H2CacheModule());
-    modules.add(new ExternalIdCacheModule());
     modules.add(new GroupModule());
     modules.add(new NoteDbModule());
     modules.add(AccountCacheImpl.module());
@@ -203,6 +202,7 @@
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeIsVisibleToPredicate.Factory.class);
     factory(DistinctVotersPredicate.Factory.class);
+    factory(SubmitRequirementLabelExtensionPredicate.Factory.class);
     factory(HasSubmoduleUpdatePredicate.Factory.class);
     factory(ProjectState.Factory.class);
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index a23e7bc..886fe708 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -28,6 +28,12 @@
  * cache is up to date.
  *
  * <p>All returned collections are unmodifiable.
+ *
+ * <p>NOTE: Modules which bind {@link ExternalIdCache} by using modules other than {@link
+ * com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl.ExternalIdCacheBindingModule},
+ * should also provide an {@code Optional<}{@link
+ * com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheImpl}{@code >}
+ * binding.
  */
 public interface ExternalIdCache {
   Optional<ExternalId> byKey(ExternalId.Key key) throws IOException;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java
index 8e53277..fd19fcc 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.server.account.externalids.ExternalIdCache;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Optional;
 
@@ -32,6 +34,12 @@
       protected void configure() {
         bind(ExternalIdCache.class).to(DisabledExternalIdCache.class);
       }
+
+      @Provides
+      @Singleton
+      Optional<ExternalIdCacheImpl> provideNoteDbExternalIdCacheImpl() {
+        return Optional.empty();
+      }
     };
   }
 
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java
index dbfe205..20c94eb 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java
@@ -14,27 +14,86 @@
 
 package com.google.gerrit.server.account.externalids.storage.notedb;
 
+import static com.google.inject.Scopes.SINGLETON;
+
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdCache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.Optional;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 
-/** Caches external IDs of all accounts. The external IDs are always loaded from NoteDb. */
-@Singleton
-class ExternalIdCacheImpl implements ExternalIdCache {
+/**
+ * Caches external IDs of all accounts. The external IDs are always loaded from NoteDb. *
+ *
+ * <p>This class should be bounded as a Singleton. However, due to internal limitations in Google,
+ * it cannot be marked as a singleton. The common installation pattern should therefore be:
+ *
+ * <pre>{@code
+ * * install(new ExternalIdCacheModule());
+ * * install(new ExternalIdCacheBindingModule());
+ * *
+ * }</pre>
+ */
+public class ExternalIdCacheImpl implements ExternalIdCache {
   public static final String CACHE_NAME = "external_ids_map";
 
+  public static class ExternalIdCacheModule extends CacheModule {
+    @Override
+    protected void configure() {
+      persist(CACHE_NAME, ObjectId.class, new TypeLiteral<AllExternalIds>() {})
+          // The cached data is potentially pretty large and we are always only interested
+          // in the latest value. However, due to a race condition, it is possible for different
+          // threads to observe different values of the meta ref, and hence request different keys
+          // from the cache. Extend the cache size by 1 to cover this case, but expire the extra
+          // object after a short period of time, since it may be a potentially large amount of
+          // memory.
+          // When loading a new value because the primary data advanced, we want to leverage the old
+          // cache state to recompute only what changed. This doesn't affect cache size though as
+          // Guava calls the loader first and evicts later on.
+          .maximumWeight(2)
+          .expireFromMemoryAfterAccess(Duration.ofMinutes(1))
+          .diskLimit(-1)
+          .version(1)
+          .keySerializer(ObjectIdCacheSerializer.INSTANCE)
+          .valueSerializer(AllExternalIds.Serializer.INSTANCE);
+    }
+  }
+
+  public static class ExternalIdCacheBindingModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class).in(SINGLETON);
+    }
+
+    /**
+     * Used by {@link ExternalIdsNoteDbImpl}. Modules which bind {@link ExternalIdCache} by using
+     * modules other than {@link ExternalIdCacheBindingModule}, should also provide an {@code
+     * Optional<ExternalIdCacheImpl>} binding.
+     */
+    @Provides
+    @Singleton
+    Optional<ExternalIdCacheImpl> provideNoteDbExternalIdCacheImpl(
+        ExternalIdCacheImpl externalIdCache) {
+      return Optional.of(externalIdCache);
+    }
+  }
+
   private final Cache<ObjectId, AllExternalIds> extIdsByAccount;
   private final ExternalIdReader externalIdReader;
   private final ExternalIdCacheLoader externalIdCacheLoader;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheModule.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheModule.java
deleted file mode 100644
index aca0e1a..0000000
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheModule.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account.externalids.storage.notedb;
-
-import com.google.gerrit.server.account.externalids.ExternalIdCache;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
-import com.google.inject.TypeLiteral;
-import java.time.Duration;
-import org.eclipse.jgit.lib.ObjectId;
-
-public class ExternalIdCacheModule extends CacheModule {
-  @Override
-  protected void configure() {
-    persist(ExternalIdCacheImpl.CACHE_NAME, ObjectId.class, new TypeLiteral<AllExternalIds>() {})
-        // The cached data is potentially pretty large and we are always only interested
-        // in the latest value. However, due to a race condition, it is possible for different
-        // threads to observe different values of the meta ref, and hence request different keys
-        // from the cache. Extend the cache size by 1 to cover this case, but expire the extra
-        // object after a short period of time, since it may be a potentially large amount of
-        // memory.
-        // When loading a new value because the primary data advanced, we want to leverage the old
-        // cache state to recompute only what changed. This doesn't affect cache size though as
-        // Guava calls the loader first and evicts later on.
-        .maximumWeight(2)
-        .expireFromMemoryAfterAccess(Duration.ofMinutes(1))
-        .diskLimit(-1)
-        .version(1)
-        .keySerializer(ObjectIdCacheSerializer.INSTANCE)
-        .valueSerializer(AllExternalIds.Serializer.INSTANCE);
-
-    bind(ExternalIdCacheImpl.class);
-    bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class);
-  }
-}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
index 7a2945c..4c26442 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdCache;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AuthConfig;
@@ -47,21 +46,15 @@
   @Inject
   ExternalIdsNoteDbImpl(
       ExternalIdReader externalIdReader,
-      ExternalIdCache externalIdCache,
+      Optional<ExternalIdCacheImpl> externalIdCacheImpl,
       ExternalIdKeyFactory externalIdKeyFactory,
       AuthConfig authConfig) {
     this.externalIdReader = externalIdReader;
-    if (externalIdCache instanceof ExternalIdCacheImpl) {
-      this.externalIdCache = (ExternalIdCacheImpl) externalIdCache;
-    } else if (externalIdCache instanceof DisabledExternalIdCache) {
-      // Supported case for testing only. Non of the disabled cache methods should be called, so
-      // it's safe to not assign the var.
-      this.externalIdCache = null;
-    } else {
-      throw new IllegalStateException(
-          "The cache provided in ExternalIdsNoteDbImpl should be either ExternalIdCacheImpl or"
-              + " DisabledExternalIdCache");
-    }
+    this.externalIdCache =
+        externalIdCacheImpl.orElse(
+            // Supported case for tests or Google implementation. None of the disabled cache methods
+            // should be called from these flows, so it's safe to not assign the var.
+            null);
     this.externalIdKeyFactory = externalIdKeyFactory;
     this.authConfig = authConfig;
   }
diff --git a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index c76eeeb..bdabcbd 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -16,10 +16,12 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.extensions.api.changes.ChangeEditApi;
 import com.google.gerrit.extensions.api.changes.ChangeEditIdentityType;
 import com.google.gerrit.extensions.api.changes.FileContentInput;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.api.changes.RebaseChangeEditInput;
 import com.google.gerrit.extensions.client.ChangeEditDetailOption;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.Input;
@@ -150,9 +152,14 @@
 
   @Override
   public void rebase() throws RestApiException {
+    rebase(new RebaseChangeEditInput());
+  }
+
+  @Override
+  @CanIgnoreReturnValue
+  public EditInfo rebase(RebaseChangeEditInput input) throws RestApiException {
     try {
-      @SuppressWarnings("unused")
-      var unused = rebaseChangeEdit.apply(changeResource, null);
+      return rebaseChangeEdit.apply(changeResource, input).value();
     } catch (Exception e) {
       throw asRestApiException("Cannot rebase change edit", e);
     }
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 0f6ae5f..1f0bd6e 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -109,7 +109,6 @@
 import com.google.gerrit.server.account.ServiceUserClassifierImpl;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalIdModule;
-import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheModule;
 import com.google.gerrit.server.approval.ApprovalsUtil;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -189,8 +188,8 @@
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.CommentLinkProvider;
+import com.google.gerrit.server.project.LockManager;
 import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.project.ProjectNameLockManager;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.PrologRulesWarningValidator;
 import com.google.gerrit.server.project.SubmitRequirementConfigValidator;
@@ -219,6 +218,7 @@
 import com.google.gerrit.server.submitrequirement.predicate.DistinctVotersPredicate;
 import com.google.gerrit.server.submitrequirement.predicate.FileEditsPredicate;
 import com.google.gerrit.server.submitrequirement.predicate.HasSubmoduleUpdatePredicate;
+import com.google.gerrit.server.submitrequirement.predicate.SubmitRequirementLabelExtensionPredicate;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.util.IdGenerator;
@@ -279,7 +279,6 @@
     install(new AccessControlModule());
     install(new AccountModule());
     install(new CmdLineParserModule());
-    install(new ExternalIdCacheModule());
     install(new ExternalIdModule());
     install(new GitModule());
     install(new GroupDbModule());
@@ -303,6 +302,7 @@
     factory(ChangeJson.AssistedFactory.class);
     factory(ChangeIsVisibleToPredicate.Factory.class);
     factory(DistinctVotersPredicate.Factory.class);
+    factory(SubmitRequirementLabelExtensionPredicate.Factory.class);
     factory(HasSubmoduleUpdatePredicate.Factory.class);
     factory(DeadlineChecker.Factory.class);
     factory(EmailNewPatchSet.Factory.class);
@@ -453,7 +453,7 @@
     DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
     DynamicSet.setOf(binder(), ActionVisitor.class);
     DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
-    DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
+    DynamicItem.itemOf(binder(), LockManager.class);
     DynamicSet.setOf(binder(), SubmitRule.class);
     DynamicSet.setOf(binder(), SubmitRequirement.class);
     DynamicSet.setOf(binder(), QuotaEnforcer.class);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 7a89fb3..1030baa 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.server.edit;
 
+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.CHANGE_MODIFICATION;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BooleanProjectConfig;
@@ -25,6 +28,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ChangeEditIdentityType;
+import com.google.gerrit.extensions.api.changes.RebaseChangeEditInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -37,6 +41,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.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.RenameFileModification;
@@ -44,6 +49,9 @@
 import com.google.gerrit.server.edit.tree.TreeCreator;
 import com.google.gerrit.server.edit.tree.TreeModification;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+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.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -62,15 +70,19 @@
 import java.time.Instant;
 import java.time.ZoneId;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.diff.Sequence;
+import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.InvalidPathException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -81,9 +93,9 @@
 import org.eclipse.jgit.merge.MergeChunk;
 import org.eclipse.jgit.merge.MergeResult;
 import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ResolveMerger;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
@@ -106,9 +118,11 @@
   private final ProjectCache projectCache;
   private final NoteDbEdits noteDbEdits;
   private final ChangeUtil changeUtil;
+  private final boolean useDiff3;
 
   @Inject
   ChangeEditModifier(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent gerritIdent,
       ChangeIndexer indexer,
       Provider<CurrentUser> currentUser,
@@ -126,6 +140,9 @@
     this.projectCache = projectCache;
     noteDbEdits = new NoteDbEdits(gitReferenceUpdated, zoneId, indexer, currentUser);
     this.changeUtil = changeUtil;
+    this.useDiff3 =
+        cfg.getBoolean(
+            "change", /* subsection= */ null, "diff3ConflictView", /* defaultValue= */ false);
   }
 
   /**
@@ -157,12 +174,15 @@
    *
    * @param repository the affected Git repository
    * @param notes the {@link ChangeNotes} of the change whose change edit should be rebased
+   * @param input the request input
+   * @return the rebased change edit commit
    * @throws AuthException if the user isn't authenticated or not allowed to use change edits
    * @throws InvalidChangeOperationException if a change edit doesn't exist for the specified
    *     change, the change edit is already based on the latest patch set, or the change represents
    *     the root commit
    */
-  public void rebaseEdit(Repository repository, ChangeNotes notes)
+  public CodeReviewCommit rebaseEdit(
+      Repository repository, ChangeNotes notes, RebaseChangeEditInput input)
       throws AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException, ResourceConflictException {
     assertCanEdit(notes);
@@ -182,14 +202,16 @@
               notes.getChangeId(), currentPatchSet.id()));
     }
 
-    rebase(notes.getProjectName(), repository, changeEdit, currentPatchSet);
+    return rebase(
+        notes.getProjectName(), repository, changeEdit, currentPatchSet, input.allowConflicts);
   }
 
-  private void rebase(
+  private CodeReviewCommit rebase(
       Project.NameKey project,
       Repository repository,
       ChangeEdit changeEdit,
-      PatchSet currentPatchSet)
+      PatchSet currentPatchSet,
+      boolean allowConflicts)
       throws IOException, MergeConflictException, InvalidChangeOperationException {
     RevCommit currentEditCommit = changeEdit.getEditCommit();
     if (currentEditCommit.getParentCount() == 0) {
@@ -197,20 +219,10 @@
           "Rebase change edit against root commit not supported");
     }
 
-    RevCommit basePatchSetCommit = NoteDbEdits.lookupCommit(repository, currentPatchSet.commitId());
-    RevTree basePatchSetTree = basePatchSetCommit.getTree();
-
-    ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree);
     Instant nowTimestamp = TimeUtil.now();
-    String commitMessage = currentEditCommit.getFullMessage();
-    ObjectId newEditCommitId =
-        createCommit(
-            repository,
-            basePatchSetCommit,
-            newTreeId,
-            commitMessage,
-            currentEditCommit.getAuthorIdent(),
-            new PersonIdent(currentEditCommit.getCommitterIdent(), nowTimestamp));
+    RevCommit basePatchSetCommit = NoteDbEdits.lookupCommit(repository, currentPatchSet.commitId());
+    CodeReviewCommit newEditCommit =
+        merge(repository, changeEdit, basePatchSetCommit, nowTimestamp, allowConflicts);
 
     noteDbEdits.baseEditOnDifferentPatchset(
         project,
@@ -218,8 +230,10 @@
         changeEdit,
         currentPatchSet,
         currentEditCommit,
-        newEditCommitId,
+        newEditCommit,
         nowTimestamp);
+
+    return newEditCommit;
   }
 
   /**
@@ -532,7 +546,7 @@
     return editBasePatchSet.id().equals(patchSet.id());
   }
 
-  private static ObjectId createNewTree(
+  public static ObjectId createNewTree(
       Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications)
       throws BadRequestException, IOException, InvalidChangeOperationException {
     if (treeModifications.isEmpty()) {
@@ -554,22 +568,106 @@
     return newTreeId;
   }
 
-  private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId)
+  private CodeReviewCommit merge(
+      Repository repository,
+      ChangeEdit changeEdit,
+      RevCommit basePatchSetCommit,
+      Instant timestamp,
+      boolean allowConflicts)
       throws IOException, MergeConflictException {
     PatchSet basePatchSet = changeEdit.getBasePatchSet();
     ObjectId basePatchSetCommitId = basePatchSet.commitId();
     ObjectId editCommitId = changeEdit.getEditCommit();
 
-    ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repository, true);
-    threeWayMerger.setBase(basePatchSetCommitId);
-    boolean successful = threeWayMerger.merge(newTreeId, editCommitId);
+    try (CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(repository);
+        ObjectInserter objectInserter = repository.newObjectInserter()) {
+      ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(repository, true);
+      merger.setBase(basePatchSetCommitId);
+
+      DirCache dc = DirCache.newInCore();
+      if (allowConflicts && merger instanceof ResolveMerger) {
+        // The DirCache must be set on ResolveMerger before calling
+        // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get
+        // populated.
+        ((ResolveMerger) merger).setDirCache(dc);
+      }
+
+      boolean successful = merger.merge(basePatchSetCommit, editCommitId);
+
+      ObjectId newTreeId;
+      ImmutableSet<String> filesWithGitConflicts;
+      if (successful) {
+        newTreeId = merger.getResultTreeId();
+        filesWithGitConflicts = null;
+      } else {
+        List<String> conflicts = ImmutableList.of();
+        if (merger instanceof ResolveMerger) {
+          conflicts = ((ResolveMerger) merger).getUnmergedPaths();
+        }
+
+        if (!allowConflicts || !(merger instanceof ResolveMerger)) {
+          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.",
+                  MergeUtil.createConflictMessage(conflicts)));
+        }
+
+        Map<String, MergeResult<? extends Sequence>> mergeResults =
+            ((ResolveMerger) merger).getMergeResults();
+        filesWithGitConflicts =
+            mergeResults.entrySet().stream()
+                .filter(e -> e.getValue().containsConflicts())
+                .map(Map.Entry::getKey)
+                .collect(toImmutableSet());
+
+        newTreeId =
+            MergeUtil.mergeWithConflicts(
+                revWalk,
+                objectInserter,
+                dc,
+                "PATCH SET",
+                basePatchSetCommit,
+                "EDIT",
+                revWalk.parseCommit(editCommitId),
+                mergeResults,
+                useDiff3);
+        objectInserter.flush();
+      }
+
+      RevCommit currentEditCommit = changeEdit.getEditCommit();
+      ObjectId newEditCommitId =
+          createCommit(
+              objectInserter,
+              basePatchSetCommit,
+              newTreeId,
+              currentEditCommit.getFullMessage(),
+              currentEditCommit.getAuthorIdent(),
+              new PersonIdent(currentEditCommit.getCommitterIdent(), timestamp));
+
+      CodeReviewCommit newEditCommit = revWalk.parseCommit(newEditCommitId);
+      newEditCommit.setFilesWithGitConflicts(filesWithGitConflicts);
+      return newEditCommit;
+    }
+  }
+
+  private static ObjectId mergeTrees(Repository repository, ChangeEdit changeEdit, ObjectId treeId)
+      throws IOException, MergeConflictException {
+    PatchSet basePatchSet = changeEdit.getBasePatchSet();
+    ObjectId basePatchSetCommitId = basePatchSet.commitId();
+    ObjectId editCommitId = changeEdit.getEditCommit();
+
+    ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(repository, true);
+    merger.setBase(basePatchSetCommitId);
+    boolean successful = merger.merge(treeId, editCommitId);
 
     if (!successful) {
       throw new MergeConflictException(
           "Rebasing change edit onto another patchset results in merge conflicts. Download the edit"
               + " patchset and rebase manually to preserve changes.");
     }
-    return threeWayMerger.getResultTreeId();
+    return merger.getResultTreeId();
   }
 
   private String createNewCommitMessage(
@@ -606,18 +704,30 @@
       PersonIdent committer)
       throws IOException {
     try (ObjectInserter objectInserter = repository.newObjectInserter()) {
-      CommitBuilder builder = new CommitBuilder();
-      builder.setTreeId(tree);
-      builder.setParentIds(basePatchsetCommit.getParents());
-      builder.setAuthor(author);
-      builder.setCommitter(committer);
-      builder.setMessage(commitMessage);
-      ObjectId newCommitId = objectInserter.insert(builder);
-      objectInserter.flush();
-      return newCommitId;
+      return createCommit(
+          objectInserter, basePatchsetCommit, tree, commitMessage, author, committer);
     }
   }
 
+  private ObjectId createCommit(
+      ObjectInserter objectInserter,
+      RevCommit basePatchsetCommit,
+      ObjectId tree,
+      String commitMessage,
+      PersonIdent author,
+      PersonIdent committer)
+      throws IOException {
+    CommitBuilder builder = new CommitBuilder();
+    builder.setTreeId(tree);
+    builder.setParentIds(basePatchsetCommit.getParents());
+    builder.setAuthor(author);
+    builder.setCommitter(committer);
+    builder.setMessage(commitMessage);
+    ObjectId newCommitId = objectInserter.insert(builder);
+    objectInserter.flush();
+    return newCommitId;
+  }
+
   private PersonIdent getCommitterIdent(RevCommit basePatchsetCommit, Instant commitTimestamp) {
     IdentifiedUser user = currentUser.get().asIdentifiedUser();
     return Optional.ofNullable(basePatchsetCommit.getCommitterIdent())
@@ -690,7 +800,7 @@
       if (ObjectId.isEqual(changeEdit.getEditCommit(), commitToModify)) {
         return newTreeId;
       }
-      return merge(repository, changeEdit, newTreeId);
+      return mergeTrees(repository, changeEdit, newTreeId);
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/index/OnlineReindexer.java b/java/com/google/gerrit/server/index/OnlineReindexer.java
index 98abf46..3701ab8 100644
--- a/java/com/google/gerrit/server/index/OnlineReindexer.java
+++ b/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -112,7 +112,7 @@
     if (!reuseExistingDocuments && oldVersion != newVersion) {
       index.deleteAll();
     }
-    SiteIndexer.Result result = batchIndexer.indexAll(index);
+    SiteIndexer.Result result = batchIndexer.indexAll(index, false);
     if (!result.success()) {
       logger.atSevere().log(
           "Online reindex of %s schema version %s failed. Successfully"
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
index ecf808d..4c63453 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.MailUtil;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -41,12 +42,43 @@
 public class FromAddressGeneratorProvider implements Provider<FromAddressGenerator> {
   private final FromAddressGenerator generator;
 
+  public static class UserAddressGenModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(UserAddressGenFactory.class).to(DefaultUserAddressGenFactory.class);
+    }
+  }
+
+  /** A generic interface for creating user address generators. */
+  public interface UserAddressGenFactory {
+    FromAddressGenerator create(
+        AccountCache accountCache,
+        Pattern domainPattern,
+        String anonymousCowardName,
+        ParameterizedString nameRewriteTmpl,
+        Address serverAddress);
+  }
+
+  public static class DefaultUserAddressGenFactory implements UserAddressGenFactory {
+    @Override
+    public FromAddressGenerator create(
+        AccountCache accountCache,
+        Pattern domainPattern,
+        String anonymousCowardName,
+        ParameterizedString nameRewriteTmpl,
+        Address serverAddress) {
+      return new UserGen(
+          accountCache, domainPattern, anonymousCowardName, nameRewriteTmpl, serverAddress);
+    }
+  }
+
   @Inject
   FromAddressGeneratorProvider(
       @GerritServerConfig Config cfg,
       @AnonymousCowardName String anonymousCowardName,
       @GerritPersonIdent PersonIdent myIdent,
-      AccountCache accountCache) {
+      AccountCache accountCache,
+      UserAddressGenFactory userAddressGenFactory) {
     final String from = cfg.getString("sendemail", null, "from");
     final Address srvAddr = toAddress(myIdent);
 
@@ -58,7 +90,8 @@
       Pattern domainPattern = MailUtil.glob(domains);
       ParameterizedString namePattern = new ParameterizedString("${user} (Code Review)");
       generator =
-          new UserGen(accountCache, domainPattern, anonymousCowardName, namePattern, srvAddr);
+          userAddressGenFactory.create(
+              accountCache, domainPattern, anonymousCowardName, namePattern, srvAddr);
     } else if ("SERVER".equalsIgnoreCase(from)) {
       generator = new ServerGen(srvAddr);
     } else {
diff --git a/java/com/google/gerrit/server/patch/ApplyPatchUtil.java b/java/com/google/gerrit/server/patch/ApplyPatchUtil.java
index a319a11..11471c5 100644
--- a/java/com/google/gerrit/server/patch/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/patch/ApplyPatchUtil.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.patch.DiffUtil;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
diff --git a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java b/java/com/google/gerrit/server/project/DefaultLockManager.java
similarity index 71%
rename from java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
rename to java/com/google/gerrit/server/project/DefaultLockManager.java
index 762e244..ab1148e 100644
--- a/java/com/google/gerrit/server/project/DefaultProjectNameLockManager.java
+++ b/java/com/google/gerrit/server/project/DefaultLockManager.java
@@ -15,28 +15,26 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.util.concurrent.Striped;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.inject.AbstractModule;
 import com.google.inject.Singleton;
 import java.util.concurrent.locks.Lock;
 
-/** In-memory lock for project names. */
+/** In-memory lock manager */
 @Singleton
-public class DefaultProjectNameLockManager implements ProjectNameLockManager {
+public class DefaultLockManager implements LockManager {
 
-  public static class DefaultProjectNameLockManagerModule extends AbstractModule {
+  public static class DefaultLockManagerModule extends AbstractModule {
     @Override
     protected void configure() {
-      DynamicItem.bind(binder(), ProjectNameLockManager.class)
-          .to(DefaultProjectNameLockManager.class);
+      DynamicItem.bind(binder(), LockManager.class).to(DefaultLockManager.class);
     }
   }
 
   Striped<Lock> locks = Striped.lock(10);
 
   @Override
-  public Lock getLock(Project.NameKey name) {
+  public Lock getLock(String name) {
     return locks.get(name);
   }
 }
diff --git a/java/com/google/gerrit/server/project/ProjectNameLockManager.java b/java/com/google/gerrit/server/project/LockManager.java
similarity index 67%
rename from java/com/google/gerrit/server/project/ProjectNameLockManager.java
rename to java/com/google/gerrit/server/project/LockManager.java
index f67dd04..8a85b32 100644
--- a/java/com/google/gerrit/server/project/ProjectNameLockManager.java
+++ b/java/com/google/gerrit/server/project/LockManager.java
@@ -14,17 +14,19 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.entities.Project;
 import java.util.concurrent.locks.Lock;
 
 /**
- * A per-repo lock mechanism.
- *
- * <p>This ensures that project creation (repo creation, config creation, first commit) is atomic,
- * and can be used to separate creation and deletion in the delete-project plugin.
+ * A global locking mechanism.
  *
  * <p>This is an interface because distributed setup may need something beyond an in-memory lock.
+ *
+ * <p>A Gerrit system consisting of a single Gerrit server only needs an in-memory lock manager
+ * which is implemented by the DefaultLockManager.
+ *
+ * <p>A distributed setup, consisting of more than one Gerrit server, can implement a distributed
+ * lock manager that provides global locks.
  */
-public interface ProjectNameLockManager {
-  public Lock getLock(Project.NameKey name);
+public interface LockManager {
+  public Lock getLock(String name);
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index d598739..9b85582 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -236,6 +236,7 @@
   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_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);
@@ -1108,6 +1109,9 @@
                     "count=%d is not allowed. Maximum allowed value for count is %d.",
                     count, LabelPredicate.MAX_COUNT));
           }
+        } else if (key.equalsIgnoreCase(ARG_USERS)) {
+          throw new QueryParseException(
+              String.format("Cannot use the '%s' argument in search", ARG_USERS));
         } else {
           throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
         }
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index cb92ddd..55d3505 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -28,13 +28,16 @@
 import com.google.gerrit.server.submitrequirement.predicate.RegexAuthorEmailPredicate;
 import com.google.gerrit.server.submitrequirement.predicate.RegexCommitterEmailPredicate;
 import com.google.gerrit.server.submitrequirement.predicate.RegexUploaderEmailPredicateFactory;
+import com.google.gerrit.server.submitrequirement.predicate.SubmitRequirementLabelExtensionPredicate;
 import com.google.inject.Inject;
+import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /**
  * A query builder for submit requirement expressions that includes all {@link ChangeQueryBuilder}
@@ -48,6 +51,8 @@
       new QueryBuilder.Definition<>(SubmitRequirementChangeQueryBuilder.class);
 
   private final DistinctVotersPredicate.Factory distinctVotersPredicateFactory;
+  private final SubmitRequirementLabelExtensionPredicate.Factory
+      submitRequirementLabelExtensionPredicateFactory;
   private final HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory;
 
   /**
@@ -70,11 +75,15 @@
   SubmitRequirementChangeQueryBuilder(
       Arguments args,
       DistinctVotersPredicate.Factory distinctVotersPredicateFactory,
+      SubmitRequirementLabelExtensionPredicate.Factory
+          submitRequirementLabelExtensionPredicateFactory,
       FileEditsPredicate.Factory fileEditsPredicateFactory,
       HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory,
       RegexUploaderEmailPredicateFactory regexUploaderEmailPredicateFactory) {
     super(def, args);
     this.distinctVotersPredicateFactory = distinctVotersPredicateFactory;
+    this.submitRequirementLabelExtensionPredicateFactory =
+        submitRequirementLabelExtensionPredicateFactory;
     this.fileEditsPredicateFactory = fileEditsPredicateFactory;
     this.hasSubmoduleUpdateFactory = hasSubmoduleUpdateFactory;
     this.regexUploaderEmailPredicateFactory = regexUploaderEmailPredicateFactory;
@@ -150,6 +159,16 @@
     return distinctVotersPredicateFactory.create(value);
   }
 
+  @Override
+  public Predicate<ChangeData> label(String value)
+      throws QueryParseException, IOException, ConfigInvalidException {
+    if (SubmitRequirementLabelExtensionPredicate.matches(value)) {
+      return submitRequirementLabelExtensionPredicateFactory.create(value);
+    }
+    SubmitRequirementLabelExtensionPredicate.validateIfNoMatch(value);
+    return super.label(value);
+  }
+
   /**
    * A SR operator that can match with file path and content pattern. The value should be of the
    * form:
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java b/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
index a55ef84b..4e05420 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyProvidedFix.java
@@ -16,10 +16,12 @@
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 
+import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.entities.Comment.Range;
 import com.google.gerrit.entities.FixReplacement;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
 import com.google.gerrit.extensions.common.ApplyProvidedFixInput;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,15 +29,19 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditJson;
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.edit.CommitModification;
+import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
 import com.google.gerrit.server.fixes.FixReplacementInterpreter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.ApplyPatchUtil;
+import com.google.gerrit.server.patch.MagicFile;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -45,7 +51,14 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.patch.PatchApplier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
 
 /** Applies a fix that is provided as part of the request body. */
 @Singleton
@@ -74,7 +87,7 @@
   public Response<EditInfo> apply(
       RevisionResource revisionResource, ApplyProvidedFixInput applyProvidedFixInput)
       throws AuthException, BadRequestException, ResourceConflictException, IOException,
-          ResourceNotFoundException, PermissionBackendException {
+          ResourceNotFoundException, PermissionBackendException, RestApiException {
     if (applyProvidedFixInput == null) {
       throw new BadRequestException("applyProvidedFixInput is required");
     }
@@ -83,9 +96,19 @@
     }
     Project.NameKey project = revisionResource.getProject();
     ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
-    PatchSet patchSet = revisionResource.getPatchSet();
+    PatchSet targetPatchSet = revisionResource.getPatchSet();
 
     ChangeNotes changeNotes = revisionResource.getNotes();
+    PatchSet originPatchSetForFix =
+        applyProvidedFixInput.originalPatchsetForFix != null
+                && applyProvidedFixInput.originalPatchsetForFix > 0
+            ? changeNotes
+                .getPatchSets()
+                .get(
+                    PatchSet.id(
+                        revisionResource.getChange().getId(),
+                        applyProvidedFixInput.originalPatchsetForFix))
+            : targetPatchSet;
 
     List<FixReplacement> fixReplacements =
         applyProvidedFixInput.fixReplacementInfos.stream()
@@ -94,15 +117,87 @@
 
     try (Repository repository = gitRepositoryManager.openRepository(project)) {
       CommitModification commitModification =
-          fixReplacementInterpreter.toCommitModification(
-              repository, projectState, patchSet.commitId(), fixReplacements);
+          getCommitModification(
+              repository, projectState, originPatchSetForFix, targetPatchSet, fixReplacements);
       ChangeEdit changeEdit =
           changeEditModifier.combineWithModifiedPatchSetTree(
-              repository, changeNotes, patchSet, commitModification);
+              repository, changeNotes, targetPatchSet, commitModification);
 
       return Response.ok(changeEditJson.toEditInfo(changeEdit, false));
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
     }
   }
+
+  /**
+   * Returns CommitModification for fixes and rebase it if the fix is for an older patchset.
+   *
+   * <p>The method creates CommitModification by applying {@code fixReplacements} to the {@code
+   * basePatchSetForFix}. If the {@code targetPatchSetForFix} is different from the {@code
+   * basePatchSetForFix}, CommitModification is created from the {@link PatchApplier.Result}, after
+   * applying the patch generated from {@code basePatchSetForFix} to the {@code
+   * targetPatchSetForFix}.
+   *
+   * <p>Note: if there is a fix for a commit message and commit messages are different in {@code
+   * basePatchSetForFix} and {@code targetPatchSetForFix}, the method can't move the fix to the
+   * {@code targetPatchSetForFix} and throws {@link ResourceConflictException}. This limitations
+   * exists because the method uses ApplyPatchUtil which operates only on files.
+   */
+  private CommitModification getCommitModification(
+      Repository repository,
+      ProjectState projectState,
+      PatchSet basePatchSetForFix,
+      PatchSet targetPatchSetForFix,
+      List<FixReplacement> fixReplacements)
+      throws IOException, InvalidChangeOperationException, RestApiException {
+    CommitModification originCommitModification =
+        fixReplacementInterpreter.toCommitModification(
+            repository, projectState, basePatchSetForFix.commitId(), fixReplacements);
+    if (basePatchSetForFix.id().equals(targetPatchSetForFix.id())) {
+      return originCommitModification;
+    }
+    RevCommit originCommit = repository.parseCommit(basePatchSetForFix.commitId());
+    ObjectId newTreeId =
+        ChangeEditModifier.createNewTree(
+            repository, originCommit, originCommitModification.treeModifications());
+    CommitModification.Builder resultBuilder = CommitModification.builder();
+    String patch;
+    try (RevWalk rw = new RevWalk(repository)) {
+      ObjectId targetCommit = targetPatchSetForFix.commitId();
+      if (originCommitModification.newCommitMessage().isPresent()) {
+        MagicFile originCommitMessageFile =
+            MagicFile.forCommitMessage(rw.getObjectReader(), originCommit);
+        String originCommitMessage = originCommitMessageFile.modifiableContent();
+        MagicFile targetCommitMessageFile =
+            MagicFile.forCommitMessage(rw.getObjectReader(), targetCommit);
+        String targetCommitMessage = targetCommitMessageFile.modifiableContent();
+        if (!originCommitMessage.equals(targetCommitMessage)) {
+          throw new ResourceConflictException(
+              "The fix attempts to modify commit message of an older patchset, but commit message has been updated in a newer patchset. The fix can't be applied.");
+        }
+        resultBuilder.newCommitMessage(originCommitModification.newCommitMessage().get());
+      }
+
+      patch =
+          ApplyPatchUtil.getResultPatch(
+              repository, repository.newObjectReader(), originCommit, rw.lookupTree(newTreeId));
+      PatchApplier.Result result;
+      try (ObjectInserter oi = repository.newObjectInserter()) {
+        ApplyPatchInput inp = new ApplyPatchInput();
+        inp.patch = patch;
+        inp.allowConflicts = false;
+        result =
+            ApplyPatchUtil.applyPatch(repository, oi, inp, repository.parseCommit(targetCommit));
+        oi.flush();
+        for (String path : result.getPaths()) {
+          try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, result.getTreeId())) {
+            ObjectLoader loader = rw.getObjectReader().open(tw.getObjectId(0));
+            resultBuilder.addTreeModification(
+                new ChangeFileContentModification(path, RawInputUtil.create(loader.getBytes())));
+          }
+        }
+      }
+    }
+    return resultBuilder.build();
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/Fixes.java b/java/com/google/gerrit/server/restapi/change/Fixes.java
index 080c1e7..1d0b8df 100644
--- a/java/com/google/gerrit/server/restapi/change/Fixes.java
+++ b/java/com/google/gerrit/server/restapi/change/Fixes.java
@@ -60,6 +60,9 @@
     allComments.addAll(
         commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().id()));
     for (Comment comment : allComments) {
+      if (comment.fixSuggestions == null) {
+        continue;
+      }
       for (FixSuggestion fixSuggestion : comment.fixSuggestions) {
         if (Objects.equals(fixId, fixSuggestion.fixId)) {
           return new FixResource(revisionResource, fixSuggestion.replacements);
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewFix.java b/java/com/google/gerrit/server/restapi/change/PreviewFix.java
index e771898..042f73d 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewFix.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewFix.java
@@ -128,6 +128,11 @@
       if (applyProvidedFixInput.fixReplacementInfos == null) {
         throw new BadRequestException("applyProvidedFixInput.fixReplacementInfos is required");
       }
+      if (applyProvidedFixInput.originalPatchsetForFix != null
+          && applyProvidedFixInput.originalPatchsetForFix > 0) {
+        throw new BadRequestException(
+            "applyProvidedFixInput.originalPatchsetForFix is not supported on preview.");
+      }
 
       PreviewFix previewFix = previewFixFactory.create(revisionResource);
 
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
index 9fb8de8..ad35b37 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChangeEdit.java
@@ -14,42 +14,70 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.api.changes.RebaseChangeEditInput;
+import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditJson;
 import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Repository;
 
 @Singleton
-public class RebaseChangeEdit implements RestModifyView<ChangeResource, Input> {
+public class RebaseChangeEdit implements RestModifyView<ChangeResource, RebaseChangeEditInput> {
   private final GitRepositoryManager repositoryManager;
   private final ChangeEditModifier editModifier;
+  private final ChangeEditUtil editUtil;
+  private final ChangeEditJson editJson;
 
   @Inject
-  RebaseChangeEdit(GitRepositoryManager repositoryManager, ChangeEditModifier editModifier) {
+  RebaseChangeEdit(
+      GitRepositoryManager repositoryManager,
+      ChangeEditModifier editModifier,
+      ChangeEditUtil editUtil,
+      ChangeEditJson editJson) {
     this.repositoryManager = repositoryManager;
     this.editModifier = editModifier;
+    this.editUtil = editUtil;
+    this.editJson = editJson;
   }
 
   @Override
-  public Response<Object> apply(ChangeResource rsrc, Input in)
+  public Response<EditInfo> apply(ChangeResource rsrc, RebaseChangeEditInput input)
       throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
+    if (input == null) {
+      input = new RebaseChangeEditInput();
+    }
+
     Project.NameKey project = rsrc.getProject();
     try (Repository repository = repositoryManager.openRepository(project)) {
-      editModifier.rebaseEdit(repository, rsrc.getNotes());
+      CodeReviewCommit rebasedChangeEditCommit =
+          editModifier.rebaseEdit(repository, rsrc.getNotes(), input);
+
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
+      checkState(edit.isPresent(), "change edit missing after rebase");
+      EditInfo editInfo = editJson.toEditInfo(edit.get(), /* downloadCommands= */ false);
+      if (!rebasedChangeEditCommit.getFilesWithGitConflicts().isEmpty()) {
+        editInfo.containsGitConflicts = true;
+      }
+      return Response.ok(editInfo);
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
     }
-    return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index bc47adc..2803c0e 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
@@ -25,6 +27,8 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -48,15 +52,17 @@
 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.update.BatchUpdate;
 import com.google.gerrit.server.util.AttentionSetUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -76,6 +82,7 @@
   private final ServiceUserClassifier serviceUserClassifier;
   private final CommentsUtil commentsUtil;
   private final DraftCommentsReader draftCommentsReader;
+  private final ProjectCache projectCache;
 
   @Inject
   ReplyAttentionSetUpdates(
@@ -86,7 +93,8 @@
       AccountResolver accountResolver,
       ServiceUserClassifier serviceUserClassifier,
       CommentsUtil commentsUtil,
-      DraftCommentsReader draftCommentsReader) {
+      DraftCommentsReader draftCommentsReader,
+      ProjectCache projectCache) {
     this.permissionBackend = permissionBackend;
     this.addToAttentionSetOpFactory = addToAttentionSetOpFactory;
     this.removeFromAttentionSetOpFactory = removeFromAttentionSetOpFactory;
@@ -95,6 +103,7 @@
     this.serviceUserClassifier = serviceUserClassifier;
     this.commentsUtil = commentsUtil;
     this.draftCommentsReader = draftCommentsReader;
+    this.projectCache = projectCache;
   }
 
   /** Adjusts the attention set but only based on the automatic rules. */
@@ -235,7 +244,7 @@
 
     addOwnerAndUploaderToAttentionSetIfSomeoneElseReplied(
         bu, postReviewOp, changeNotes, currentUser, readyForReview, allNewComments);
-    addAllAuthorsOfCommentThreads(bu, changeNotes, allNewComments);
+    addAllAuthorsOfCommentThreads(bu, changeNotes, allNewComments, currentUser);
   }
 
   /**
@@ -329,17 +338,71 @@
 
   /** Adds all authors of all comment threads that received a reply during this update */
   private void addAllAuthorsOfCommentThreads(
-      BatchUpdate bu, ChangeNotes changeNotes, ImmutableSet<HumanComment> allNewComments) {
-    List<HumanComment> publishedComments = commentsUtil.publishedHumanCommentsByChange(changeNotes);
-    ImmutableSet<CommentThread<HumanComment>> repliedToCommentThreads =
-        CommentThreads.forComments(publishedComments).getThreadsForChildren(allNewComments);
+      BatchUpdate bu,
+      ChangeNotes changeNotes,
+      ImmutableSet<HumanComment> allNewComments,
+      CurrentUser currentUser) {
+    boolean isOwnerOrUploader =
+        currentUser.getAccountId().equals(changeNotes.getChange().getOwner())
+            || currentUser.getAccountId().equals(changeNotes.getCurrentPatchSet().uploader());
 
-    ImmutableSet<Account.Id> repliedToUsers =
-        repliedToCommentThreads.stream()
-            .map(CommentThread::comments)
-            .flatMap(Collection::stream)
-            .map(comment -> comment.author.getId())
-            .collect(toImmutableSet());
+    boolean noCRLabel = false;
+    Optional<LabelValue> maxCRValue =
+        projectCache
+            .get(changeNotes.getChange().getProject())
+            .orElseThrow(
+                () ->
+                    new IllegalStateException(
+                        String.format(
+                            "Couldn't find project \"%s\" for a change \"%s\"",
+                            changeNotes.getChange().getProject(), changeNotes.getChangeId())))
+            .getLabelTypes(changeNotes)
+            .byLabel(LabelId.CODE_REVIEW)
+            .map(l -> l.getMax());
+
+    ImmutableSet<Account.Id> maxCrApprovers;
+    if (maxCRValue.isPresent()) {
+      maxCrApprovers =
+          changeNotes.getApprovals().all().get(changeNotes.getCurrentPatchSet().id()).stream()
+              .filter(
+                  a ->
+                      a.label().equals(LabelId.CODE_REVIEW)
+                          && a.value() == maxCRValue.get().getValue())
+              .map(a -> a.accountId())
+              .collect(toImmutableSet());
+    } else {
+      noCRLabel = true;
+      maxCrApprovers = ImmutableSet.of();
+    }
+
+    // Include newly published comments, when building threads.
+    ImmutableList<HumanComment> relevantComments =
+        Stream.concat(
+                commentsUtil.publishedHumanCommentsByChange(changeNotes).stream(),
+                allNewComments.stream())
+            .collect(toImmutableList());
+    ImmutableSet<CommentThread<HumanComment>> repliedToCommentThreads =
+        CommentThreads.forComments(relevantComments).getThreadsForChildren(allNewComments);
+
+    LinkedHashSet<Account.Id> repliedToUsers = new LinkedHashSet<>();
+    for (CommentThread<HumanComment> thread : repliedToCommentThreads) {
+      // If thread is resolved, we only bring back the commenters who have not yet left max
+      // Code-Review vote.
+      // If Owner replied but didn't resolve, we assume clarification was asked add everyone on the
+      // thread to attention set.
+      boolean ignoreVoteCheck = noCRLabel || (thread.unresolved() && isOwnerOrUploader);
+      if (thread.unresolved() && !isOwnerOrUploader) {
+        // Reviewer replied. Owner is still the one to act. No need to add commenters.
+        continue;
+      }
+      thread.comments().stream()
+          .map(comment -> comment.author.getId())
+          .filter(
+              a ->
+                  !a.equals(currentUser.getAccountId())
+                      && (ignoreVoteCheck || !maxCrApprovers.contains(a)))
+          .forEach(repliedToUsers::add);
+    }
     ImmutableSet<Account.Id> possibleUsersToAdd = approvalsUtil.getReviewers(changeNotes).all();
     SetView<Account.Id> usersToAdd = Sets.intersection(possibleUsersToAdd, repliedToUsers);
 
diff --git a/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java b/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java
index 1955511..98cf7e3 100644
--- a/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java
+++ b/java/com/google/gerrit/server/restapi/config/GetIndexVersion.java
@@ -17,6 +17,7 @@
 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.index.Index;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.server.config.IndexVersionResource;
 import com.google.gerrit.server.restapi.config.IndexInfo.IndexVersionInfo;
@@ -27,9 +28,10 @@
   public Response<IndexVersionInfo> apply(IndexVersionResource rsrc)
       throws ResourceNotFoundException {
     IndexCollection<?, ?, ?> indexCollection = rsrc.getIndexDefinition().getIndexCollection();
-    int version = rsrc.getIndex().getSchema().getVersion();
+    Index<?, ?> i = rsrc.getIndex();
+    int version = i.getSchema().getVersion();
     boolean isSearch = indexCollection.getSearchIndex().getSchema().getVersion() == version;
     boolean isWrite = indexCollection.getWriteIndex(version) != null;
-    return Response.ok(IndexInfo.IndexVersionInfo.create(isWrite, isSearch));
+    return Response.ok(IndexInfo.IndexVersionInfo.create(isWrite, isSearch, i.numDocs()));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/IndexInfo.java b/java/com/google/gerrit/server/restapi/config/IndexInfo.java
index 5d52fe1..9fba0034 100644
--- a/java/com/google/gerrit/server/restapi/config/IndexInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/IndexInfo.java
@@ -28,15 +28,18 @@
       String name, IndexCollection<?, ?, ?> indexCollection) {
     ImmutableSortedMap.Builder<Integer, IndexVersionInfo> versions =
         ImmutableSortedMap.naturalOrder();
-    int searchIndexVersion = indexCollection.getSearchIndex().getSchema().getVersion();
+    Index<?, ?> searchIndex = indexCollection.getSearchIndex();
+    int searchIndexVersion = searchIndex.getSchema().getVersion();
     boolean searchIndexAdded = false;
     for (Index<?, ?> index : indexCollection.getWriteIndexes()) {
       boolean isSearchIndex = index.getSchema().getVersion() == searchIndexVersion;
-      versions.put(index.getSchema().getVersion(), IndexVersionInfo.create(true, isSearchIndex));
+      versions.put(
+          index.getSchema().getVersion(),
+          IndexVersionInfo.create(true, isSearchIndex, index.numDocs()));
       searchIndexAdded = searchIndexAdded || isSearchIndex;
     }
     if (!searchIndexAdded) {
-      versions.put(searchIndexVersion, IndexVersionInfo.create(false, true));
+      versions.put(searchIndexVersion, IndexVersionInfo.create(false, true, searchIndex.numDocs()));
     }
 
     return new AutoValue_IndexInfo(name, versions.build());
@@ -52,12 +55,14 @@
 
   @AutoValue
   public abstract static class IndexVersionInfo {
-    static IndexVersionInfo create(boolean write, boolean search) {
-      return new AutoValue_IndexInfo_IndexVersionInfo(write, search);
+    static IndexVersionInfo create(boolean write, boolean search, int numDocs) {
+      return new AutoValue_IndexInfo_IndexVersionInfo(write, search, numDocs);
     }
 
     abstract boolean isWrite();
 
     abstract boolean isSearch();
+
+    abstract int numDocs();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 8be96b5..6d6d34e 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -45,10 +45,10 @@
 import com.google.gerrit.server.plugincontext.PluginItemContext;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.LockManager;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCreator;
 import com.google.gerrit.server.project.ProjectJson;
-import com.google.gerrit.server.project.ProjectNameLockManager;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
@@ -78,7 +78,7 @@
   private final Provider<PutConfig> putConfig;
   private final AllProjectsName allProjects;
   private final AllUsersName allUsers;
-  private final PluginItemContext<ProjectNameLockManager> lockManager;
+  private final PluginItemContext<LockManager> lockManager;
   private final ProjectCreator projectCreator;
 
   private final Config gerritConfig;
@@ -94,7 +94,7 @@
       Provider<PutConfig> putConfig,
       AllProjectsName allProjects,
       AllUsersName allUsers,
-      PluginItemContext<ProjectNameLockManager> lockManager,
+      PluginItemContext<LockManager> lockManager,
       @GerritServerConfig Config gerritConfig) {
     this.projectsCollection = projectsCollection;
     this.projectCreator = projectCreator;
@@ -167,7 +167,7 @@
       throw new BadRequestException(e.getMessage());
     }
 
-    Lock nameLock = lockManager.call(lockManager -> lockManager.getLock(args.getProject()));
+    Lock nameLock = lockManager.call(lockManager -> lockManager.getLock(args.getProject().get()));
     nameLock.lock();
     try {
       try {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
index 33f0b58..b942ec8 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
@@ -21,6 +21,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
@@ -562,7 +563,18 @@
 
   private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
     return StreamSupport.stream(scan().spliterator(), false)
-        .map(projectCache::get)
+        .map(
+            key -> {
+              try {
+                return projectCache.get(key);
+              } catch (StorageException e) {
+                if (Throwables.getCausalChain(e).stream().anyMatch(IOException.class::isInstance)) {
+                  logger.atSevere().log(
+                      "Unable to load project %s : %s", key.get(), e.getCause().getMessage());
+                }
+                return Optional.<ProjectState>empty();
+              }
+            })
         .filter(Optional::isPresent)
         .map(Optional::get)
         .filter(p -> permissionCheck(p, perm));
diff --git a/java/com/google/gerrit/server/submitrequirement/predicate/SubmitRequirementLabelExtensionPredicate.java b/java/com/google/gerrit/server/submitrequirement/predicate/SubmitRequirementLabelExtensionPredicate.java
new file mode 100644
index 0000000..389c7f4
--- /dev/null
+++ b/java/com/google/gerrit/server/submitrequirement/predicate/SubmitRequirementLabelExtensionPredicate.java
@@ -0,0 +1,213 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.submitrequirement.predicate;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.base.Enums;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+import com.google.gerrit.server.query.change.LabelPredicate;
+import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Extensions of the {@link LabelPredicate} that are only available for submit requirement
+ * expressions, but not for search.
+ *
+ * <p>Supported extensions:
+ *
+ * <ul>
+ *   <li>"users=human_reviewers" arg, e.g. "label:Code-Review=MAX,users=human_reviewers" matches
+ *       changes where all human reviewers have approved the change with Code-Review=MAX
+ * </ul>
+ */
+public class SubmitRequirementLabelExtensionPredicate extends SubmitRequirementPredicate {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public interface Factory {
+    SubmitRequirementLabelExtensionPredicate create(String value) throws QueryParseException;
+  }
+
+  private static final Pattern PATTERN = Pattern.compile("(?<label>[^,]*),users=human_reviewers$");
+  private static final Pattern PATTERN_LABEL =
+      Pattern.compile("(?<label>[^,<>=]*)(?<op>=|<=|>=|<|>)(?<value>[^,]*)");
+
+  public static boolean matches(String value) {
+    return PATTERN.matcher(value).matches();
+  }
+
+  public static void validateIfNoMatch(String value) throws QueryParseException {
+    if (value.contains(",users=")) {
+      throw new QueryParseException(
+          "Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+              + " group')");
+    }
+  }
+
+  private final Arguments args;
+  private final ServiceUserClassifier serviceUserClassifier;
+  private final String label;
+
+  @Inject
+  SubmitRequirementLabelExtensionPredicate(
+      Arguments args, ServiceUserClassifier serviceUserClassifier, @Assisted String value)
+      throws QueryParseException {
+    super("label", value);
+    this.args = args;
+    this.serviceUserClassifier = serviceUserClassifier;
+
+    Matcher m = PATTERN.matcher(value);
+    if (!m.matches()) {
+      throw new QueryParseException(
+          String.format("invalid value for '%s': %s", getOperator(), value));
+    }
+    this.label = validateLabel(m.group("label"));
+  }
+
+  @CanIgnoreReturnValue
+  private String validateLabel(String label) throws QueryParseException {
+    int eq = label.indexOf('=');
+
+    if (eq <= 0) {
+      return label;
+    }
+
+    String statusName = label.substring(eq + 1).toUpperCase(Locale.US);
+    SubmitRecord.Label.Status status =
+        Enums.getIfPresent(SubmitRecord.Label.Status.class, statusName).orNull();
+    if (status != null) {
+      // We would need to use SubmitRecordPredicate but can't because it doesn't implement
+      // Matchable.
+      throw new QueryParseException(
+          "Cannot use the 'users=human_reviewers' argument in conjunction with a submit record"
+              + " label status");
+    }
+    return label;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    if (!cd.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER).isEmpty()
+        && !matchZeroVotes(label)) {
+      // Reviewers by email are reviewers that don't have a Gerrit account. Without Gerrit
+      // account they cannot vote on the change, which means changes that have any such
+      // reviewers never match when we expect a vote != 0 from all reviewers.
+      logger.atFine().log(
+          "change %s doesn't match since there are reviewers by email"
+              + " (that don't have a matching approval): %s",
+          cd.change().getChangeId(), cd.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER));
+      return false;
+    }
+
+    ImmutableSet<Account.Id> humanReviewers =
+        cd.reviewers().byState(ReviewerStateInternal.REVIEWER).stream()
+            // Ignore the change owner (if the change owner voted on their own change they are
+            // technically a reviewer).
+            .filter(accountId -> !accountId.equals(cd.change().getOwner()))
+            // Ignore reviewers that are service users.
+            .filter(accountId -> !serviceUserClassifier.isServiceUser(accountId))
+            .collect(toImmutableSet());
+
+    if (humanReviewers.isEmpty()) {
+      // a review from human reviewers is required, but no human reviewers are present
+      return false;
+    }
+
+    for (Account.Id reviewer : humanReviewers) {
+      if (!new LabelPredicate(
+              args,
+              label,
+              ImmutableSet.of(reviewer),
+              /* group= */ null,
+              /* count= */ null,
+              /* countOp= */ null)
+          .match(cd)) {
+        logger.atFine().log(
+            "change %s doesn't match because it misses matching approvals from: %s",
+            cd.change().getChangeId(), reviewer);
+        return false;
+      }
+    }
+
+    logger.atFine().log(
+        "change %s matches because it has matching approvals from all human reviewers: %s",
+        cd.change().getChangeId(), humanReviewers);
+    return true;
+  }
+
+  private boolean matchZeroVotes(String label) {
+    Matcher m = PATTERN_LABEL.matcher(label);
+    if (!m.matches()) {
+      return false;
+    }
+
+    String op = m.group("op");
+    String value = m.group("value");
+
+    Optional<Integer> intValue = Optional.ofNullable(Ints.tryParse(value));
+
+    if (op.equals("=") && (intValue.isPresent() && intValue.get() == 0)) {
+      return true;
+    } else if (op.equals("<=")) {
+      if (intValue.isPresent() && intValue.get() >= 0) {
+        return true;
+      } else if (value.equals("MAX")) {
+        return true;
+      }
+      return false;
+    } else if (op.equals("<")) {
+      if (intValue.isPresent() && intValue.get() > 0) {
+        return true;
+      } else if (value.equals("MAX")) {
+        return true;
+      }
+    } else if (op.equals(">=")) {
+      if (intValue.isPresent() && intValue.get() <= 0) {
+        return true;
+      } else if (value.equals("MIN")) {
+        return true;
+      }
+      return false;
+    } else if (op.equals(">")) {
+      if (intValue.isPresent() && intValue.get() < 0) {
+        return true;
+      } else if (value.equals("MIN")) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 8e065b9..789655f 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.server.Sequence.LightweightGroups;
 import com.google.gerrit.server.account.AccountCacheImpl;
 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;
 import com.google.gerrit.server.account.storage.notedb.AccountNoteDbWriteStorageModule;
 import com.google.gerrit.server.api.GerritApiModule;
@@ -103,6 +104,7 @@
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier.SignedTokenEmailTokenVerifierModule;
+import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
 import com.google.gerrit.server.notedb.NoteDbDraftCommentsModule;
 import com.google.gerrit.server.notedb.NoteDbStarredChangesModule;
 import com.google.gerrit.server.notedb.RepoSequence.DisabledGitRefUpdatedRepoGroupsSequenceProvider;
@@ -110,7 +112,7 @@
 import com.google.gerrit.server.patch.DiffExecutor;
 import com.google.gerrit.server.permissions.DefaultPermissionBackendModule;
 import com.google.gerrit.server.plugins.ServerInformationImpl;
-import com.google.gerrit.server.project.DefaultProjectNameLockManager.DefaultProjectNameLockManagerModule;
+import com.google.gerrit.server.project.DefaultLockManager.DefaultLockManagerModule;
 import com.google.gerrit.server.restapi.RestApiModule;
 import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.schema.SchemaCreator;
@@ -207,7 +209,10 @@
     install(cfgInjector.getInstance(GerritGlobalModule.class));
     install(new AccountNoteDbWriteStorageModule());
     install(new AccountNoteDbReadStorageModule());
+    install(new ExternalIdCacheImpl.ExternalIdCacheModule());
+    install(new ExternalIdCacheImpl.ExternalIdCacheBindingModule());
     install(new RepoSequenceModule());
+    install(new FromAddressGeneratorProvider.UserAddressGenModule());
     install(new NoteDbDraftCommentsModule());
     install(new NoteDbStarredChangesModule());
     install(new ChangeCleanupRunnerModule());
@@ -297,7 +302,7 @@
     bind(ServerInformation.class).to(ServerInformationImpl.class);
     install(new RestApiModule());
     install(new OAuthRestModule());
-    install(new DefaultProjectNameLockManagerModule());
+    install(new DefaultLockManagerModule());
     install(new FileInfoJsonModule());
     install(new ConfigExperimentFeaturesModule());
 
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 7804a43..55a2023 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -3157,6 +3157,26 @@
         .isNotEqualTo(updatedUserState.account().metaId());
   }
 
+  @Test
+  public void accountUpdate_emptyStringsToUnset() throws Exception {
+    AccountState preUpdateState = accountCache.get(admin.id()).get();
+    requestScopeOperations.setApiUser(admin.id());
+
+    accountsUpdateProvider
+        .get()
+        .update(
+            "Replace External ID",
+            admin.id(),
+            u -> u.setFullName("").setDisplayName("").setPreferredEmail("").setStatus(""));
+
+    AccountState updatedState = accountCache.get(admin.id()).get();
+    assertThat(accountCache.get(admin.id()).get()).isNotSameInstanceAs(preUpdateState);
+    assertThat(updatedState.account().fullName()).isNull();
+    assertThat(updatedState.account().displayName()).isNull();
+    assertThat(updatedState.account().preferredEmail()).isNull();
+    assertThat(updatedState.account().status()).isNull();
+  }
+
   protected ExternalId createEmailExternalId(Account.Id accountId, String email) {
     return getExternalIdFactory().createWithEmail(SCHEME_MAILTO, email, accountId, email);
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index a5d86ce..d7d2f26 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -637,6 +637,252 @@
   }
 
   @Test
+  public void submitRequirement_wantCodeReviewFromHumanReviewers() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, 2))
+        .update();
+
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("Code-Review")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("label:Code-Review=MAX"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("Want-Code-Review-From-All")
+            .setApplicabilityExpression(
+                Optional.of(
+                    SubmitRequirementExpression.create(
+                        "-label:Code-Review>=1,users=human_reviewers")))
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("label:Code-Review>=1,users=human_reviewers"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+
+    // Code-Review is unsatisfied because there is no Code-Review+2 approval.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Code-Review",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    // Want-Code-Review-From-All is unsatisfied (since a review from reviewers is required but no
+    // reviewer is present on the change).
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    // Add some reviewers
+    TestAccount reviewer1 = accountCreator.create("reviewer1");
+    gApi.changes().id(changeId).addReviewer("reviewer1");
+    TestAccount reviewer2 = accountCreator.create("reviewer2");
+    gApi.changes().id(changeId).addReviewer("reviewer2");
+
+    // Code-Review is unsatisfied because there is no Code-Review+2 approval.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Code-Review",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    // Want-Code-Review-From-All is unsatisfied since there are reviewers on the change that
+    // didn't approve it yet.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    requestScopeOperations.setApiUser(reviewer1.id());
+    voteLabel(changeId, "Code-Review", 2);
+
+    // Code-Review is satisfied because there is Code-Review+2 approval from reviewer1.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Code-Review",
+        Status.SATISFIED,
+        /* isLegacy= */ false);
+
+    // Want-Code-Review-From-All is unsatisfied since there is no approval from reviewer2.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    requestScopeOperations.setApiUser(reviewer2.id());
+    voteLabel(changeId, "Code-Review", 2);
+
+    // Code-Review is satisfied because there are Code-Review+2 approvals from reviewer1 and
+    // reviewer2.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Code-Review",
+        Status.SATISFIED,
+        /* isLegacy= */ false);
+
+    // Want-Code-Review-From-All is not applicable since there are approval from all reviewers
+    // (reviewer1 and reviewer2) which makes "label:Code-Review=MAX,users=human_reviewers"
+    // satisfied.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.NOT_APPLICABLE,
+        /* isLegacy= */ false);
+
+    // Add another reviewer
+    TestAccount reviewer3 = accountCreator.create("reviewer3");
+    gApi.changes().id(changeId).addReviewer("reviewer3");
+
+    // Want-Code-Review-From-All is unsatisfied because reviewer3 didn't vote yet.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    // Vote with Code-Review+1 by reviewer3.
+    requestScopeOperations.setApiUser(reviewer3.id());
+    voteLabel(changeId, "Code-Review", 1);
+
+    // Want-Code-Review-From-All is not applicable because all reviewers voted with Code-Review >=
+    // 1.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.NOT_APPLICABLE,
+        /* isLegacy= */ false);
+  }
+
+  @Test
+  public void submitRequirement_wantCodeReviewFromHumanReviewers_enabledByFooter()
+      throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, 2))
+        .update();
+
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("Code-Review")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("label:Code-Review=MAX"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("Want-Code-Review-From-All")
+            .setApplicabilityExpression(
+                Optional.of(
+                    SubmitRequirementExpression.create(
+                        "footer:\"Want-Code-Review: all\" -label:Code-Review>=1,users=human_reviewers")))
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("label:Code-Review>=1,users=human_reviewers"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+
+    // Add some reviewers
+    TestAccount reviewer1 = accountCreator.create("reviewer1");
+    gApi.changes().id(changeId).addReviewer("reviewer1");
+    TestAccount reviewer2 = accountCreator.create("reviewer2");
+    gApi.changes().id(changeId).addReviewer("reviewer2");
+
+    // Approve by one reviewer
+    requestScopeOperations.setApiUser(reviewer1.id());
+    voteLabel(changeId, "Code-Review", 2);
+
+    // Code-Review is satisfied because there is Code-Review+2 approval from reviewer1.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Code-Review",
+        Status.SATISFIED,
+        /* isLegacy= */ false);
+
+    // Want-Code-Review-From-All is not applicable since the commit message doesn't contain a
+    // "Want-Code-Review: all" footer.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.NOT_APPLICABLE,
+        /* isLegacy= */ false);
+
+    // Amend the change to add a "Want-Code-Review: all" footer.
+    amendChange(
+        changeId,
+        PushOneCommit.SUBJECT
+            + "\n\nSome Description\n\nChange-Id: "
+            + changeId
+            + "\nWant-Code-Review: all\n",
+        PushOneCommit.FILE_NAME,
+        "content");
+
+    // Re-Approve by reviewer1.
+    requestScopeOperations.setApiUser(reviewer1.id());
+    voteLabel(changeId, "Code-Review", 2);
+
+    // Want-Code-Review-From-All is applicable since there is a "Want-Code-Review: all" footer and
+    // it is unsatisfied since there is no approval from reviewer2.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    // Approve by reviewer2.
+    requestScopeOperations.setApiUser(reviewer2.id());
+    voteLabel(changeId, "Code-Review", 2);
+
+    // Want-Code-Review-From-All is not applicable since there are approval from all reviewers
+    // (reviewer1 and reviewer2) which makes "label:Code-Review=MAX,users=human_reviewers"
+    // satisfied.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.NOT_APPLICABLE,
+        /* isLegacy= */ false);
+
+    // Add another reviewer
+    TestAccount reviewer3 = accountCreator.create("reviewer3");
+    gApi.changes().id(changeId).addReviewer("reviewer3");
+
+    // Want-Code-Review-From-All is unsatisfied because reviewer3 didn't vote yet.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.UNSATISFIED,
+        /* isLegacy= */ false);
+
+    // Vote with Code-Review+1 by reviewer3.
+    requestScopeOperations.setApiUser(reviewer3.id());
+    voteLabel(changeId, "Code-Review", 1);
+
+    // Want-Code-Review-From-All is not applicable because all reviewers voted with Code-Review >=
+    // 1.
+    assertSubmitRequirementStatus(
+        gApi.changes().id(changeId).get().submitRequirements,
+        "Want-Code-Review-From-All",
+        Status.NOT_APPLICABLE,
+        /* isLegacy= */ false);
+  }
+
+  @Test
   public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsFulfilled()
       throws Exception {
     configSubmitRequirement(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
index 8643489..61a06a3 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -39,13 +40,19 @@
 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.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.ReviewInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
+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;
@@ -467,6 +474,373 @@
     assertMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
   }
 
+  @Test
+  public void label_requireVoteFromHumanReviewers() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, 2))
+        .update();
+
+    Account.Id owner = accountCreator.create("owner").id();
+    Account.Id reviewer1 = accountCreator.create("reviewer1").id();
+    Account.Id reviewer2 = accountCreator.create("reviewer2").id();
+    Account.Id reviewer3 = accountCreator.create("reviewer3").id();
+
+    Account.Id serviceUser = accountCreator.create("serviceUser").id();
+    gApi.groups().id(ServiceUserClassifier.SERVICE_USERS).addMembers(serviceUser.toString());
+
+    Change.Id changeApprovedByAllReviewers =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(project, changeApprovedByAllReviewers, reviewer1, reviewer2, reviewer3);
+    addReviews(
+        project,
+        changeApprovedByAllReviewers,
+        ReviewInput.approve(),
+        reviewer1,
+        reviewer2,
+        reviewer3);
+
+    Change.Id changeApprovedBySomeReviewers =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(project, changeApprovedBySomeReviewers, reviewer1, reviewer2, reviewer3);
+    addReviews(project, changeApprovedBySomeReviewers, ReviewInput.approve(), reviewer1, reviewer2);
+
+    Change.Id changeRecommendedByAllReviewers =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(project, changeRecommendedByAllReviewers, reviewer1, reviewer2, reviewer3);
+    addReviews(
+        project,
+        changeRecommendedByAllReviewers,
+        ReviewInput.recommend(),
+        reviewer1,
+        reviewer2,
+        reviewer3);
+
+    Change.Id changeRecommendedBySomeReviewers =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(project, changeRecommendedBySomeReviewers, reviewer1, reviewer2, reviewer3);
+    addReviews(
+        project, changeRecommendedBySomeReviewers, ReviewInput.recommend(), reviewer1, reviewer2);
+
+    Change.Id changeNoVotesByReviewers =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(project, changeNoVotesByReviewers, reviewer1, reviewer2, reviewer3);
+
+    Change.Id changeWithoutReviewers =
+        changeOperations.newChange().project(project).owner(owner).create();
+
+    requestScopeOperations.setApiUser(user.id());
+
+    // change without reviewers doesn't match
+    assertNotMatching("label:Code-Review=MAX,users=human_reviewers", changeWithoutReviewers);
+
+    // match changes where all reviewers have the same vote
+    assertRequirement(
+        "label:Code-Review=MAX,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+    assertRequirement(
+        "label:Code-Review=2,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+    assertRequirement(
+        "label:Code-Review=1,users=human_reviewers",
+        ImmutableList.of(changeRecommendedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeApprovedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+
+    // match changes where no reviewer voted (same as "label:Code-Review=0")
+    assertRequirement(
+        "label:Code-Review=0,users=human_reviewers",
+        ImmutableList.of(changeNoVotesByReviewers),
+        ImmutableList.of(
+            changeApprovedByAllReviewers,
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers));
+
+    // match changes where all reviewers have a vote <=, >=, < or >
+    assertRequirement(
+        "label:Code-Review<=2,users=human_reviewers",
+        ImmutableList.of(
+            changeApprovedByAllReviewers,
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers),
+        ImmutableList.of());
+    assertRequirement(
+        "label:Code-Review<=1,users=human_reviewers",
+        ImmutableList.of(
+            changeRecommendedByAllReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers),
+        ImmutableList.of(changeApprovedByAllReviewers));
+    assertRequirement(
+        "label:Code-Review>=1,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers, changeRecommendedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+    assertRequirement(
+        "label:Code-Review<1,users=human_reviewers",
+        ImmutableList.of(changeNoVotesByReviewers),
+        ImmutableList.of(
+            changeApprovedByAllReviewers,
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers));
+    assertRequirement(
+        "label:Code-Review>1,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+
+    // match changes where all reviewers have any (non-zero) vote
+    assertRequirement(
+        "label:Code-Review=ANY,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers, changeRecommendedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+
+    // votes of the change owners are ignored (as the change owner is not considered as a reviewer)
+    addReviews(project, changeApprovedByAllReviewers, ReviewInput.dislike(), owner);
+    assertRequirement(
+        "label:Code-Review=MAX,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+
+    // missing votes from service users are fine
+    addReviewers(project, changeApprovedByAllReviewers, serviceUser);
+    assertRequirement(
+        "label:Code-Review=MAX,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+
+    // votes from service users are ignored
+    addReviews(project, changeApprovedByAllReviewers, ReviewInput.dislike(), serviceUser);
+    assertRequirement(
+        "label:Code-Review=MAX,users=human_reviewers",
+        ImmutableList.of(changeApprovedByAllReviewers),
+        ImmutableList.of(
+            changeApprovedBySomeReviewers,
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            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);
+    Change.Id changeRecommendedByAllReviewersWithReviewersByEmail =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(
+        project,
+        changeRecommendedByAllReviewersWithReviewersByEmail,
+        reviewer1,
+        reviewer2,
+        reviewer3);
+    addReviews(
+        project,
+        changeRecommendedByAllReviewersWithReviewersByEmail,
+        ReviewInput.recommend(),
+        reviewer1,
+        reviewer2,
+        reviewer3);
+    addReviewer(
+        project,
+        changeRecommendedByAllReviewersWithReviewersByEmail,
+        "email-without-account@example.com");
+    Change.Id changeNoVotesByReviewersWithReviewersByEmail =
+        changeOperations.newChange().project(project).owner(owner).create();
+    addReviewers(
+        project, changeNoVotesByReviewersWithReviewersByEmail, reviewer1, reviewer2, reviewer3);
+    addReviewer(
+        project, changeNoVotesByReviewersWithReviewersByEmail, "email-without-account@example.com");
+    assertRequirement(
+        "label:Code-Review=MAX,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review=2,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review=ANY,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review=0,users=human_reviewers",
+        ImmutableList.of(changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of(changeRecommendedByAllReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review<=2,users=human_reviewers",
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of());
+    assertRequirement(
+        "label:Code-Review<=0,users=human_reviewers",
+        ImmutableList.of(changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of(changeRecommendedByAllReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review<=-1,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review<2,users=human_reviewers",
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of());
+    assertRequirement(
+        "label:Code-Review<1,users=human_reviewers",
+        ImmutableList.of(changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of(changeRecommendedByAllReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review<0,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review>=0,users=human_reviewers",
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of());
+    assertRequirement(
+        "label:Code-Review>=1,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+    assertRequirement(
+        "label:Code-Review>-1,users=human_reviewers",
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail),
+        ImmutableList.of());
+    assertRequirement(
+        "label:Code-Review>0,users=human_reviewers",
+        ImmutableList.of(),
+        ImmutableList.of(
+            changeRecommendedByAllReviewersWithReviewersByEmail,
+            changeNoVotesByReviewersWithReviewersByEmail));
+
+    // cannot combine users=human_reviewers" with submit record status
+    assertError(
+        "label:Code-Review=ok,users=human_reviewers",
+        changeApprovedByAllReviewers,
+        "Cannot use the 'users=human_reviewers' argument in conjunction with a submit record label"
+            + " status");
+
+    // cannot combine "users" arg with a "user" arg
+    assertError(
+        "label:Code-Review=MAX,users=human_reviewers,user=reviewer1",
+        changeApprovedByAllReviewers,
+        "Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+            + " group')");
+
+    // cannot combine "users" arg with a "group" arg
+    assertError(
+        "label:Code-Review=MAX,users=human_reviewers,group=foo",
+        changeApprovedByAllReviewers,
+        "Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+            + " group')");
+
+    // cannot combine "users" arg with a positional arg
+    assertError(
+        "label:Code-Review=MAX,users=human_reviewers,reviewer1",
+        changeApprovedByAllReviewers,
+        "Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+            + " group')");
+    assertError(
+        "label:Code-Review=MAX,reviewer1,users=human_reviewers",
+        changeApprovedByAllReviewers,
+        "Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+            + " group')");
+
+    // label without "users=human_reviewers" still works
+    assertRequirement(
+        "label:Code-Review=MAX,user=reviewer1",
+        ImmutableList.of(changeApprovedByAllReviewers, changeApprovedBySomeReviewers),
+        ImmutableList.of(
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+    assertRequirement(
+        "label:Code-Review=MAX,reviewer1",
+        ImmutableList.of(changeApprovedByAllReviewers, changeApprovedBySomeReviewers),
+        ImmutableList.of(
+            changeRecommendedByAllReviewers,
+            changeRecommendedBySomeReviewers,
+            changeNoVotesByReviewers));
+  }
+
+  private void addReviewers(Project.NameKey project, Change.Id changeId, Account.Id... reviewers)
+      throws Exception {
+    for (Account.Id reviewer : reviewers) {
+      addReviewer(project, changeId, reviewer.toString());
+    }
+  }
+
+  private void addReviewer(Project.NameKey project, Change.Id changeId, String reviewer)
+      throws Exception {
+    gApi.changes().id(project.get(), changeId.get()).addReviewer(reviewer);
+  }
+
+  private void addReviews(
+      Project.NameKey project, Change.Id changeId, ReviewInput reviewInput, Account.Id... reviewers)
+      throws Exception {
+    for (Account.Id reviewer : reviewers) {
+      requestScopeOperations.setApiUser(reviewer);
+      gApi.changes().id(project.get(), changeId.get()).current().review(reviewInput);
+    }
+  }
+
   private void approveAsUser(String changeId, Account.Id userId) throws Exception {
     requestScopeOperations.setApiUser(userId);
     approve(changeId);
@@ -540,13 +914,28 @@
     return threeWayMerger.getResultTreeId();
   }
 
+  private void assertRequirement(
+      String requirement,
+      ImmutableList<Change.Id> matchingChanges,
+      ImmutableList<Change.Id> nonMatchingChanges) {
+    for (Change.Id matchingChange : matchingChanges) {
+      assertMatching(requirement, matchingChange);
+    }
+
+    for (Change.Id nonMatchingChange : nonMatchingChanges) {
+      assertNotMatching(requirement, nonMatchingChange);
+    }
+  }
+
   private void assertMatching(String requirement, Change.Id change) {
-    assertThat(evaluate(requirement, change).status())
+    assertWithMessage("requirement \"%s\" doesn't match change %s", requirement, change)
+        .that(evaluate(requirement, change).status())
         .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
   }
 
   private void assertNotMatching(String requirement, Change.Id change) {
-    assertThat(evaluate(requirement, change).status())
+    assertWithMessage("requirement \"%s\" matches change %s", requirement, change)
+        .that(evaluate(requirement, change).status())
         .isEqualTo(SubmitRequirementExpressionResult.Status.FAIL);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
index b9ef0bf..d66a0a5 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
@@ -357,6 +357,142 @@
   }
 
   @Test
+  public void applyProvidedFixesOnNewerPatchsetWithModifiedFile() throws Exception {
+    // Remember patch set and add another one.
+    int previousRevision = gApi.changes().id(changeId).get().currentRevisionNumber;
+    amendChange(
+        changeId,
+        "refs/for/master",
+        admin,
+        testRepo,
+        PushOneCommit.SUBJECT,
+        FILE_NAME,
+        "New line at the start\n" + FILE_CONTENT);
+    ApplyProvidedFixInput applyProvidedFixInput =
+        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
+    applyProvidedFixInput.originalPatchsetForFix = previousRevision;
+    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
+
+    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+    BinaryResultSubject.assertThat(file)
+        .value()
+        .asString()
+        .isEqualTo(
+            "New line at the start\nFirst line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
+                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+
+    applyProvidedFixInput = createApplyProvidedFixInput(FILE_NAME, "(1st)", 1, 5, 1, 5);
+    applyProvidedFixInput.originalPatchsetForFix = previousRevision;
+    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
+
+    file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+    BinaryResultSubject.assertThat(file)
+        .value()
+        .asString()
+        .isEqualTo(
+            "New line at the start\nFirst(1st) 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 applyProvidedFixWithTwoFilesOnNewerPatchsetWithModifiedFile() throws Exception {
+    // Remember patch set and add another one.
+    int previousRevision = gApi.changes().id(changeId).get().currentRevisionNumber;
+    // Remove first 2 lines;
+    String modifiedContent =
+        FILE_CONTENT.substring(FILE_CONTENT.indexOf("\n", FILE_CONTENT.indexOf("\n") + 1) + 1);
+    amendChange(
+        changeId,
+        "refs/for/master",
+        admin,
+        testRepo,
+        PushOneCommit.SUBJECT,
+        FILE_NAME,
+        modifiedContent);
+    amendChange(
+        changeId,
+        "refs/for/master",
+        admin,
+        testRepo,
+        PushOneCommit.SUBJECT,
+        FILE_NAME2,
+        "New line at the start\n" + FILE_CONTENT2);
+    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+    fixReplacementInfo1.path = FILE_NAME;
+    fixReplacementInfo1.range = createRange(10, 0, 10, 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";
+
+    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
+
+    applyProvidedFixInput.fixReplacementInfos =
+        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
+    applyProvidedFixInput.originalPatchsetForFix = previousRevision;
+
+    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
+
+    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+    BinaryResultSubject.assertThat(file)
+        .value()
+        .asString()
+        .isEqualTo(
+            "Third line\nFourth line\nFifth line\n"
+                + "Sixth line\nSeventh line\nEighth line\nNinth line\nFirst modification\nTenth line\n");
+    Optional<BinaryResult> file2 = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
+    BinaryResultSubject.assertThat(file2)
+        .value()
+        .asString()
+        .isEqualTo("New line at the start\nDifferent file modification\n2nd line\n3rd line\n");
+  }
+
+  @Test
+  public void applyProvidedFixOnCommitMessageCanBeAppliedToNewerPatchset() 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();
+    int previousRevision = gApi.changes().id(changeId).get().currentRevisionNumber;
+    // Upload a new patchset with the same commit message.
+    amendChange(changeId, originalCommitMessage, FILE_NAME, "a" + FILE_CONTENT);
+    ApplyProvidedFixInput applyProvidedFixInput =
+        createApplyProvidedFixInput(Patch.COMMIT_MSG, "Modified line\n", 7, 0, 8, 0);
+    applyProvidedFixInput.originalPatchsetForFix = previousRevision;
+
+    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
+    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+    assertThat(commitMessage).isEqualTo("Modified line\nLine 2 of commit message\n" + footer);
+  }
+
+  @Test
+  public void applyProvidedFixOnCommitMessageRejectedIfNewerPatchsetHasDifferentCommitMessage()
+      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();
+    int previousRevision = gApi.changes().id(changeId).get().currentRevisionNumber;
+    // Upload a new patchset with the same commit message.
+    amendChange(changeId, "a" + originalCommitMessage, FILE_NAME, "a" + FILE_CONTENT);
+    ApplyProvidedFixInput applyProvidedFixInput =
+        createApplyProvidedFixInput(Patch.COMMIT_MSG, "Modified line\n", 7, 0, 8, 0);
+    applyProvidedFixInput.originalPatchsetForFix = previousRevision;
+
+    ResourceConflictException thrown =
+        assertThrows(
+            ResourceConflictException.class,
+            () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("commit message has been updated in a newer patchset");
+  }
+
+  @Test
   public void applyProvidedFixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied()
       throws Exception {
     // Create an empty change edit.
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java
index c257e703..c5dba34 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PreviewProvidedFixIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
 import com.google.gerrit.extensions.common.FixReplacementInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import java.util.Arrays;
 import java.util.List;
@@ -169,6 +170,19 @@
   }
 
   @Test
+  public void previewFixForDifferentPatchset() throws Exception {
+    int previousRevision = gApi.changes().id(changeId).get().currentRevisionNumber;
+    amendChange(changeId);
+    ApplyProvidedFixInput applyProvidedFixInput =
+        createApplyProvidedFixInput(FILE_NAME, "Modified content\n", 1, 0, 2, 0);
+    applyProvidedFixInput.originalPatchsetForFix = previousRevision;
+
+    assertThrows(
+        BadRequestException.class,
+        () -> gApi.changes().id(changeId).current().getFixPreview(applyProvidedFixInput));
+  }
+
+  @Test
   public void previewFixForCommitMsg() throws Exception {
     String footer = "Change-Id: " + changeId;
     updateCommitMessage(
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 5322785d..10d1c77 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -45,6 +45,7 @@
 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;
@@ -1644,6 +1645,35 @@
   }
 
   @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;
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 9f673c8..8d86b1e 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestProjectInput;
 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.project.ProjectOperations;
@@ -54,6 +55,7 @@
 import com.google.gerrit.extensions.api.changes.FileContentInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.api.changes.RebaseChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.client.ChangeEditDetailOption;
@@ -72,8 +74,10 @@
 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.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;
@@ -105,8 +109,10 @@
   private static final String FILE_NAME3 = "foo3";
   private static final String FILE_NAME4 = "foo4";
   private static final int FILE_MODE = 100644;
-  private static final byte[] CONTENT_OLD = "bar".getBytes(UTF_8);
-  private static final byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
+  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";
+  private static final byte[] CONTENT_NEW = CONTENT_NEW_STR.getBytes(UTF_8);
   private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
   private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
   private static final String CONTENT_BINARY_ENCODED_NEW =
@@ -269,12 +275,145 @@
     Optional<EditInfo> originalEdit = getEdit(changeId2);
     assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.commitId().name());
     Timestamp beforeRebase = originalEdit.get().commit.committer.date;
-    gApi.changes().id(changeId2).edit().rebase();
+    EditInfo rebasedEdit = gApi.changes().id(changeId2).edit().rebase(new RebaseChangeEditInput());
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW);
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2);
-    Optional<EditInfo> rebasedEdit = getEdit(changeId2);
-    assertThat(rebasedEdit).value().baseRevision().isEqualTo(currentPatchSet.commitId().name());
-    assertThat(rebasedEdit).value().commit().committer().date().isNotEqualTo(beforeRebase);
+    assertThat(rebasedEdit).baseRevision().isEqualTo(currentPatchSet.commitId().name());
+    assertThat(rebasedEdit).commit().committer().date().isNotEqualTo(beforeRebase);
+  }
+
+  @Test
+  public void rebaseEditWithConflictsFails() throws Exception {
+    PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
+    createEmptyEditFor(changeId2);
+    gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
+
+    // add new patch set that touches the same file as the edit
+    addNewPatchSetWithModifiedFile(changeId2, FILE_NAME, new String(CONTENT_NEW2, UTF_8));
+
+    Optional<EditInfo> originalEdit = getEdit(changeId2);
+    assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.commitId().name());
+
+    MergeConflictException exception =
+        assertThrows(
+            MergeConflictException.class, () -> gApi.changes().id(changeId2).edit().rebase());
+    assertThat(exception)
+        .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.",
+                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();
+  }
+
+  @Test
+  @GerritConfig(name = "change.diff3ConflictView", value = "true")
+  public void rebaseEditWithConflictsAllowedUsingDiff3() 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"
+                    + "||||||| BASE\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_OLD_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();
+  }
+
+  @Test
+  public void rebaseEditWithConflictsAllowedNoConflicts() throws Exception {
+    PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
+    createEmptyEditFor(changeId2);
+    gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
+    addNewPatchSet(changeId2);
+    PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
+
+    Optional<EditInfo> originalEdit = getEdit(changeId2);
+    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(changeId2).edit().rebase(input);
+    ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW);
+    ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2);
+    assertThat(rebasedEdit).baseRevision().isEqualTo(currentPatchSet.commitId().name());
+    assertThat(rebasedEdit).commit().committer().date().isNotEqualTo(beforeRebase);
+    assertThat(rebasedEdit).containsGitConflicts().isFalse();
   }
 
   @Test
@@ -312,7 +451,7 @@
     Optional<EditInfo> originalEdit = getEdit(changeId2);
     assertThat(originalEdit).value().baseRevision().isEqualTo(previousPatchSet.commitId().name());
     Timestamp beforeRebase = originalEdit.get().commit.committer.date;
-    adminRestSession.post(urlRebase(changeId2)).assertNoContent();
+    adminRestSession.post(urlRebase(changeId2)).assertOK();
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW);
     ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2);
     Optional<EditInfo> rebasedEdit = getEdit(changeId2);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 92da64e..0f245bd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -130,6 +130,11 @@
   @Before
   public void setUp() {
     TimeUtil.setCurrentMillisSupplier(fakeClock);
+    projectOperations
+        .project(allProjects)
+        .forUpdate()
+        .add(allowLabel("Code-Review").ref("refs/heads/*").group(REGISTERED_USERS).range(-2, 2))
+        .update();
   }
 
   @Test
@@ -1402,10 +1407,51 @@
   }
 
   @Test
-  public void reviewAddsAllUsersInCommentThread() throws Exception {
+  public void ownerReplyResolvedAddsNonVotedInCommentThread() throws Exception {
     PushOneCommit.Result r = createChange();
     requestScopeOperations.setApiUser(user.id());
-    change(r).current().review(reviewWithComment());
+    ReviewInput ri = reviewWithComment();
+    ri.label("Code-Review", 2);
+    change(r).current().review(ri);
+
+    TestAccount user2 = accountCreator.user2();
+
+    requestScopeOperations.setApiUser(user2.id());
+    change(r)
+        .current()
+        .review(
+            reviewInReplyToComment(
+                Iterables.getOnlyElement(
+                        gApi.changes().id(r.getChangeId()).current().commentsAsList())
+                    .id));
+
+    change(r).attention(user.email()).remove(new AttentionSetInput("removal"));
+    requestScopeOperations.setApiUser(admin.id());
+    ri =
+        reviewInReplyToComment(
+            gApi.changes().id(r.getChangeId()).current().commentsAsList().get(1).id);
+    ri.comments.get(Patch.COMMIT_MSG).get(0).unresolved = false;
+    change(r).current().review(ri);
+
+    // First user already voted, no need to bring them back.
+    assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+
+    AttentionSetUpdate attentionSet =
+        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user2));
+    assertThat(attentionSet).hasAccountIdThat().isEqualTo(user2.id());
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+    assertThat(attentionSet)
+        .hasReasonThat()
+        .isEqualTo("Someone else replied on a comment you posted");
+  }
+
+  @Test
+  public void ownerReplyUnresolvedAddsAllUsersInCommentThread() throws Exception {
+    PushOneCommit.Result r = createChange();
+    requestScopeOperations.setApiUser(user.id());
+    ReviewInput ri = reviewWithComment();
+    ri.label("Code-Review", 2);
+    change(r).current().review(ri);
 
     TestAccount user2 = accountCreator.user2();
 
@@ -1443,6 +1489,71 @@
   }
 
   @Test
+  public void reviewerReplyUnresolvedAddsOnlyOwner() throws Exception {
+    PushOneCommit.Result r = createChange();
+    requestScopeOperations.setApiUser(user.id());
+    change(r).current().review(reviewWithComment());
+
+    TestAccount user2 = accountCreator.user2();
+    change(r).attention(admin.email()).remove(new AttentionSetInput("removal"));
+    requestScopeOperations.setApiUser(user2.id());
+    change(r)
+        .current()
+        .review(
+            reviewInReplyToComment(
+                Iterables.getOnlyElement(
+                        gApi.changes().id(r.getChangeId()).current().commentsAsList())
+                    .id));
+
+    // First user is not needed, owner haven't addressed their comment yet.
+    assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+
+    AttentionSetUpdate attentionSet =
+        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+    assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id());
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+    assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change");
+  }
+
+  @Test
+  public void reviewerReplyResolvedAddsNonVotedInCommentThread() throws Exception {
+    PushOneCommit.Result r = createChange();
+    requestScopeOperations.setApiUser(user.id());
+    ReviewInput ri = reviewWithComment();
+    ri.label("Code-Review", 2);
+    change(r).current().review(ri);
+
+    TestAccount user2 = accountCreator.user2();
+    requestScopeOperations.setApiUser(user2.id());
+    change(r)
+        .current()
+        .review(
+            reviewInReplyToComment(
+                Iterables.getOnlyElement(
+                        gApi.changes().id(r.getChangeId()).current().commentsAsList())
+                    .id));
+
+    TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
+    requestScopeOperations.setApiUser(user3.id());
+    ri =
+        reviewInReplyToComment(
+            gApi.changes().id(r.getChangeId()).current().commentsAsList().get(1).id);
+    ri.comments.get(Patch.COMMIT_MSG).get(0).unresolved = false;
+    change(r).current().review(ri);
+
+    // First user already voted, no need to bring them back.
+    assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+
+    AttentionSetUpdate attentionSet =
+        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user2));
+    assertThat(attentionSet).hasAccountIdThat().isEqualTo(user2.id());
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+    assertThat(attentionSet)
+        .hasReasonThat()
+        .isEqualTo("Someone else replied on a comment you posted");
+  }
+
+  @Test
   public void reviewAddsAllUsersInCommentThreadWhenOriginalCommentIsARobotComment()
       throws Exception {
     PushOneCommit.Result result = createChange();
@@ -3278,6 +3389,7 @@
     comment.message = "comment";
     comment.setUpdated(TimeUtil.now());
     comment.inReplyTo = id;
+    comment.unresolved = true;
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
     return reviewInput;
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
index 2a06900..c24c1f2 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -28,6 +28,8 @@
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import java.util.List;
@@ -39,6 +41,8 @@
   @Inject private ExtensionRegistry extensionRegistry;
   @Inject private IndexOperations.Change changeIndexOperations;
 
+  @Inject private ChangeIndexCollection changeIndexes;
+
   @Test
   @GerritConfig(name = "index.autoReindexIfStale", value = "false")
   public void indexChange() throws Exception {
@@ -109,4 +113,19 @@
       assertThat(ids).doesNotContain(change.getId().get());
     }
   }
+
+  @Test
+  public void testNumDocs() throws Exception {
+    testNumDocs(changeIndexes.getSearchIndex());
+    for (ChangeIndex i : changeIndexes.getWriteIndexes()) {
+      testNumDocs(i);
+    }
+  }
+
+  private void testNumDocs(ChangeIndex index) throws Exception {
+    int before = index.numDocs();
+    createChange("a change", "a.txt", "test");
+    int after = index.numDocs();
+    assertThat(after).isEqualTo(before + 1);
+  }
 }
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index b5c8a50..2f6d565 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -96,7 +96,7 @@
   }
 
   @Test
-  public void usePreloadRest() throws Exception {
+  public void usePreloadRestWithBasePatchNum() throws Exception {
     Accounts accountsApi = mock(Accounts.class);
     when(accountsApi.self()).thenThrow(new AuthException("user needs to be authenticated"));
 
@@ -114,12 +114,43 @@
     when(gerritApi.accounts()).thenReturn(accountsApi);
     when(gerritApi.config()).thenReturn(configApi);
 
-    assertThat(dynamicTemplateData(gerritApi, "/c/project/+/123", ""))
+    String requestedPath = "/c/project/+/123/4..6";
+    assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(4);
+
+    assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
         .containsAtLeast(
             "defaultChangeDetailHex", "9996394",
             "changeRequestsPath", "changes/project~123");
   }
 
+  @Test
+  public void usePreloadRestWithNoBasePatchNum() 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, ""))
+        .containsAtLeast(
+            "defaultChangeDetailHex", "1996394",
+            "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/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index 05965fb..14554c4 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -59,12 +59,12 @@
       Ref ref1 = createRefWithNonEmptyTreeCommit(usersRepo, 1, 1000001);
       Ref ref2 = createRefWithEmptyTreeCommit(usersRepo, 1, 1000002);
 
-      DeleteZombieCommentsRefs clean =
+      try (DeleteZombieCommentsRefs clean =
           new DeleteZombieCommentsRefs(
-              new AllUsersName("All-Users"), repoManager, null, (msg) -> {});
-      int deletedDrafts = clean.execute();
-      assertThat(deletedDrafts).isEqualTo(1);
-
+              new AllUsersName("All-Users"), repoManager, null, (msg) -> {})) {
+        int deletedDrafts = clean.execute();
+        assertThat(deletedDrafts).isEqualTo(1);
+      }
       /* Check that ref1 still exists, and ref2 is deleted */
       assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
       assertThat(usersRepo.exactRef(ref2.getName())).isNull();
@@ -81,21 +81,21 @@
       assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(3);
 
       int cleanupPercentage = 50;
-      DeleteZombieCommentsRefs clean =
+      try (DeleteZombieCommentsRefs clean =
           new DeleteZombieCommentsRefs(
-              new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {});
-      int deletedDrafts = clean.execute();
-      assertThat(deletedDrafts).isEqualTo(1);
+              new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {})) {
+        int deletedDrafts = clean.execute();
+        assertThat(deletedDrafts).isEqualTo(1);
+        /* ref1 not deleted, ref2 deleted, ref3 not deleted because of the clean percentage */
+        assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2);
+        assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
+        assertThat(usersRepo.exactRef(ref2.getName())).isNull();
+        assertThat(usersRepo.exactRef(ref3.getName())).isNotNull();
 
-      /* ref1 not deleted, ref2 deleted, ref3 not deleted because of the clean percentage */
-      assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2);
-      assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
-      assertThat(usersRepo.exactRef(ref2.getName())).isNull();
-      assertThat(usersRepo.exactRef(ref3.getName())).isNotNull();
-
-      /* Re-execute the cleanup and make sure nothing's changed */
-      deletedDrafts = clean.execute();
-      assertThat(deletedDrafts).isEqualTo(0);
+        /* Re-execute the cleanup and make sure nothing's changed */
+        deletedDrafts = clean.execute();
+        assertThat(deletedDrafts).isEqualTo(0);
+      }
       assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2);
       assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
       assertThat(usersRepo.exactRef(ref2.getName())).isNull();
@@ -103,13 +103,13 @@
 
       /* Increase the cleanup percentage */
       cleanupPercentage = 70;
-      clean =
+      try (DeleteZombieCommentsRefs clean =
           new DeleteZombieCommentsRefs(
-              new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {});
+              new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {})) {
 
-      deletedDrafts = clean.execute();
-      assertThat(deletedDrafts).isEqualTo(1);
-
+        int deletedDrafts = clean.execute();
+        assertThat(deletedDrafts).isEqualTo(1);
+      }
       /* Now ref3 is deleted */
       assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(1);
       assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
@@ -141,11 +141,12 @@
       assertThat(usersRepo.getRefDatabase().getRefs().size())
           .isEqualTo(goodRefs.size() + badRefs.size());
 
-      DeleteZombieCommentsRefs clean =
+      try (DeleteZombieCommentsRefs clean =
           new DeleteZombieCommentsRefs(
-              new AllUsersName("All-Users"), repoManager, null, (msg) -> {});
-      int deletedDrafts = clean.execute();
-      assertThat(deletedDrafts).isEqualTo(5001);
+              new AllUsersName("All-Users"), repoManager, null, (msg) -> {})) {
+        int deletedDrafts = clean.execute();
+        assertThat(deletedDrafts).isEqualTo(5001);
+      }
 
       assertThat(
               usersRepo.getRefDatabase().getRefs().stream()
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index 6e3514e..1a51c00 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -110,6 +110,11 @@
   }
 
   @Override
+  public int numDocs() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
       throws QueryParseException {
     return new FakeChangeIndex.Source(p);
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 629b0cc..8a8db44 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider.DefaultUserAddressGenFactory;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.util.Arrays;
 import java.util.List;
@@ -47,7 +48,9 @@
   }
 
   private FromAddressGenerator create() {
-    return new FromAddressGeneratorProvider(config, "Anonymous Coward", ident, accountCache).get();
+    return new FromAddressGeneratorProvider(
+            config, "Anonymous Coward", ident, accountCache, new DefaultUserAddressGenFactory())
+        .get();
   }
 
   private void setFrom(String newFrom) {
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index a0aad96..1a546fa 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1552,6 +1552,12 @@
   }
 
   @Test
+  public void cannotUseUsersArgWithLabel() throws Exception {
+    assertFailingQuery(
+        "label:Code-Review=MAX,users=human_reviewers", "Cannot use the 'users' argument in search");
+  }
+
+  @Test
   public void byLabelMulti() throws Exception {
     Project.NameKey project = Project.nameKey("repo");
     repo = createAndOpenProject(project);
diff --git a/plugins/hooks b/plugins/hooks
index 4f43f5d..83d5aae 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 4f43f5db6b8aa7f36381f4f9a4c9ec1fc335d949
+Subproject commit 83d5aae0fce1956858c1b5595012e68867c53437
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 86f7ec6..ed7870e 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 86f7ec61a9785df246f653a1336520b9607399b1
+Subproject commit ed7870eb3c8b6e48511d0eb3bd54606927b46019
diff --git a/plugins/replication b/plugins/replication
index 60693ef..90c6420 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 60693efae661105fb6278c16cae9502d29bfa7f3
+Subproject commit 90c64204e1e27ec245f41b3d08b10f431dd72faf
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 2c397e0..60bcf8a 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-access-section';
 import {
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 fe5aa22..750beab 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-admin-group-list';
 import {GrAdminGroupList} from './gr-admin-group-list';
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 cbac9de..34e8d24 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-admin-view';
 import {AdminSubsectionLink, GrAdminView} from './gr-admin-view';
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 d4b5f03..a30b0df 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-confirm-delete-item-dialog';
 import {GrConfirmDeleteItemDialog} from './gr-confirm-delete-item-dialog';
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 87916b6..d26df6e 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-create-change-dialog';
 import {GrCreateChangeDialog} from './gr-create-change-dialog';
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 e2da5d7..885e1cf 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-create-file-edit-dialog';
 import {createChange} from '../../../test/test-data-generators';
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 c5fbde3..407d015 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-create-group-dialog';
 import {GrCreateGroupDialog} from './gr-create-group-dialog';
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 c16a108..5dc4e42 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-group-members';
 import {GrGroupMembers, ItemType} from './gr-group-members';
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 5cf71f8..aaf8dfc 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-group';
 import {GrGroup} from './gr-group';
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 46f6ac4..2909694 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-permission';
 import {GrPermission} from './gr-permission';
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 672d58e..fea0d58 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {ConfigParameterInfoType} from '../../../constants/constants';
 import '../../../test/common-test-setup';
 import './gr-plugin-config-array-editor';
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 495de52..c321abf 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo-access';
 import {GrRepoAccess} from './gr-repo-access';
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 af2831a..deec717 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo-commands';
 import {GrRepoCommands} from './gr-repo-commands';
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 5e25b33..3dd5e69 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo-detail-list';
 import {GrRepoDetailList} from './gr-repo-detail-list';
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 36674f5..d7377aa 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo-list';
 import {GrRepoList} from './gr-repo-list';
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 3dc6f1e..1691b88 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo-plugin-config';
 import {GrRepoPluginConfig} from './gr-repo-plugin-config';
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 a4e0f09..e33fd04 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo';
 import {GrRepo} from './gr-repo';
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 8066289..650876d 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-rule-editor';
 import {GrRuleEditor} from './gr-rule-editor';
diff --git a/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts b/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts
index b67239e..836a5eb2 100644
--- a/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts
+++ b/polygerrit-ui/app/elements/admin/gr-server-info/gr-server-info.ts
@@ -55,12 +55,6 @@
         .genericList tr th:last-of-type {
           text-align: left;
         }
-        .metadataDescription,
-        .metadataName,
-        .metadataValue,
-        .metadataWebLinks {
-          white-space: nowrap;
-        }
         .placeholder {
           color: var(--deemphasized-text-color);
         }
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 15f022b..b91a0a0 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {createChange} from '../../../test/test-data-generators';
 import {
   NumericChangeId,
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 c65371b..df1b297 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {GrChangeListBulkVoteFlow} from './gr-change-list-bulk-vote-flow';
 import {
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 94ca74a..859a2ba 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * 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 {SinonStubbedMember} from 'sinon';
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 4f5a888..2722efe 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {fixture, assert} from '@open-wc/testing';
 import {html} from 'lit';
 import {
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 d2f5fa2..c494448 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {fixture, html, assert} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {
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 694d953..75a5b5c8 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {
   GrChangeListSection,
   computeLabelShortcut,
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 2934772..087b8a8 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * 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 {SinonStubbedMember} from 'sinon';
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 93a530d..d69eac8 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-change-list-view';
 import {GrChangeListView} from './gr-change-list-view';
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 c53f813..30d3455 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-change-list';
 import {GrChangeList, computeRelativeIndex} from './gr-change-list';
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 01ce0b6..9b09c84 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-dashboard-view';
 import {GrDashboardView} from './gr-dashboard-view';
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 ebcec4b..e122fd3 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-change-actions';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
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 9a78cd6..e96caa4 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
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-change-metadata';
 
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 7b9c291..ca3de4a 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../../edit/gr-edit-constants';
 import '../gr-thread-list/gr-thread-list';
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 3602ebc..e449332 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-confirm-abandon-dialog';
 import {GrConfirmAbandonDialog} from './gr-confirm-abandon-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 891175f..61cc227 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {fixture, html, assert} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {queryAndAssert} from '../../../utils/common-util';
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 ae55a4d..d3e48ca 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-confirm-cherrypick-dialog';
 import {
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 1c2a89d..ea58df9 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,6 +3,7 @@
  * 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';
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 920ff00..ad19f85 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {fixture, html, assert} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {createParsedChange} from '../../../test/test-data-generators';
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 37dd9aa..f2c536b 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {fixture, html, assert} from '@open-wc/testing';
 import './gr-copy-links';
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 c730671..18da58d 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {
   createChange,
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 e6005c2..8bf316f 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-file-list-header';
 import {FilesExpandedState} from '../gr-file-list-constants';
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 ca7ab14..a375e1d 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../../shared/gr-date-formatter/gr-date-formatter';
 import './gr-file-list';
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 8752a6c..7129247 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-label-score-row';
 import {GrLabelScoreRow} from './gr-label-score-row';
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 1dbacc1..afd8ac2 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-message';
 import {
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 68fbe8b..d4c7b63 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
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-messages-list';
 import {CombinedMessage, GrMessagesList, TEST_ONLY} from './gr-messages-list';
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 4f951f3..504c8b5 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
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-reply-dialog';
 import {
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 c76e928..e6717b2 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
@@ -1687,9 +1687,10 @@
 
     if (this.change.status === ChangeStatus.NEW) {
       // Add everyone that the user is replying to in a comment thread.
-      this.computeCommentAccounts(draftCommentThreads).forEach(id =>
-        newAttention.add(id)
-      );
+      this.computeCommentAccountsForAttention(
+        draftCommentThreads,
+        isUploader
+      ).forEach(id => newAttention.add(id));
       // Remove the current user.
       newAttention.delete(this.account._account_id);
       // Add all new reviewers, but not the current reviewer, if they are also
@@ -1756,18 +1757,48 @@
     return this.isOwner && addedIds.length >= minimum;
   }
 
-  computeCommentAccounts(threads: CommentThread[]) {
+  /**
+   * Pick previous commenters for addition to attention set.
+   *
+   * For every thread:
+   *   - If owner replied and thread is unresolved: add all commenters.
+   *   - If owner replied and thread is resolved: add commenters who need to vote.
+   *   - If reviewer replied and thread is resolved: add commenters who need to vote.
+   *   - If reviewer replied and thread is unresolved: only add owner
+   *     (owner added outside this function).
+   */
+  computeCommentAccountsForAttention(
+    threads: CommentThread[],
+    isUploader: boolean
+  ) {
     const crLabel = this.change?.labels?.[StandardLabels.CODE_REVIEW];
     const maxCrVoteAccountIds = getMaxAccounts(crLabel).map(a => a._account_id);
     const accountIds = new Set<AccountId>();
     threads.forEach(thread => {
       const unresolved = isUnresolved(thread);
+      let ignoreVoteCheck = false;
+      if (unresolved) {
+        if (this.isOwner || isUploader) {
+          // Owner replied but didn't resolve, we assume clarification was asked
+          // add everyone on the thread to attention set.
+          ignoreVoteCheck = true;
+        } else {
+          // Reviewer replied owner is still the one to act. No need to add
+          // commenters.
+          return;
+        }
+      }
+      // If thread is resolved, we only bring back the commenters who have not
+      // yet left max Code-Review vote.
       thread.comments.forEach(comment => {
         if (comment.author) {
           // A comment author must have an account_id.
           const authorId = comment.author._account_id!;
-          const hasGivenMaxReviewVote = maxCrVoteAccountIds.includes(authorId);
-          if (unresolved || !hasGivenMaxReviewVote) accountIds.add(authorId);
+          const needsToVote =
+            !maxCrVoteAccountIds.includes(authorId) && // Didn't give max-vote
+            this.uploader?._account_id !== authorId && // Not uploader
+            this.change?.owner._account_id !== authorId; // Not owner
+          if (ignoreVoteCheck || needsToVote) accountIds.add(authorId);
         }
       });
     });
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 7c8aece..eea645e 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-reply-dialog';
 import {
@@ -1161,7 +1162,72 @@
     );
   });
 
-  test('computeCommentAccounts', () => {
+  test('computeCommentAccountsForAttention owner comments', () => {
+    element.change = {
+      ...createChange(),
+      labels: {
+        'Code-Review': {
+          all: [
+            {_account_id: 1 as AccountId, value: 0},
+            {_account_id: 2 as AccountId, value: 1},
+            {_account_id: 3 as AccountId, value: 2},
+          ],
+          values: {
+            '-2': 'This shall not be submitted',
+            '-1': 'I would prefer this is not submitted as is',
+            ' 0': 'No score',
+            '+1': 'Looks good to me, but someone else must approve',
+            '+2': 'Looks good to me, approved',
+          },
+        },
+      },
+    };
+    element.isOwner = true;
+    const threads = [
+      {
+        ...createCommentThread([
+          {
+            ...createComment(),
+            id: '1' as UrlEncodedCommentId,
+            author: {_account_id: 1 as AccountId},
+            unresolved: false,
+          },
+          {
+            ...createComment(),
+            id: '2' as UrlEncodedCommentId,
+            in_reply_to: '1' as UrlEncodedCommentId,
+            author: {_account_id: 2 as AccountId},
+            unresolved: true,
+          },
+        ]),
+      },
+      {
+        ...createCommentThread([
+          {
+            ...createComment(),
+            id: '3' as UrlEncodedCommentId,
+            author: {_account_id: 3 as AccountId},
+            unresolved: false,
+          },
+          {
+            ...createComment(),
+            id: '4' as UrlEncodedCommentId,
+            in_reply_to: '3' as UrlEncodedCommentId,
+            author: {_account_id: 4 as AccountId},
+            unresolved: false,
+          },
+        ]),
+      },
+    ];
+    const actualAccounts = [
+      ...element.computeCommentAccountsForAttention(threads, false),
+    ];
+    // Account 3 is not included, because the comment is resolved *and* they
+    // have given the highest possible vote on the Code-Review label.
+    assert.sameMembers(actualAccounts, [1, 2, 4]);
+  });
+
+  test('computeCommentAccountsForAttention reviewer comments', () => {
     element.change = {
       ...createChange(),
       labels: {
@@ -1211,16 +1277,30 @@
             ...createComment(),
             id: '4' as UrlEncodedCommentId,
             in_reply_to: '3' as UrlEncodedCommentId,
+            author: element.change.owner,
+            unresolved: false,
+          },
+          {
+            ...createComment(),
+            id: '5' as UrlEncodedCommentId,
+            in_reply_to: '4' as UrlEncodedCommentId,
             author: {_account_id: 4 as AccountId},
             unresolved: false,
           },
         ]),
       },
     ];
-    const actualAccounts = [...element.computeCommentAccounts(threads)];
+    const actualAccounts = [
+      ...element.computeCommentAccountsForAttention(threads, false),
+    ];
+    // Accounts 1 and 2 are not included, because the thread is still unresolved
+    // and the new comment is from another reviewer.
     // Account 3 is not included, because the comment is resolved *and* they
     // have given the highest possible vote on the Code-Review label.
-    assert.sameMembers(actualAccounts, [1, 2, 4]);
+    // element.change.owner is similarly not included, because they don't need
+    // to vote. (In the overall logic owner is added as part of
+    // computeNewAttention)
+    assert.sameMembers(actualAccounts, [4]);
   });
 
   test('label picker', async () => {
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 f050ab50..0fcb83c 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-reviewer-list';
 import {mockPromise, queryAndAssert} from '../../../test/test-utils';
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 e91d9c9..1c8be76 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-fix-preview.ts
@@ -3,23 +3,12 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '../../embed/diff/gr-diff/gr-diff';
-import {css, html, LitElement, nothing, PropertyValues} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
-import {getAppContext} from '../../services/app-context';
-import {EDIT, BasePatchSetNum, RepoName} from '../../types/common';
-import {anyLineTooLong} from '../../utils/diff-util';
-import {Timing} from '../../constants/reporting';
-import {
-  DiffInfo,
-  DiffLayer,
-  DiffPreferencesInfo,
-  DiffViewMode,
-  RenderPreferences,
-} from '../../api/diff';
-import {GrSyntaxLayerWorker} from '../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
+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 {BasePatchSetNum, RepoName} from '../../types/common';
 import {resolve} from '../../models/dependency';
-import {highlightServiceToken} from '../../services/highlight/highlight-service';
 import {
   FixSuggestionInfo,
   NumericChangeId,
@@ -27,28 +16,13 @@
 } 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';
-import {userModelToken} from '../../models/user/user-model';
-import {navigationToken} from '../core/gr-navigation/gr-navigation';
 import {fire} from '../../utils/event-util';
-import {createChangeUrl} from '../../models/views/change';
 import {OpenFixPreviewEventDetail} from '../../types/events';
 
 /**
- * This component renders a <gr-diff> and an "apply fix" button and can be used
- * when showing check results that have a fix for an easy preview and a quick
- * apply-fix experience.
- *
- * There is a certain overlap with similar components for comment fixes:
- * GrSuggestionDiffPreview also renders a <gr-diff> and fetches a diff preview,
- * it relies on a `comment` (and the comment model) to be available. It supports
- * both a `string` fix and `FixSuggestionInfo`. It also differs in logging and
- * event handling. And it misses the header component that we need for the
- * buttons.
- *
- * There is also `GrUserSuggestionsFix` which wraps `GrSuggestionDiffPreview`
- * and has the header that we also need. But it is very targeted to be used for
- * user suggestions and inside comments.
+ * There is a certain overlap with `GrUserSuggestionsFix` which wraps
+ * `GrSuggestionDiffPreview` and has the header that we also need.
+ * But it is very targeted to be used for user suggestions and inside comments.
  *
  * So there is certainly an opportunity for cleanup and unification, but at the
  * time of component creation it did not feel wortwhile investing into this
@@ -56,6 +30,9 @@
  */
 @customElement('gr-checks-fix-preview')
 export class GrChecksFixPreview extends LitElement {
+  @query('gr-suggestion-diff-preview')
+  suggestionDiffPreview?: GrSuggestionDiffPreview;
+
   @property({type: Object})
   fixSuggestionInfo?: FixSuggestionInfo;
 
@@ -63,9 +40,6 @@
   patchSet?: PatchSetNumber;
 
   @state()
-  layers: DiffLayer[] = [];
-
-  @state()
   repo?: RepoName;
 
   @state()
@@ -74,37 +48,13 @@
   @state()
   latestPatchNum?: PatchSetNumber;
 
-  @state()
-  diff?: DiffPreview;
+  @state() previewLoaded = false;
 
   @state()
   applyingFix = false;
 
-  @state()
-  diffPrefs?: DiffPreferencesInfo;
-
-  @state()
-  renderPrefs: RenderPreferences = {
-    disable_context_control_buttons: true,
-    show_file_comment_button: false,
-    hide_line_length_indicator: true,
-  };
-
-  private readonly reporting = getAppContext().reportingService;
-
-  private readonly restApiService = getAppContext().restApiService;
-
   private readonly getChangeModel = resolve(this, changeModelToken);
 
-  private readonly getUserModel = resolve(this, userModelToken);
-
-  private readonly getNavigation = resolve(this, navigationToken);
-
-  private readonly syntaxLayer = new GrSyntaxLayerWorker(
-    resolve(this, highlightServiceToken),
-    () => getAppContext().reportingService
-  );
-
   constructor() {
     super();
     subscribe(
@@ -119,15 +69,6 @@
     );
     subscribe(
       this,
-      () => this.getUserModel().diffPreferences$,
-      diffPreferences => {
-        if (!diffPreferences) return;
-        this.diffPrefs = diffPreferences;
-        this.syntaxLayer.setEnabled(!!this.diffPrefs.syntax_highlighting);
-      }
-    );
-    subscribe(
-      this,
       () => this.getChangeModel().repo$,
       x => (this.repo = x)
     );
@@ -150,11 +91,6 @@
         .header .title {
           flex: 1;
         }
-        .diff-container {
-          border: 1px solid var(--border-color);
-          border-top: none;
-          border-bottom: none;
-        }
         .loading {
           border: 1px solid var(--border-color);
           padding: var(--spacing-xl);
@@ -163,12 +99,6 @@
     ];
   }
 
-  override willUpdate(changed: PropertyValues) {
-    if (changed.has('fixSuggestionInfo')) {
-      this.fetchDiffPreview().then(diff => (this.diff = diff));
-    }
-  }
-
   override render() {
     if (!this.fixSuggestionInfo) return nothing;
     return html`${this.renderHeader()}${this.renderDiff()}`;
@@ -185,7 +115,7 @@
             class="showFix"
             secondary
             flatten
-            .disabled=${!this.diff}
+            .disabled=${!this.previewLoaded}
             @click=${this.showFix}
           >
             Show fix side-by-side
@@ -207,48 +137,16 @@
   }
 
   private renderDiff() {
-    if (!this.diff) {
-      return html`<div class="loading">Loading fix preview ...</div>`;
-    }
-    const diff = this.diff.preview;
-    if (!anyLineTooLong(diff)) {
-      this.syntaxLayer.process(diff);
-    }
     return html`
-      <div class="diff-container">
-        <gr-diff
-          .prefs=${this.getDiffPrefs()}
-          .path=${this.diff.filepath}
-          .diff=${diff}
-          .layers=${this.layers}
-          .renderPrefs=${this.renderPrefs}
-          .viewMode=${DiffViewMode.UNIFIED}
-        ></gr-diff>
-      </div>
+      <gr-suggestion-diff-preview
+        .fixSuggestionInfo=${this.fixSuggestionInfo}
+        .patchSet=${this.patchSet}
+        .codeText=${'Loading fix preview ...'}
+        @preview-loaded=${() => (this.previewLoaded = true)}
+      ></gr-suggestion-diff-preview>
     `;
   }
 
-  /**
-   * Calls the REST API to convert the fix into a DiffInfo.
-   */
-  private async fetchDiffPreview(): Promise<DiffPreview | undefined> {
-    if (!this.changeNum || !this.patchSet || !this.fixSuggestionInfo) return;
-    const pathsToDiffs: {[path: string]: DiffInfo} | undefined =
-      await this.restApiService.getFixPreview(
-        this.changeNum,
-        this.patchSet,
-        this.fixSuggestionInfo.replacements
-      );
-
-    if (!pathsToDiffs) return;
-    const diffs = Object.keys(pathsToDiffs).map(filepath => {
-      const diff = pathsToDiffs[filepath];
-      return {filepath, preview: diff};
-    });
-    // Showing diff for one file only.
-    return diffs?.[0];
-  }
-
   private showFix() {
     if (!this.patchSet || !this.fixSuggestionInfo) return;
     const eventDetail: OpenFixPreviewEventDetail = {
@@ -268,63 +166,22 @@
     if (!changeNum || !basePatchNum || !this.fixSuggestionInfo) return;
 
     this.applyingFix = true;
-    this.reporting.time(Timing.APPLY_FIX_LOAD);
-    const res = await this.restApiService.applyFixSuggestion(
-      changeNum,
-      basePatchNum,
-      this.fixSuggestionInfo.replacements
-    );
-    this.applyingFix = false;
-    this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
-      method: '1-click',
-      description: this.fixSuggestionInfo.description,
-    });
-    if (res?.ok) this.navigateToEditPatchset();
-  }
-
-  private navigateToEditPatchset() {
-    const changeNum = this.changeNum;
-    const repo = this.repo;
-    const basePatchNum = this.patchSet;
-    if (!changeNum || !repo || !basePatchNum) return;
-
-    const url = createChangeUrl({
-      changeNum,
-      repo,
-      patchNum: EDIT,
-      basePatchNum,
-      // We have to force reload, because the EDIT patchset is otherwise not yet known.
-      forceReload: true,
-    });
-    this.getNavigation().setUrl(url);
-  }
-
-  /**
-   * We have to override some diff prefs of the user, because for example in the context of showing
-   * an inline diff for fixes we do not want to show context lines around the changes lines of code
-   * as we would normally do for a diff.
-   */
-  private getDiffPrefs() {
-    if (!this.diffPrefs) return undefined;
-    return {
-      ...this.diffPrefs,
-      context: 0,
-      line_length: Math.min(this.diffPrefs.line_length, 100),
-      line_wrapping: true,
-    };
+    try {
+      await this.suggestionDiffPreview?.applyFix();
+    } finally {
+      this.applyingFix = false;
+    }
   }
 
   private isApplyEditDisabled() {
-    if (!this.diff || this.patchSet === undefined) return true;
-    return this.patchSet !== this.latestPatchNum;
+    if (this.patchSet === undefined) return true;
+    return !this.previewLoaded;
   }
 
   private computeApplyFixTooltip() {
     if (this.patchSet === undefined) return '';
-    if (!this.diff) return 'Fix is still loading ...';
-    return this.patchSet !== this.latestPatchNum
-      ? 'You cannot apply this fix because it is from a previous patchset'
-      : '';
+    if (!this.previewLoaded) return 'Fix is still loading ...';
+    return '';
   }
 }
 
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 21fa5d6..7dbac27 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
@@ -3,11 +3,13 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import './gr-checks-results';
+import './gr-checks-fix-preview';
 import {html} from 'lit';
 import {fixture, assert} from '@open-wc/testing';
-import {createCheckFix, createDiff} from '../../test/test-data-generators';
+import {createCheckFix} from '../../test/test-data-generators';
 import {GrChecksFixPreview} from './gr-checks-fix-preview';
 import {rectifyFix} from '../../models/checks/checks-util';
 import {
@@ -15,12 +17,10 @@
   mockPromise,
   queryAndAssert,
   stubRestApi,
-  waitUntil,
 } from '../../test/test-utils';
 import {NumericChangeId, PatchSetNumber, RepoName} from '../../api/rest-api';
 import {FilePathToDiffInfoMap} from '../../types/common';
-import {testResolver} from '../../test/common-test-setup';
-import {navigationToken} from '../core/gr-navigation/gr-navigation';
+import {GrSuggestionDiffPreview} from '../shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 
 suite('gr-checks-fix-preview test', () => {
   let element: GrChecksFixPreview;
@@ -44,11 +44,6 @@
     await element.updateComplete;
   });
 
-  const loadDiff = async () => {
-    promise.resolve({'foo.c': createDiff()});
-    await waitUntil(() => !!element.diff);
-  };
-
   test('renders loading', async () => {
     assert.shadowDom.equal(
       element,
@@ -83,58 +78,14 @@
             </gr-button>
           </div>
         </div>
-        <div class="loading">Loading fix preview ...</div>
-      `
-    );
-  });
-
-  test('renders diff', async () => {
-    await loadDiff();
-    assert.shadowDom.equal(
-      element,
-      /* HTML */ `
-        <div class="header">
-          <div class="title">
-            <span> Attached Fix </span>
-          </div>
-          <div>
-            <gr-button
-              class="showFix"
-              aria-disabled="false"
-              flatten=""
-              role="button"
-              secondary=""
-              tabindex="0"
-            >
-              Show fix side-by-side
-            </gr-button>
-            <gr-button
-              class="applyFix"
-              aria-disabled="false"
-              flatten=""
-              primary=""
-              role="button"
-              tabindex="0"
-              title=""
-            >
-              Apply fix
-            </gr-button>
-          </div>
-        </div>
-        <div class="diff-container">
-          <gr-diff
-            class="disable-context-control-buttons hide-line-length-indicator"
-            style="--line-limit-marker: 100ch; --content-width: none; --diff-max-width: none; --font-size: 12px;"
-          >
-          </gr-diff>
-        </div>
+        <gr-suggestion-diff-preview></gr-suggestion-diff-preview>
       `
     );
   });
 
   test('show-fix', async () => {
-    await loadDiff();
-
+    element.previewLoaded = true;
+    await element.updateComplete;
     const stub = sinon.stub();
     element.addEventListener('open-fix-preview', stub);
 
@@ -151,9 +102,13 @@
   });
 
   test('apply-fix', async () => {
-    await loadDiff();
-
-    const setUrlSpy = sinon.stub(testResolver(navigationToken), 'setUrl');
+    element.previewLoaded = true;
+    await element.updateComplete;
+    const diffPreview = queryAndAssert<GrSuggestionDiffPreview>(
+      element,
+      'gr-suggestion-diff-preview'
+    );
+    const applyFixSpy = sinon.spy(diffPreview, 'applyFix');
     stubRestApi('applyFixSuggestion').returns(
       Promise.resolve({ok: true} as Response)
     );
@@ -162,10 +117,6 @@
     assert.isFalse(button.hasAttribute('disabled'));
     button.click();
 
-    await waitUntil(() => setUrlSpy.called);
-    assert.equal(
-      setUrlSpy.lastCall.args[0],
-      '/c/test-repo/+/123/5..edit?forceReload=true'
-    );
+    assert.isTrue(applyFixSpy.called);
   });
 });
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 e9fbeda..8e99c3c 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import './gr-hovercard-run';
 import {fixture, html, assert} from '@open-wc/testing';
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 fff69ef..4f3b0fb 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-error-manager';
 import {
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 cbbcee0..c48946b 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-notifications-prompt';
 import {GrNotificationsPrompt} from './gr-notifications-prompt';
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 729a15b..4e1f37d 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
@@ -3,6 +3,7 @@
  * Copyright 2023 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {html, assert, fixture, waitUntil} from '@open-wc/testing';
 import './gr-router';
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 4a78140..6a86ad1 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-router';
 import {Page, PageContext} from './gr-page';
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
index bc8da05..62ef654 100644
--- 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
@@ -3,6 +3,7 @@
  * 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';
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 7c224a9..5319c90 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
@@ -56,12 +56,6 @@
   @query('#applyFixDialog')
   applyFixDialog?: GrDialog;
 
-  /** The currently observed dialog by `dialogOberserver`. */
-  observedDialog?: GrDialog;
-
-  /** The current observer observing the `observedDialog`. */
-  dialogObserver?: ResizeObserver;
-
   @query('#nextFix')
   nextFix?: GrButton;
 
@@ -211,10 +205,6 @@
     `;
   }
 
-  override disconnectedCallback() {
-    super.disconnectedCallback();
-  }
-
   private renderHeader() {
     return html`
       <div slot="header">${this.currentFix?.description ?? ''}</div>
@@ -249,13 +239,21 @@
   private renderFooter() {
     const fixCount = this.fixSuggestions?.length ?? 0;
     const reasonForDisabledApplyButton = this.computeTooltip();
-    if (fixCount < 2 && !reasonForDisabledApplyButton) return nothing;
-    return html`<div slot="footer" class="fix-picker">
-      ${when(fixCount >= 2, () =>
-        this.renderNavForMultipleSuggestedFixes(fixCount)
-      )}
-      ${this.renderWarning(reasonForDisabledApplyButton)}
-    </div>`;
+    const shouldRenderNav = fixCount >= 2;
+    const shouldRenderWarning = !!reasonForDisabledApplyButton;
+
+    if (!shouldRenderNav && !shouldRenderWarning) return nothing;
+
+    return html`
+      <div slot="footer" class="fix-picker">
+        ${when(shouldRenderNav, () =>
+          this.renderNavForMultipleSuggestedFixes(fixCount)
+        )}
+        ${when(shouldRenderWarning, () =>
+          this.renderWarning(reasonForDisabledApplyButton)
+        )}
+      </div>
+    `;
   }
 
   private renderNavForMultipleSuggestedFixes(fixCount: number) {
@@ -392,23 +390,20 @@
 
   private computeTooltip() {
     if (!this.change || !this.patchNum) return '';
-    return this.latestPatchNum !== this.patchNum
-      ? 'You cannot apply this fix because it is from a previous patchset'
-      : '';
+    if (this.isApplyFixLoading) return 'Fix is still loading ...';
+    return '';
   }
 
   private computeDisableApplyFixButton() {
     if (!this.change || !this.patchNum) return true;
-    return this.patchNum !== this.latestPatchNum || this.isApplyFixLoading;
+    return this.isApplyFixLoading;
   }
 
   // visible for testing
   async handleApplyFix(e: Event) {
     if (e) e.stopPropagation();
 
-    const changeNum = this.changeNum;
-    const patchNum = this.patchNum;
-    const change = this.change;
+    const {changeNum, patchNum, change} = this;
     if (!changeNum || !patchNum || !change || !this.currentFix) {
       throw new Error('Not all required properties are set.');
     }
@@ -419,7 +414,8 @@
       res = await this.restApiService.applyFixSuggestion(
         changeNum,
         patchNum,
-        this.fixSuggestions[0].replacements
+        this.fixSuggestions[0].replacements,
+        this.latestPatchNum
       );
     } else {
       res = await this.restApiService.applyRobotFixSuggestion(
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 dc7ab98..d72a85e 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
@@ -3,6 +3,7 @@
  * Copyright 2019 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-apply-fix-dialog';
 import {
@@ -162,25 +163,7 @@
       await open(TWO_FIXES);
       const button = getConfirmButton();
       assert.isTrue(button.hasAttribute('disabled'));
-      assert.equal(button.getAttribute('title'), '');
-    });
-
-    test('apply fix button is disabled on older patchset', async () => {
-      element.change = element.change = {
-        ...createParsedChange(),
-        revisions: createRevisions(2),
-        current_revision: getCurrentRevision(0),
-      };
-      element.latestPatchNum = element.change.revisions[
-        element.change.current_revision
-      ]._number as PatchSetNumber;
-      await open(TWO_FIXES);
-      const button = getConfirmButton();
-      assert.isTrue(button.hasAttribute('disabled'));
-      assert.equal(
-        button.getAttribute('title'),
-        'You cannot apply this fix because it is from a previous patchset'
-      );
+      assert.equal(button.getAttribute('title'), 'Fix is still loading ...');
     });
   });
 
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 eea8aaa..54a48b9 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-diff-host';
 import {
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 2d51eed..43dee0a 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-diff-mode-selector';
 import {GrDiffModeSelector} from './gr-diff-mode-selector';
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 96467b4..789c84d 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-diff-view';
 import {
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 65c6f1a..7aeda18 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../../shared/revision-info/revision-info';
 import './gr-patch-range-select';
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 ea5e9f3..c92f5bb 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-documentation-search';
 import {GrDocumentationSearch} from './gr-documentation-search';
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 aacbb36..bd6ec1cd 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-edit-controls';
 import {GrEditControls} from './gr-edit-controls';
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 bd27660..a406251 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-edit-file-controls';
 import {GrEditFileControls} from './gr-edit-file-controls';
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 e20d66d..a26eb42 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-editor-view';
 import {GrEditorView} from './gr-editor-view';
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index cd19872..7929d9b 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../test/common-test-setup';
 import './gr-app';
 import {getAppContext} from '../services/app-context';
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 5c15816..76b0a4d 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+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';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts
index cef6a8b..5164e98 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import {HookApi, PluginElement} from '../../../api/hook';
 import {PluginApi} from '../../../api/plugin';
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 e0b792f..932dc96 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-plugin-host';
 import {GrPluginHost} from './gr-plugin-host';
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 8e7605d..80ae34d 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {fixture, html, assert} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {stubElement} from '../../../test/test-utils';
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 5354ea5..e6c7100 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../../shared/gr-js-api-interface/gr-js-api-interface';
 import {GrPopupInterface} from './gr-popup-interface';
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 12321c2..fbcd494 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-email-editor';
 import {GrEmailEditor} from './gr-email-editor';
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 5be5b29..7192235 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-gpg-editor';
 import {
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 d52b423..e96fa39 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-identities';
 import {GrIdentities} from './gr-identities';
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 c97a036..a65c8a4 100644
--- a/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-preferences/gr-preferences.ts
@@ -124,15 +124,13 @@
       () => this.getConfigModel().docsBaseUrl$,
       docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
     );
-    if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)) {
-      subscribe(
-        this,
-        () => this.getPluginLoader().pluginsModel.suggestionsPlugins$,
-        // We currently support results from only 1 provider.
-        suggestionsPlugins =>
-          (this.suggestionsProvider = suggestionsPlugins?.[0]?.provider)
-      );
-    }
+    subscribe(
+      this,
+      () => this.getPluginLoader().pluginsModel.suggestionsPlugins$,
+      // We currently support results from only 1 provider.
+      suggestionsPlugins =>
+        (this.suggestionsProvider = suggestionsPlugins?.[0]?.provider)
+    );
   }
 
   static override get styles() {
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 be81e4e..ec8a0e2 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-settings-view';
 import {GrSettingsView} from './gr-settings-view';
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 9528fb2..fddb603 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-ssh-editor';
 import {
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 c608656..e771a9d 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-watched-projects-editor';
 import {GrWatchedProjectsEditor} from './gr-watched-projects-editor';
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 552e321..e8d7dae 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-account-entry';
 import {GrAccountEntry} from './gr-account-entry';
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 eaf8974..887ef96 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-account-list';
 import {GrAccountList} from './gr-account-list';
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
index 6908e95..1fe860f 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-alert';
 import {GrAlert} from './gr-alert';
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 10ba5d0..b9063ba 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-autocomplete-dropdown';
 import {
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 0cef331..bf3ba66 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-autocomplete';
 import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
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 145e39d..f695ffa 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-button';
 import {addListener} from '@polymer/polymer/lib/utils/gestures';
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 b56fcfd..fb44c56 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-comment-thread';
 import {sortComments} from '../../../utils/comment-util';
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 b55c9fd..4c3aeaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -90,8 +90,11 @@
 import {getFileExtension} from '../../../utils/file-util';
 import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
 import {deepEqual} from '../../../utils/deep-util';
-import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
-import {noAwait, waitUntil} from '../../../utils/async-util';
+import {
+  GrSuggestionDiffPreview,
+  PreviewLoadedDetail,
+} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
+import {waitUntil} from '../../../utils/async-util';
 import {
   AutocompleteCache,
   AutocompletionContext,
@@ -436,32 +439,29 @@
         this.autocompleteComment();
       }
     );
-    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.generateSuggestionTrigger$.pipe(
+          debounceTime(GENERATE_SUGGESTION_DEBOUNCE_DELAY_MS)
+        ),
+      () => {
+        this.generateSuggestEdit();
+      }
+    );
+    subscribe(
+      this,
+      () => this.getUserModel().preferences$,
+      prefs => {
+        this.autocompleteEnabled = !!prefs.allow_autocompleting_comments;
+        if (
+          this.generateSuggestion !==
+          !!prefs.allow_suggest_code_while_commenting
+        ) {
+          this.generateSuggestion = !!prefs.allow_suggest_code_while_commenting;
         }
-      );
-      subscribe(
-        this,
-        () => this.getUserModel().preferences$,
-        prefs => {
-          this.autocompleteEnabled = !!prefs.allow_autocompleting_comments;
-          if (
-            this.generateSuggestion !==
-            !!prefs.allow_suggest_code_while_commenting
-          ) {
-            this.generateSuggestion =
-              !!prefs.allow_suggest_code_while_commenting;
-          }
-        }
-      );
-    }
+      }
+    );
   }
 
   override connectedCallback() {
@@ -1150,7 +1150,6 @@
   // private but used in test
   showGeneratedSuggestion() {
     return (
-      this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2) &&
       this.suggestionsProvider &&
       this.editing &&
       !this.permanentEditingMode &&
@@ -1181,25 +1180,19 @@
     if (this.generatedFixSuggestion) {
       return html`<gr-suggestion-diff-preview
         id="suggestionDiffPreview"
+        .uuid=${this.generatedSuggestionId}
         .fixSuggestionInfo=${this.generatedFixSuggestion}
+        .patchSet=${this.comment?.patch_set}
+        .commentId=${this.comment?.id}
+        @preview-loaded=${(event: CustomEvent<PreviewLoadedDetail>) =>
+          (this.previewedGeneratedFixSuggestion =
+            event.detail.previewLoadedFor)}
       ></gr-suggestion-diff-preview>`;
     } else {
       return nothing;
     }
   }
 
-  // visible for testing
-  async waitPreviewForGeneratedSuggestion() {
-    const generatedFixSuggestion = this.generatedFixSuggestion;
-    if (!generatedFixSuggestion) return;
-    await waitUntil(
-      () =>
-        !!this.suggestionDiffPreview?.previewed &&
-        this.suggestionDiffPreview?.previewLoadedFor === generatedFixSuggestion
-    );
-    this.previewedGeneratedFixSuggestion = generatedFixSuggestion;
-  }
-
   private renderGenerateSuggestEditButton() {
     if (!this.showGeneratedSuggestion()) {
       return nothing;
@@ -1224,14 +1217,8 @@
               if (this.generateSuggestion) {
                 this.generateSuggestionTrigger$.next();
               } else {
-                if (
-                  this.flagsService.isEnabled(
-                    KnownExperimentId.ML_SUGGESTED_EDIT_V2
-                  )
-                ) {
-                  this.generatedFixSuggestion = undefined;
-                  this.autoSaveTrigger$.next();
-                }
+                this.generatedFixSuggestion = undefined;
+                this.autoSaveTrigger$.next();
               }
               this.reporting.reportInteraction(
                 this.generateSuggestion
@@ -1240,9 +1227,7 @@
               );
             }}
           />
-          ${this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
-            ? 'Attach AI-suggested fix'
-            : 'Generate Suggestion'}
+          Attach AI-suggested fix
           ${when(
             this.suggestionLoading,
             () => html`<span class="loadingSpin"></span>`,
@@ -1278,13 +1263,7 @@
     }
   }
 
-  private generateSuggestEdit() {
-    if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT_V2)) {
-      this.generateSuggestEdit_v2();
-    }
-  }
-
-  private async generateSuggestEdit_v2() {
+  private async generateSuggestEdit() {
     const suggestionsProvider = this.suggestionsProvider;
     const changeInfo = this.getChangeModel().getChange();
     if (
@@ -1338,7 +1317,6 @@
       return;
     }
     this.generatedFixSuggestion = suggestion;
-    noAwait(this.waitPreviewForGeneratedSuggestion());
 
     try {
       await waitUntil(() => this.getFixSuggestions() !== undefined);
@@ -1575,9 +1553,6 @@
     assert(isDraft(this.comment), 'only drafts are editable');
     if (this.editing) return;
     this.editing = true;
-    // For quickly opening and closing the comment, the suggestion diff preview
-    // might not have time to load and preview.
-    noAwait(this.waitPreviewForGeneratedSuggestion());
   }
 
   // TODO: Move this out of gr-comment. gr-comment should not have a comments
@@ -1854,8 +1829,6 @@
   }
 
   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
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 7292c64..84eee96 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-comment';
 import {AUTO_SAVE_DEBOUNCE_DELAY_MS, GrComment} from './gr-comment';
@@ -48,7 +49,6 @@
   CommentsModel,
   commentsModelToken,
 } from '../../../models/comments/comments-model';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {GrSuggestionDiffPreview} from '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
 
 suite('gr-comment tests', () => {
@@ -1005,11 +1005,6 @@
         },
       ],
     };
-    setup(async () => {
-      stubFlags('isEnabled')
-        .withArgs(KnownExperimentId.ML_SUGGESTED_EDIT_V2)
-        .returns(true);
-    });
 
     test('renders suggestions in comment', async () => {
       const comment = {
@@ -1160,7 +1155,14 @@
         suggestionDiffPreview.previewed = true;
         suggestionDiffPreview.previewLoadedFor = generatedFixSuggestion;
         await element.updateComplete;
-        await element.waitPreviewForGeneratedSuggestion();
+        // trigger event preview-loaded on suggestionDiffPreview with detail
+        suggestionDiffPreview.dispatchEvent(
+          new CustomEvent('preview-loaded', {
+            bubbles: true,
+            detail: {previewLoadedFor: generatedFixSuggestion},
+          })
+        );
+        // await element.waitPreviewForGeneratedSuggestion();
         await element.updateComplete;
         element.save();
         await element.updateComplete;
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 ef01dad..7e476eb 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-copy-clipboard';
 import {GrCopyClipboard} from './gr-copy-clipboard';
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 81d2b45..b03a421 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {fixture, html, assert} from '@open-wc/testing';
 import {AbortStop, CursorMoveResult} from '../../../api/core';
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 98f2d82..d6a01ab 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-date-formatter';
 import {GrDateFormatter} from './gr-date-formatter';
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 41fcfed..c4928d9 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-dialog';
 import {GrDialog} from './gr-dialog';
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 b1d4e36..2f656b2 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-download-commands';
 import {GrDownloadCommands} from './gr-download-commands';
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 c148a1b..fb150fc 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-dropdown-list';
 import {GrDropdownList} from './gr-dropdown-list';
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 e9ef52b..3a01748 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-dropdown';
 import {GrDropdown} from './gr-dropdown';
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 fe96d56..3d2469a 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-editable-content';
 import {GrEditableContent} from './gr-editable-content';
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 3bb058e..bb7989c 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-editable-label';
 import {GrEditableLabel} from './gr-editable-label';
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 36266a0..137c853 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
@@ -16,7 +16,7 @@
 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, NumericChangeId, PatchSetNumber} from '../../../types/common';
+import {Comment, PatchSetNumber} from '../../../types/common';
 import {OpenFixPreviewEventDetail} from '../../../types/events';
 import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
 import {SuggestionsProvider} from '../../../api/suggestions';
@@ -25,7 +25,6 @@
 import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
 import {getAppContext} from '../../../services/app-context';
 import {Interaction} from '../../../constants/reporting';
-import {isFileUnchanged} from '../../../utils/diff-util';
 
 export const COLLAPSE_SUGGESTION_STORAGE_KEY = 'collapseSuggestionStorageKey';
 
@@ -48,15 +47,11 @@
 
   @state() latestPatchNum?: PatchSetNumber;
 
-  @state() changeNum?: NumericChangeId;
-
   @state()
   suggestionsProvider?: SuggestionsProvider;
 
   @state() private isOwner = false;
 
-  @state() private enableApplyOnUnModifiedFile = false;
-
   /**
    * This is just a reflected property such that css rules can be based on it.
    */
@@ -73,7 +68,7 @@
 
   private readonly reporting = getAppContext().reportingService;
 
-  private readonly restApiService = getAppContext().restApiService;
+  @state() private previewLoaded = false;
 
   constructor() {
     super();
@@ -92,17 +87,6 @@
       () => this.getChangeModel().isOwner$,
       x => (this.isOwner = x)
     );
-    subscribe(
-      this,
-      () => this.getChangeModel().changeNum$,
-      x => (this.changeNum = x)
-    );
-  }
-
-  override updated(changed: PropertyValues) {
-    if (changed.has('changeNum') || changed.has('latestPatchNum')) {
-      this.checkIfcanEnableApplyOnUnModifiedFile();
-    }
   }
 
   override connectedCallback() {
@@ -230,6 +214,9 @@
       </div>
       <gr-suggestion-diff-preview
         .fixSuggestionInfo=${this.comment?.fix_suggestions?.[0]}
+        .patchSet=${this.comment?.patch_set}
+        .commentId=${this.comment?.id}
+        @preview-loaded=${() => (this.previewLoaded = true)}
       ></gr-suggestion-diff-preview>`;
   }
 
@@ -295,9 +282,7 @@
     if (!this.comment?.fix_suggestions) return;
     this.applyingFix = true;
     try {
-      await this.suggestionDiffPreview?.applyFixSuggestion(
-        this.enableApplyOnUnModifiedFile
-      );
+      await this.suggestionDiffPreview?.applyFix();
     } finally {
       this.applyingFix = false;
     }
@@ -305,37 +290,19 @@
 
   private isApplyEditDisabled() {
     if (this.comment?.patch_set === undefined) return true;
-    if (this.enableApplyOnUnModifiedFile) return false;
-    return this.comment.patch_set !== this.latestPatchNum;
+    return !this.previewLoaded;
   }
 
   private computeApplyEditTooltip() {
     if (this.comment?.patch_set === undefined) return '';
-    return this.comment.patch_set !== this.latestPatchNum
-      ? 'You cannot apply this fix because it is from a previous patchset'
-      : '';
+    if (!this.previewLoaded) return 'Fix is still loading ...';
+    return '';
   }
 
-  private async checkIfcanEnableApplyOnUnModifiedFile() {
-    // if enabled we don't need to enable
-    if (!this.isApplyEditDisabled()) return;
-
-    const basePatchNum = this.comment?.patch_set;
-    const path = this.comment?.path;
-
-    if (!basePatchNum || !this.latestPatchNum || !path || !this.changeNum) {
-      return;
-    }
-
-    const diff = await this.restApiService.getDiff(
-      this.changeNum,
-      basePatchNum,
-      this.latestPatchNum,
-      path
-    );
-
-    if (diff && isFileUnchanged(diff)) {
-      this.enableApplyOnUnModifiedFile = true;
+  override updated(changedProperties: PropertyValues) {
+    super.updated(changedProperties);
+    if (changedProperties.has('comment') && this.comment?.fix_suggestions) {
+      this.previewLoaded = false;
     }
   }
 }
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 d29d2a6..723267e 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {assert, fixture, html} from '@open-wc/testing';
 import {changeModelToken} from '../../../models/change/change-model';
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 5afd53b..647282f 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
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {fixture, assert} from '@open-wc/testing';
 import {html} from 'lit';
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 d42dc7c..b2e412c 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../../change/gr-change-actions/gr-change-actions';
 import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
index ddba546..329d363 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-js-api-interface';
 import {EndpointType, GrPluginEndpoints} from './gr-plugin-endpoints';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
index b2ac2bf..3b07ddd 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils';
 import {PluginLoader} from './gr-plugin-loader';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
index 472093e..8b5ce6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-js-api-interface';
 import {GrPluginRestApi} from './gr-plugin-rest-api';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
index 8e4edd6..2758d21 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../../change/gr-reply-dialog/gr-reply-dialog';
 import {getAppContext} from '../../../services/app-context';
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 dad056b..6345754 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,6 +3,7 @@
  * 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 {
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 a8f6ff2..1a2d23c 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-labeled-autocomplete';
 import {GrLabeledAutocomplete} from './gr-labeled-autocomplete';
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.ts b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.ts
index 7e353f6..112ec78 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import '../../../test/common-test-setup';
 import {waitEventLoop} from '../../../test/test-utils';
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 08572b6..3f9a32b 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-linked-chip';
 import {GrLinkedChip} from './gr-linked-chip';
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 5b1e162..c0d9755 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-list-view';
 import {GrListView} from './gr-list-view';
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 1cdb5c7..f627577 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-page-nav';
 import {GrPageNav} from './gr-page-nav';
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 6839431..24b377c 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-repo-branch-picker';
 import {GrRepoBranchPicker} from './gr-repo-branch-picker';
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 0f7acae..cea6704 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
@@ -3,6 +3,7 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../../test/common-test-setup';
 import {
   SiteBasedCache,
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 4bb63ea..20f708e 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-select';
 import {fixture, html, assert} from '@open-wc/testing';
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 f489664..b2fed78 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
@@ -3,6 +3,7 @@
  * Copyright 2018 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-shell-command';
 import {GrShellCommand} from './gr-shell-command';
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 d059c7c..93f17c8 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
@@ -8,7 +8,6 @@
 import {customElement, property, state} from 'lit/decorators.js';
 import {getAppContext} from '../../../services/app-context';
 import {
-  Comment,
   EDIT,
   BasePatchSetNum,
   PatchSetNumber,
@@ -30,14 +29,15 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {DiffPreview} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
 import {userModelToken} from '../../../models/user/user-model';
-import {createUserFixSuggestion} from '../../../utils/comment-util';
-import {commentModelToken} from '../gr-comment-model/gr-comment-model';
 import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {fire} from '../../../utils/event-util';
 import {Timing} from '../../../constants/reporting';
 import {createChangeUrl} from '../../../models/views/change';
 import {getFileExtension} from '../../../utils/file-util';
 
+export interface PreviewLoadedDetail {
+  previewLoadedFor?: FixSuggestionInfo;
+}
 /**
  * Diff preview for
  * 1. code block suggestion vs commented Text
@@ -48,29 +48,41 @@
  */
 @customElement('gr-suggestion-diff-preview')
 export class GrSuggestionDiffPreview extends LitElement {
+  // Optional. Used as backup when preview is not loaded.
   @property({type: String})
-  suggestion?: string;
+  codeText?: string;
 
+  // Required.
   @property({type: Object})
   fixSuggestionInfo?: FixSuggestionInfo;
 
+  // Used to determine if the preview has been loaded
+  // this is identical to previewLoadedFor !== undefined and can be removed
   @property({type: Boolean, attribute: 'previewed', reflect: true})
   previewed = false;
 
+  // Optional. Used in logging.
   @property({type: String})
   uuid?: string;
 
-  @state()
-  comment?: Comment;
+  @property({type: Number})
+  patchSet?: BasePatchSetNum;
 
-  @state()
-  commentedText?: string;
+  // Optional. Used in logging.
+  @property({type: String})
+  commentId?: string;
 
   @state()
   layers: DiffLayer[] = [];
 
+  /**
+   * The fix suggestion info that the preview is loaded for.
+   *
+   * This is used to determine if the preview has been loaded for the same
+   * fix suggestion info currently in gr-comment.
+   */
   @state()
-  previewLoadedFor?: string | FixSuggestionInfo;
+  public previewLoadedFor?: FixSuggestionInfo;
 
   @state() repo?: RepoName;
 
@@ -102,8 +114,6 @@
 
   private readonly getUserModel = resolve(this, userModelToken);
 
-  private readonly getCommentModel = resolve(this, commentModelToken);
-
   private readonly getNavigation = resolve(this, navigationToken);
 
   private readonly syntaxLayer = new GrSyntaxLayerWorker(
@@ -120,14 +130,6 @@
     );
     subscribe(
       this,
-      () => this.getChangeModel().revisions$,
-      revisions =>
-        (this.hasEdit = Object.values(revisions).some(
-          info => info._number === EDIT
-        ))
-    );
-    subscribe(
-      this,
       () => this.getChangeModel().latestPatchNum$,
       x => (this.latestPatchNum = x)
     );
@@ -142,16 +144,6 @@
     );
     subscribe(
       this,
-      () => this.getCommentModel().comment$,
-      comment => (this.comment = comment)
-    );
-    subscribe(
-      this,
-      () => this.getCommentModel().commentedText$,
-      commentedText => (this.commentedText = commentedText)
-    );
-    subscribe(
-      this,
       () => this.getChangeModel().repo$,
       x => (this.repo = x)
     );
@@ -192,27 +184,22 @@
   }
 
   override updated(changed: PropertyValues) {
-    if (changed.has('commentedText') || changed.has('comment')) {
-      if (this.previewLoadedFor !== this.suggestion) {
-        this.fetchFixPreview();
-      }
-    }
-
-    if (changed.has('changeNum') || changed.has('comment')) {
-      if (this.previewLoadedFor !== this.fixSuggestionInfo) {
-        this.fetchfixSuggestionInfoPreview();
-      }
+    if (
+      changed.has('fixSuggestionInfo') ||
+      changed.has('changeNum') ||
+      changed.has('patchSet')
+    ) {
+      this.fetchFixPreview();
     }
   }
 
   override render() {
-    if (!this.suggestion && !this.fixSuggestionInfo) return nothing;
-    const code = this.suggestion;
+    if (!this.fixSuggestionInfo) return nothing;
     return html`
       ${when(
         this.previewLoadedFor,
         () => this.renderDiff(),
-        () => html`<code>${code}</code>`
+        () => html`<code>${this.codeText}</code>`
       )}
     `;
   }
@@ -236,88 +223,34 @@
   }
 
   private async fetchFixPreview() {
-    if (
-      !this.changeNum ||
-      !this.comment?.patch_set ||
-      !this.suggestion ||
-      !this.commentedText
-    )
-      return;
-    const fixSuggestions = createUserFixSuggestion(
-      this.comment,
-      this.commentedText,
-      this.suggestion
-    );
+    if (!this.changeNum || !this.patchSet || !this.fixSuggestionInfo) return;
+
     this.reporting.time(Timing.PREVIEW_FIX_LOAD);
     const res = await this.restApiService.getFixPreview(
       this.changeNum,
-      this.comment?.patch_set,
-      fixSuggestions[0].replacements
-    );
-    if (!res) return;
-    const currentPreviews = Object.keys(res).map(key => {
-      return {filepath: key, preview: res[key]};
-    });
-    this.reporting.timeEnd(Timing.PREVIEW_FIX_LOAD, {
-      uuid: this.uuid,
-      commentId: this.comment?.id ?? '',
-    });
-    if (currentPreviews.length > 0) {
-      this.preview = currentPreviews[0];
-      this.previewLoadedFor = this.suggestion;
-      this.previewed = true;
-    }
-
-    return res;
-  }
-
-  private async fetchfixSuggestionInfoPreview() {
-    if (
-      this.suggestion ||
-      !this.changeNum ||
-      !this.comment?.patch_set ||
-      !this.fixSuggestionInfo
-    )
-      return;
-
-    this.previewed = false;
-    this.reporting.time(Timing.PREVIEW_FIX_LOAD);
-    const res = await this.restApiService.getFixPreview(
-      this.changeNum,
-      this.comment?.patch_set,
+      this.patchSet,
       this.fixSuggestionInfo.replacements
     );
-
     if (!res) return;
     const currentPreviews = Object.keys(res).map(key => {
       return {filepath: key, preview: res[key]};
     });
     this.reporting.timeEnd(Timing.PREVIEW_FIX_LOAD, {
       uuid: this.uuid,
-      commentId: this.comment?.id ?? '',
+      commentId: this.commentId ?? '',
     });
     if (currentPreviews.length > 0) {
       this.preview = currentPreviews[0];
-      this.previewed = true;
       this.previewLoadedFor = this.fixSuggestionInfo;
+      this.previewed = true;
+
+      fire(this, 'preview-loaded', {
+        previewLoadedFor: this.fixSuggestionInfo,
+      });
     }
 
     return res;
   }
-
-  /**
-   * Applies a fix (fix_suggestion in comment) previewed in
-   * `suggestion-diff-preview`, navigating to the new change URL with the EDIT
-   * patchset.
-   *
-   * Similar code flow is in gr-apply-fix-dialog.handleApplyFix
-   * Used in gr-fix-suggestions
-   */
-  public applyFixSuggestion(onLatestPatchset = false) {
-    if (this.suggestion || !this.fixSuggestionInfo) return;
-    return this.applyFix(this.fixSuggestionInfo, onLatestPatchset);
-  }
-
   /**
    * Applies a fix (codeblock in comment message) previewed in
    * `suggestion-diff-preview`, navigating to the new change URL with the EDIT
@@ -326,30 +259,19 @@
    * Similar code flow is in gr-apply-fix-dialog.handleApplyFix
    * Used in gr-user-suggestion-fix
    */
-  public applyUserSuggestedFix() {
-    if (!this.comment || !this.suggestion || !this.commentedText) return;
 
-    const fixSuggestions = createUserFixSuggestion(
-      this.comment,
-      this.commentedText,
-      this.suggestion
-    );
-    this.applyFix(fixSuggestions[0]);
-  }
-
-  private async applyFix(
-    fixSuggestion: FixSuggestionInfo,
-    onLatestPatchset = false
-  ) {
+  public async applyFix() {
     const changeNum = this.changeNum;
-    const basePatchNum = this.comment?.patch_set as BasePatchSetNum;
+    const basePatchNum = this.patchSet;
+    const fixSuggestion = this.fixSuggestionInfo;
     if (!changeNum || !basePatchNum || !fixSuggestion) return;
 
     this.reporting.time(Timing.APPLY_FIX_LOAD);
     const res = await this.restApiService.applyFixSuggestion(
       changeNum,
-      onLatestPatchset ? this.latestPatchNum ?? basePatchNum : basePatchNum,
-      fixSuggestion.replacements
+      basePatchNum,
+      fixSuggestion.replacements,
+      this.latestPatchNum
     );
     this.reporting.timeEnd(Timing.APPLY_FIX_LOAD, {
       method: '1-click',
@@ -357,7 +279,7 @@
       fileExtension: getFileExtension(
         fixSuggestion?.replacements?.[0].path ?? ''
       ),
-      commentId: this.comment?.id ?? '',
+      commentId: this.commentId ?? '',
     });
     if (res?.ok) {
       this.getNavigation().setUrl(
@@ -369,7 +291,7 @@
           forceReload: !this.hasEdit,
         })
       );
-      fire(this, 'reload-diff', {path: this.comment?.path});
+      fire(this, 'reload-diff', {path: fixSuggestion.replacements[0].path});
       fire(this, 'apply-user-suggestion', {});
     }
   }
@@ -389,4 +311,7 @@
   interface HTMLElementTagNameMap {
     'gr-suggestion-diff-preview': GrSuggestionDiffPreview;
   }
+  interface HTMLElementEventMap {
+    'preview-loaded': CustomEvent<PreviewLoadedDetail>;
+  }
 }
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 2630aad..593d0a6 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
@@ -11,7 +11,10 @@
   commentModelToken,
 } from '../gr-comment-model/gr-comment-model';
 import {wrapInProvider} from '../../../models/di-provider-element';
-import {createComment} from '../../../test/test-data-generators';
+import {
+  createComment,
+  createFixSuggestionInfo,
+} from '../../../test/test-data-generators';
 import {getAppContext} from '../../../services/app-context';
 import {GrSuggestionDiffPreview} from './gr-suggestion-diff-preview';
 import {stubFlags} from '../../../test/test-utils';
@@ -29,7 +32,8 @@
         wrapInProvider(
           html`
             <gr-suggestion-diff-preview
-              .suggestion=${'Hello World'}
+              .codeText=${'Hello World'}
+              .fixSuggestionInfo=${createFixSuggestionInfo()}
             ></gr-suggestion-diff-preview>
           `,
           commentModelToken,
@@ -48,9 +52,8 @@
 
   test('render diff', async () => {
     stubFlags('isEnabled').returns(true);
-    element.suggestion =
-      '  private handleClick(e: MouseEvent) {\ne.stopPropagation();\ne.preventDefault();';
-    element.previewLoadedFor =
+    element.previewLoadedFor = createFixSuggestionInfo();
+    element.codeText =
       '  private handleClick(e: MouseEvent) {\ne.stopPropagation();\ne.preventDefault();';
     element.preview = {
       filepath:
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 3d36dfa..a6a1d83 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-suggestion-textarea';
 import {GrSuggestionTextarea} from './gr-suggestion-textarea';
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 a9080e8..661515a 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
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-tooltip-content';
 import {GrTooltipContent} from './gr-tooltip-content';
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 3f4f99e..42a2434 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
@@ -7,7 +7,7 @@
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../gr-suggestion-diff-preview/gr-suggestion-diff-preview';
-import {css, html, LitElement, nothing} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, state, query} from 'lit/decorators.js';
 import {fire} from '../../../utils/event-util';
 import {getDocUrl} from '../../../utils/url-util';
@@ -18,6 +18,7 @@
 import {changeModelToken} from '../../../models/change/change-model';
 import {Comment, PatchSetNumber} from '../../../types/common';
 import {commentModelToken} from '../gr-comment-model/gr-comment-model';
+import {createUserFixSuggestion} from '../../../utils/comment-util';
 
 declare global {
   interface HTMLElementEventMap {
@@ -44,6 +45,10 @@
 
   @state() comment?: Comment;
 
+  @state() private previewLoaded = false;
+
+  @state() commentedText?: string;
+
   private readonly getConfigModel = resolve(this, configModelToken);
 
   private readonly getChangeModel = resolve(this, changeModelToken);
@@ -67,6 +72,11 @@
       () => this.getCommentModel().comment$,
       comment => (this.comment = comment)
     );
+    subscribe(
+      this,
+      () => this.getCommentModel().commentedText$,
+      commentedText => (this.commentedText = commentedText)
+    );
   }
 
   static override get styles() {
@@ -92,8 +102,14 @@
   }
 
   override render() {
-    if (!this.textContent) return nothing;
+    if (!this.textContent || !this.comment || !this.commentedText)
+      return nothing;
     const code = this.textContent;
+    const fixSuggestions = createUserFixSuggestion(
+      this.comment,
+      this.commentedText,
+      code
+    );
     return html`<div class="header">
         <div class="title">
           <span>Suggested edit</span>
@@ -136,10 +152,21 @@
         </div>
       </div>
       <gr-suggestion-diff-preview
-        .suggestion=${this.textContent}
+        .patchSet=${this.comment?.patch_set}
+        .commentId=${this.comment?.id}
+        .fixSuggestionInfo=${fixSuggestions[0]}
+        .codeText=${code}
+        @preview-loaded=${() => (this.previewLoaded = true)}
       ></gr-suggestion-diff-preview>`;
   }
 
+  override updated(changedProperties: PropertyValues) {
+    super.updated(changedProperties);
+    if (changedProperties.has('commentedText') && this.commentedText) {
+      this.previewLoaded = false;
+    }
+  }
+
   handleShowFix() {
     if (!this.textContent) return;
     fire(this, 'open-user-suggest-preview', {code: this.textContent});
@@ -148,20 +175,19 @@
   async handleApplyFix() {
     if (!this.textContent) return;
     this.applyingFix = true;
-    await this.suggestionDiffPreview?.applyUserSuggestedFix();
+    await this.suggestionDiffPreview?.applyFix();
     this.applyingFix = false;
   }
 
   private isApplyEditDisabled() {
     if (this.comment?.patch_set === undefined) return true;
-    return this.comment.patch_set !== this.latestPatchNum;
+    return !this.previewLoaded;
   }
 
   private computeApplyEditTooltip() {
     if (this.comment?.patch_set === undefined) return '';
-    return this.comment.patch_set !== this.latestPatchNum
-      ? 'You cannot apply this fix because it is from a previous patchset'
-      : '';
+    if (!this.previewLoaded) return 'Fix is still loading ...';
+    return '';
   }
 }
 
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 c97d23d..56d826e 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
@@ -22,6 +22,7 @@
     const commentModel = new CommentModel(getAppContext().restApiService);
     commentModel.updateState({
       comment: createComment(),
+      commentedText: 'Hello World',
     });
     element = (
       await fixture<GrUserSuggestionsFix>(
@@ -76,7 +77,7 @@
               role="button"
               tabindex="-1"
               flatten=""
-              title="You cannot apply this fix because it is from a previous patchset"
+              title="Fix is still loading ..."
               >Apply edit</gr-button
             >
           </div>
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 581b577..62a1461 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
@@ -3,6 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {fixture, assert} from '@open-wc/testing';
 import {html} from 'lit';
diff --git a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts
index a8cdff6..088dac6 100644
--- a/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-coverage-layer/gr-coverage-layer_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2019 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {CoverageType, Side} from '../../../api/diff';
 import {GrCoverageLayer, mergeRanges} from './gr-coverage-layer';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts
index 8d0050f..8eeaa84 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {
   GrDiffLineType,
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 70fece3..6f5674c 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../gr-diff/gr-diff';
 import './gr-diff-cursor';
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 378c255..6ee5f18 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {
   TEST_ONLY,
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 32decb1..e186b72 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-diff-highlight';
 import {getTextOffset} from './gr-range-normalizer';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index d138414..89bb49e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -181,18 +181,29 @@
     return chunkIndex;
   }
 
+  /**
+   * Check if a chunk is collapsible.
+   *
+   * A chunk is collapsible if it is either common or skippable, and it is not
+   * a key location, or it is outside of the focus range.
+   *
+   * @param chunk The chunk to check.
+   * @param offsetLeft The offset of the left side of the chunk.
+   * @param offsetRight The offset of the right side of the chunk.
+   * @return True if the chunk is collapsible, false otherwise.
+   */
   private isCollapsibleChunk(
     chunk: DiffContent,
     offsetLeft: number,
     offsetRight: number
   ) {
-    return (
-      (chunk.ab ||
-        chunk.common ||
-        chunk.skip ||
-        this.isChunkOutsideOfFocusRange(chunk, offsetLeft, offsetRight)) &&
-      !chunk.keyLocation
+    const isCommonOrSkip = chunk.ab || chunk.common || chunk.skip;
+    const isOutsideOfFocusRange = this.isChunkOutsideOfFocusRange(
+      chunk,
+      offsetLeft,
+      offsetRight
     );
+    return (isCommonOrSkip && !chunk.keyLocation) || isOutsideOfFocusRange;
   }
 
   private isChunkOutsideOfFocusRange(
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
index 3f01096..19a687e 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
@@ -1054,6 +1054,32 @@
           assert.equal(result.groups[0].lines.length, 5);
           assert.equal(result.groups[1].type, GrDiffGroupType.DELTA);
         });
+
+        test('collapse chunks with key locations if out of focus range', () => {
+          const keyLocationLineText = 'key location behind a context group';
+          state = {
+            lineNums: {left: 7, right: 6},
+            chunkIndex: 4,
+          };
+          const result = processor.processNext(state, [
+            ...chunks,
+            {
+              ab: Array.from<string>({length: 2}).fill(
+                'all work and no play make jill a dull boy'
+              ),
+              keyLocation: false,
+            },
+            {
+              ab: Array.from<string>({length: 5}).fill(keyLocationLineText),
+              keyLocation: true,
+            },
+          ]);
+          assert.equal(result.groups.length, 3);
+          assert.equal(
+            result.groups[2].contextGroups[0].lines[0].text,
+            keyLocationLineText
+          );
+        });
       });
     });
 
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 9cc6a90..dd39aa5 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-diff-selection';
 import '../gr-diff/gr-diff';
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 98a2093..2913fc8 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
@@ -369,6 +369,9 @@
     background-color: var(--dark-add-highlight-color);
     &:has(.is-out-of-focus-range) {
       background-color: transparent;
+      .intraline {
+        background-color: transparent;
+      }
     }
   }
   gr-diff-row td.content.add div.contentText,
@@ -376,6 +379,9 @@
     background-color: var(--light-add-highlight-color);
     &:has(.is-out-of-focus-range) {
       background-color: transparent;
+      .intraline {
+        background-color: transparent;
+      }
     }
   }
   /* If there are no intraline info, consider everything changed */
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 5fa4788..ffb5c90 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
@@ -3,6 +3,7 @@
  * Copyright 2015 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import {
   createConfig,
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 f76ddef..9687ee8 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
@@ -3,6 +3,7 @@
  * Copyright 2024 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 
 import {assert} from '@open-wc/testing';
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 338ac07..a7e215b 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import '../gr-diff/gr-diff-line';
 import './gr-ranged-comment-layer';
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 67836a4..5cc8409 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../../test/common-test-setup';
 import './gr-selection-action-box';
 import {GrSelectionActionBox} from './gr-selection-action-box';
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
index 221eada..1d8b4ed 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import {DiffInfo, GrDiffLineType, Side} from '../../../api/diff';
 import {getAppContext} from '../../../services/app-context';
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 1ac9593..8f2c751 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
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {
   createAccountWithIdNameAndEmail,
   createChange,
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index bf7b9dc..e6175c0 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {Subject} from 'rxjs';
 import {ChangeStatus} from '../../constants/constants';
 import '../../test/common-test-setup';
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index a8eda0f..fdaacd2 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import './checks-model';
 import {
diff --git a/polygerrit-ui/app/models/config/config-model_test.ts b/polygerrit-ui/app/models/config/config-model_test.ts
index b78a933..bb6af87 100644
--- a/polygerrit-ui/app/models/config/config-model_test.ts
+++ b/polygerrit-ui/app/models/config/config-model_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2023 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {assert} from '@open-wc/testing';
 import {getBaseUrl} from '../../utils/url-util';
diff --git a/polygerrit-ui/app/models/views/admin_test.ts b/polygerrit-ui/app/models/views/admin_test.ts
index 1cd1897..eab362a 100644
--- a/polygerrit-ui/app/models/views/admin_test.ts
+++ b/polygerrit-ui/app/models/views/admin_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import '../../test/common-test-setup';
 import {assertRouteFalse, assertRouteState} from '../../test/test-utils';
diff --git a/polygerrit-ui/app/models/views/search_test.ts b/polygerrit-ui/app/models/views/search_test.ts
index ed6de419..0b0594f 100644
--- a/polygerrit-ui/app/models/views/search_test.ts
+++ b/polygerrit-ui/app/models/views/search_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import {SinonStubbedMember} from 'sinon';
 import {
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 0bf3c97..750b421 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -19,7 +19,6 @@
   CHECKS_DEVELOPER = 'UiFeature__checks_developer',
   PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
   PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
-  ML_SUGGESTED_EDIT_V2 = 'UiFeature__ml_suggested_edit_v2',
   REVISION_PARENTS_DATA = 'UiFeature__revision_parents_data',
   COMMENT_AUTOCOMPLETION = 'UiFeature__comment_autocompletion_enabled',
   SAVE_PROJECT_CONFIG_FOR_REVIEW = 'UiFeature__save_project_config_for_review',
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
index f50921e..28be742 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {Auth, AuthStatus} from './gr-auth_impl';
 import {SinonFakeTimers} from 'sinon';
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 91d742a..86e1cd1 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {
   GrReporting,
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 0765485..a65eddc 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
@@ -2368,15 +2368,28 @@
 
   async applyFixSuggestion(
     changeNum: NumericChangeId,
-    patchNum: PatchSetNum,
-    fixReplacementInfos: FixReplacementInfo[]
+    fixPatchNum: PatchSetNum,
+    fixReplacementInfos: FixReplacementInfo[],
+    targetPatchNum?: PatchSetNum
   ): Promise<Response> {
-    const url = await this._changeBaseURL(changeNum, patchNum);
+    const url = await this._changeBaseURL(
+      changeNum,
+      targetPatchNum ?? fixPatchNum
+    );
+    const body: {
+      fix_replacement_infos: FixReplacementInfo[];
+      original_patchset_for_fix?: PatchSetNum;
+    } = {
+      fix_replacement_infos: fixReplacementInfos,
+    };
+    if (targetPatchNum !== undefined && targetPatchNum !== fixPatchNum) {
+      body.original_patchset_for_fix = fixPatchNum;
+    }
     return this._restApiHelper.fetch({
       fetchOptions: getFetchOptions({
         method: HttpMethod.POST,
         headers: {Accept: 'application/json'},
-        body: {fix_replacement_infos: fixReplacementInfos},
+        body,
       }),
       url: `${url}/fix:apply`,
       anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/fix:apply`,
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 af12a9c..204d626 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
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {
   addListenerForTest,
@@ -20,6 +21,7 @@
   createChange,
   createComment,
   createEditInfo,
+  createFixReplacementInfo,
   createParsedChange,
   createServerInfo,
   TEST_PROJECT_NAME,
@@ -1898,4 +1900,69 @@
       anonymizedUrl: '/accounts/self/starred.changes/*',
     });
   });
+  suite('applyFixSuggestion', () => {
+    const fixReplacementInfo = createFixReplacementInfo();
+    let fetchStub: sinon.SinonStub;
+    setup(() => {
+      element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
+      fetchStub = sinon
+        .stub(element._restApiHelper, 'fetch')
+        .resolves(new Response(makePrefixedJSON({})));
+    });
+    test('applyFixSuggestion without targetPatchNum', async () => {
+      await element.applyFixSuggestion(
+        123 as NumericChangeId,
+        1 as PatchSetNum,
+        [fixReplacementInfo]
+      );
+      assert.isTrue(fetchStub.calledOnce);
+      assert.equal(
+        fetchStub.lastCall.args[0].url,
+        '/changes/test-project~123/revisions/1/fix:apply'
+      );
+      const body = JSON.parse(fetchStub.lastCall.args[0].fetchOptions.body);
+      assert.isTrue(
+        Object.keys(body).length === 1 &&
+          body.fix_replacement_infos.length === 1
+      );
+      assert.deepEqual(body.fix_replacement_infos[0], fixReplacementInfo);
+    });
+
+    test('applyFixSuggestion with same patchNum and targetPatchNum', async () => {
+      const fixReplacementInfo = createFixReplacementInfo();
+      await element.applyFixSuggestion(
+        123 as NumericChangeId,
+        1 as PatchSetNum,
+        [fixReplacementInfo],
+        1 as PatchSetNum
+      );
+      assert.isTrue(fetchStub.calledOnce);
+      assert.equal(
+        fetchStub.lastCall.args[0].url,
+        '/changes/test-project~123/revisions/1/fix:apply'
+      );
+      const body = JSON.parse(fetchStub.lastCall.args[0].fetchOptions.body);
+      assert.isTrue(Object.keys(body).length === 1);
+      assert.deepEqual(body.fix_replacement_infos[0], fixReplacementInfo);
+    });
+
+    test('applyFixSuggestion with targetPatchNum', async () => {
+      const fixReplacementInfo = createFixReplacementInfo();
+      await element.applyFixSuggestion(
+        123 as NumericChangeId,
+        1 as PatchSetNum,
+        [fixReplacementInfo],
+        2 as PatchSetNum
+      );
+      assert.isTrue(fetchStub.calledOnce);
+      assert.equal(
+        fetchStub.lastCall.args[0].url,
+        '/changes/test-project~123/revisions/2/fix:apply'
+      );
+      const body = JSON.parse(fetchStub.lastCall.args[0].fetchOptions.body);
+      assert.isTrue(Object.keys(body).length === 2);
+      assert.deepEqual(body.fix_replacement_infos[0], fixReplacementInfo);
+      assert.deepEqual(body.original_patchset_for_fix, 1);
+    });
+  });
 });
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 3f79115..799cf43 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
@@ -738,8 +738,9 @@
    */
   applyFixSuggestion(
     changeNum: NumericChangeId,
-    patchNum: PatchSetNum,
-    fixReplacementInfos: FixReplacementInfo[]
+    fixPatchNum: PatchSetNum,
+    fixReplacementInfos: FixReplacementInfo[],
+    targetPatchNum?: PatchSetNum
   ): Promise<Response>;
 
   /**
diff --git a/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
index e96a2ad..0118e2e 100644
--- a/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
+++ b/polygerrit-ui/app/services/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2019 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {GrReviewerSuggestionsProvider} from './gr-reviewer-suggestions-provider';
 import {getAppContext} from '../app-context';
diff --git a/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts b/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
index 041aed2..ee02cfc 100644
--- a/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
+++ b/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {assertFails, waitEventLoop} from '../../test/test-utils';
 import {Scheduler} from './scheduler';
diff --git a/polygerrit-ui/app/services/service-worker-installer_test.ts b/polygerrit-ui/app/services/service-worker-installer_test.ts
index e13cf19..cfcebc1 100644
--- a/polygerrit-ui/app/services/service-worker-installer_test.ts
+++ b/polygerrit-ui/app/services/service-worker-installer_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {getAppContext} from './app-context';
 import '../test/common-test-setup';
 import {ServiceWorkerInstaller} from './service-worker-installer';
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index 164000a..4421e47 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2021 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import '../../test/common-test-setup';
 import {
   COMBO_TIMEOUT_MS,
diff --git a/polygerrit-ui/app/services/storage/gr-storage_test.ts b/polygerrit-ui/app/services/storage/gr-storage_test.ts
index 72878f8..6c22005 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_test.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import {NumericChangeId} from '../../api/rest-api';
 import '../../test/common-test-setup';
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 2309977..5f40cad 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -98,6 +98,7 @@
 import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
 import {
   DetailedLabelInfo,
+  FixReplacementInfo,
   PatchSetNumber,
   QuickLabelInfo,
   SubmitRequirementExpressionInfo,
@@ -1231,3 +1232,11 @@
 export function createQuickLabelInfo(): QuickLabelInfo {
   return {};
 }
+
+export function createFixReplacementInfo(): FixReplacementInfo {
+  return {
+    path: 'test/path',
+    range: createRange(),
+    replacement: 'replacement',
+  };
+}
diff --git a/polygerrit-ui/app/utils/async-util_test.ts b/polygerrit-ui/app/utils/async-util_test.ts
index afc16d3..4383cbd 100644
--- a/polygerrit-ui/app/utils/async-util_test.ts
+++ b/polygerrit-ui/app/utils/async-util_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import {SinonFakeTimers} from 'sinon';
 import '../test/common-test-setup';
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 1324061..3cb1407 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -198,7 +198,10 @@
 }
 
 export function createNewReply(
-  replyingTo: CommentInfo,
+  replyingTo: Pick<
+    CommentInfo,
+    'id' | 'path' | 'patch_set' | 'line' | 'range' | 'side' | 'parent'
+  >,
   message: string,
   unresolved: boolean
 ): DraftInfo {
diff --git a/polygerrit-ui/app/utils/date-util_test.ts b/polygerrit-ui/app/utils/date-util_test.ts
index 8d16655..45cdc25 100644
--- a/polygerrit-ui/app/utils/date-util_test.ts
+++ b/polygerrit-ui/app/utils/date-util_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2020 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {Timestamp} from '../types/common';
 import '../test/common-test-setup';
 import {
diff --git a/polygerrit-ui/app/workers/service-worker-class_test.ts b/polygerrit-ui/app/workers/service-worker-class_test.ts
index 33a19d9..5368911 100644
--- a/polygerrit-ui/app/workers/service-worker-class_test.ts
+++ b/polygerrit-ui/app/workers/service-worker-class_test.ts
@@ -3,6 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import * as sinon from 'sinon';
 import {assert} from '@open-wc/testing';
 import {Timestamp} from '../api/rest-api';
 import '../test/common-test-setup';
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index 3c80fc3..e196c10 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,3 +1,5 @@
+load("@rules_java//java:defs.bzl", "JavaInfo")
+
 def _classpath_collector(ctx):
     all = []
     for d in ctx.attr.deps:
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 5aba90e..131254e 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -14,6 +14,8 @@
 
 # Javadoc rule.
 
+load("@rules_java//java:defs.bzl", "JavaInfo", "java_common")
+
 def _impl(ctx):
     zip_output = ctx.outputs.zip
 
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 52fa1dd..1c3444e 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -14,6 +14,7 @@
 
 # War packaging.
 
+load("@rules_java//java:defs.bzl", "JavaInfo")
 load("//tools:deps.bzl", "AUTO_VALUE_GSON_VERSION")
 load("//tools:nongoogle.bzl", "AUTO_FACTORY_VERSION", "AUTO_VALUE_VERSION")
 
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 774f040..2de37d5 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -10,7 +10,7 @@
 GREENMAIL_VERS = "1.5.5"
 MAIL_VERS = "1.6.0"
 MIME4J_VERS = "0.8.1"
-OW2_VERS = "9.2"
+OW2_VERS = "9.7"
 AUTO_VALUE_GSON_VERSION = "1.3.1"
 PROLOG_VERS = "1.4.4"
 PROLOG_REPO = GERRIT
@@ -246,31 +246,31 @@
     maven_jar(
         name = "ow2-asm",
         artifact = "org.ow2.asm:asm:" + OW2_VERS,
-        sha1 = "81a03f76019c67362299c40e0ba13405f5467bff",
+        sha1 = "073d7b3086e14beb604ced229c302feff6449723",
     )
 
     maven_jar(
         name = "ow2-asm-analysis",
         artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS,
-        sha1 = "7487dd756daf96cab9986e44b9d7bcb796a61c10",
+        sha1 = "e4a258b7eb96107106c0599f0061cfc1832fe07a",
     )
 
     maven_jar(
         name = "ow2-asm-commons",
         artifact = "org.ow2.asm:asm-commons:" + OW2_VERS,
-        sha1 = "f4d7f0fc9054386f2893b602454d48e07d4fbead",
+        sha1 = "e86dda4696d3c185fcc95d8d311904e7ce38a53f",
     )
 
     maven_jar(
         name = "ow2-asm-tree",
         artifact = "org.ow2.asm:asm-tree:" + OW2_VERS,
-        sha1 = "d96c99a30f5e1a19b0e609dbb19a44d8518ac01e",
+        sha1 = "e446a17b175bfb733b87c5c2560ccb4e57d69f1a",
     )
 
     maven_jar(
         name = "ow2-asm-util",
         artifact = "org.ow2.asm:asm-util:" + OW2_VERS,
-        sha1 = "fbc178fc5ba3dab50fd7e8a5317b8b647c8e8946",
+        sha1 = "c0655519f24d92af2202cb681cd7c1569df6ead6",
     )
 
     maven_jar(
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 603e6c8..366f22c 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.11.0-SNAPSHOT</version>
+  <version>3.12.0-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 241d59b..59f9016 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.11.0-SNAPSHOT</version>
+  <version>3.12.0-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 00132bd..c549677 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.11.0-SNAPSHOT</version>
+  <version>3.12.0-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 5a21ab7..9b26260 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.11.0-SNAPSHOT</version>
+  <version>3.12.0-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/version.bzl b/version.bzl
index 181159e..8d942e01 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.11.0-SNAPSHOT"
+GERRIT_VERSION = "3.12.0-SNAPSHOT"