Merge "Expand INFO check chips, if there are no errors and running runs"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 31c3536..bc70451 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -951,15 +951,6 @@
 can always edit or remove hashtags (even without having the `Edit Hashtags`
 access right assigned).
 
-[[category_edit_assigned_to]]
-=== Edit Assignee
-
-This category permits users to set who is assigned to a change that is
-uploaded for review.
-
-The change owner, ref owners, and the user currently assigned to a change
-can always change the assignee.
-
 [[example_roles]]
 == Examples of typical roles in a project
 
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 5fd0bfc..2456662 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -58,21 +58,6 @@
 
 [[events]]
 == EVENTS
-=== Assignee Changed
-
-Sent when the assignee of a change has been modified.
-
-type:: "assignee-changed"
-
-change:: link:json.html#change[change attribute]
-
-changer:: link:json.html#account[account attribute]
-
-oldAssignee:: Assignee before it was changed.
-
-eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
-created.
-
 === Change Abandoned
 
 Sent when a change has been abandoned.
diff --git a/Documentation/concept-changes.txt b/Documentation/concept-changes.txt
index 6e76a8a..323b32a 100644
--- a/Documentation/concept-changes.txt
+++ b/Documentation/concept-changes.txt
@@ -39,10 +39,6 @@
 `git push` command, or the user that triggered the patch set creation through
 an action in the UI).
 
-|Assignee
-|The contributor responsible for the change. Often used when a change has
-mulitple reviewers to identify the individual responsible for final approval.
-
 |Reviewers
 |A list of one or more contributors responsible for reviewing the change.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8d30db2..9e76efe 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1415,20 +1415,6 @@
 +
 The default is false.
 
-[[change.enableAttentionSet]]change.enableAttentionSet::
-+
-If set to true, then all UI features for using and interacting with the
-attention set are enabled.
-+
-The default is true.
-
-[[change.enableAssignee]]change.enableAssignee::
-+
-If set to true, then all UI features for using and interacting with the
-assignee are enabled.
-+
-The default is false.
-
 [[change.maxComments]]change.maxComments::
 +
 Maximum number of comments (regular plus robot) allowed per change. Additional
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8bd5dc7..4f11ca8 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -139,12 +139,6 @@
 change being reverted.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
 ChangeFooter.
 
-=== SetAssignee.soy and SetAssigneeHtml.soy
-
-The SetAssignee templates will determine the contents of the email related to a
-user being assigned to a change. It is a `ChangeEmail`: see `ChangeSubject.soy`
-and ChangeFooter.
-
 
 == Mail Variables and Methods
 
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index d61dbc43..ba20cea 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -160,6 +160,22 @@
 link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank]
 is used for the evaluation of such patterns.
 
+[[operator_committeremail]]
+committeremail:'EMAIL_PATTERN'::
++
+An operator that returns true if the change committer's email address matches a
+specific regular expression pattern. The
+link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank]
+is used for the evaluation of such patterns.
+
+[[operator_uploaderemail]]
+uploaderemail:'EMAIL_PATTERN'::
++
+An operator that returns true if the change uploader's primary email address
+matches a specific regular expression pattern. The
+link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank]
+is used for the evaluation of such patterns.
+
 [[operator_distinctvoters]]
 distinctvoters:'[Label1,Label2,...,LabelN],value=MAX,count>1'::
 +
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index 56c9ecd..b0149fe 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -100,13 +100,6 @@
 E.g. a plugin could use this to enforce a certain name scheme for
 group names.
 
-[[assignee-validation]]
-== Assignee validation
-
-
-Plugins implementing the `AssigneeValidationListener` interface can perform
-validation of assignees before they are assigned to a change.
-
 [[hashtag-validation]]
 == Hashtag validation
 
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 89b88aa..ee2c289 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -22,12 +22,13 @@
 . link:intro-project-owner.html[Project Owner Guide]
 . link:https://source.android.com/source/developing[Default Android Workflow,role=external,window=_blank] (external)
 
-== Tutorials
+== Features and Workflows
 . Web
 .. link:user-review-ui.html[Review UI Overview]
 .. link:user-search.html[Searching Changes]
 .. link:user-inline-edit.html[Manipulating Changes in Browser]
 .. link:user-notify.html[Subscribing to Email Notifications]
+.. link:user-attention-set.html[Attention Set]
 . SSH
 .. link:user-upload.html#ssh[SSH connection details]
 .. link:cmd-index.html[Command Line Tools]
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 114aa3a..f685af9 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -603,39 +603,6 @@
 ----
 
 
-[[ba-linkify]]
-ba-linkify
-
-* ba-linkify
-
-[[ba-linkify_license]]
-----
-Copyright (c) 2009 "Cowboy" Ben Alman

-

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

-obtaining a copy of this software and associated documentation

-files (the "Software"), to deal in the Software without

-restriction, including without limitation the rights to use,

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

-copies of the Software, and to permit persons to whom the

-Software is furnished to do so, subject to the following

-conditions:

-

-The above copyright notice and this permission notice shall be

-included in all copies or substantial portions of the Software.

-

-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

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

-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND

-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT

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

-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

-OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[codemirror-minified]]
 codemirror-minified
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index f8ca85b..be41d0c 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3507,39 +3507,6 @@
 ----
 
 
-[[ba-linkify]]
-ba-linkify
-
-* ba-linkify
-
-[[ba-linkify_license]]
-----
-Copyright (c) 2009 "Cowboy" Ben Alman

-

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

-obtaining a copy of this software and associated documentation

-files (the "Software"), to deal in the Software without

-restriction, including without limitation the rights to use,

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

-copies of the Software, and to permit persons to whom the

-Software is furnished to do so, subject to the following

-conditions:

-

-The above copyright notice and this permission notice shall be

-included in all copies or substantial portions of the Software.

-

-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

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

-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND

-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT

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

-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

-OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[codemirror-minified]]
 codemirror-minified
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 46724e5..b761687 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1125,154 +1125,6 @@
   HTTP/1.1 204 No Content
 ----
 
-[[get-assignee]]
-=== Get Assignee
---
-'GET /changes/link:#change-id[\{change-id\}]/assignee'
---
-
-Retrieves the account of the user assigned to a change.
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/assignee HTTP/1.0
-----
-
-As a response an link:rest-api-accounts.html#account-info[AccountInfo] entity
-describing the assigned account is returned.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "_account_id": 1000096,
-    "name": "John Doe",
-    "email": "john.doe@example.com",
-    "username": "jdoe"
-  }
-----
-
-If the change has no assignee the response is "`204 No Content`".
-
-[[get-past-assignees]]
-=== Get Past Assignees
---
-'GET /changes/link:#change-id[\{change-id\}]/past_assignees'
---
-
-Returns a list of every user ever assigned to a change, in the order in which
-they were first assigned.
-
-.Request
-----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/past_assignees HTTP/1.0
-----
-
-As a response a list of link:rest-api-accounts.html#account-info[AccountInfo]
-entities is returned.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    {
-      "_account_id": 1000051,
-      "name": "Jane Doe",
-      "email": "jane.doe@example.com",
-      "username": "janed"
-    },
-    {
-      "_account_id": 1000096,
-      "name": "John Doe",
-      "email": "john.doe@example.com",
-      "username": "jdoe"
-    }
-  ]
-
-----
-
-
-[[set-assignee]]
-=== Set Assignee
---
-'PUT /changes/link:#change-id[\{change-id\}]/assignee'
---
-
-Sets the assignee of a change.
-
-The new assignee must be provided in the request body inside a
-link:#assignee-input[AssigneeInput] entity.
-
-.Request
-----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/assignee HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "assignee": "jdoe"
-  }
-----
-
-As a response an link:rest-api-accounts.html#account-info[AccountInfo] entity
-describing the assigned account is returned.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "_account_id": 1000096,
-    "name": "John Doe",
-    "email": "john.doe@example.com",
-    "username": "jdoe"
-  }
-----
-
-[[delete-assignee]]
-=== Delete Assignee
---
-'DELETE /changes/link:#change-id[\{change-id\}]/assignee'
---
-
-Deletes the assignee of a change.
-
-
-.Request
-----
-  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/assignee HTTP/1.0
-----
-
-As a response an link:rest-api-accounts.html#account-info[AccountInfo] entity
-describing the account of the deleted assignee is returned.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "_account_id": 1000096,
-    "name": "John Doe",
-    "email": "john.doe@example.com",
-    "username": "jdoe"
-  }
-----
-
-If the change had no assignee the response is "`204 No Content`".
-
 [[get-pure-revert]]
 === Get Pure Revert
 --
@@ -6918,18 +6770,6 @@
 If true, this vote was made after the change was submitted.
 |===========================
 
-[[assignee-input]]
-=== AssigneeInput
-The `AssigneeInput` entity contains the identity of the user to be set as assignee.
-
-[options="header",cols="1,^1,5"]
-|===========================
-|Field Name    ||Description
-|`assignee`     ||
-The link:rest-api-accounts.html#account-id[ID] of one account that
-should be added as assignee.
-|===========================
-
 [[attention-set-info]]
 === AttentionSetInfo
 The `AttentionSetInfo` entity contains details of users that are in
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 505def0..9f3ef6d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1575,10 +1575,6 @@
 configuration parameter] that controls whether the mergeability bit in
 link:rest-api-changes.html#change-info[ChangeInfo] will never be set and if the
 bit is indexed.
-|`enable_attention_set` |defaults to `false`|
-Returns true if attention set UI features are enabled.
-|`enable_assignee` |defaults to `true`|
-Returns true if assignee related UI features are enabled.
 |=============================
 
 [[change-index-config-info]]
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 5e5d3f8..738205a 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -164,13 +164,7 @@
 
 === For Gerrit Admins
 
-The Attention Set has been available since the 3.3 release (late 2020). It
-is enabled by default, but you can disable it by setting
-link:config-gerrit.html#change.enableAttentionSet[enableAttentionSet] to false.
-
-As part of Gerrit 3.3 upgrade, the user group "Non-Interactive Users" is
-renamed "Service Users". For a new installation, the group is automatically
-created upon initialization.
+The Attention Set has been available since the 3.3 release (late 2020).
 
 === Important note for all host owners, project owners, and bot owners
 
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 24c35f0..d2a22a7 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -181,7 +181,6 @@
 * newpatchset
 * restore
 * revert
-* setassignee
 
 [[Gerrit-Change-Id]]Gerrit-Change-Id::
 
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 780d3ec..39929e1 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -116,7 +116,7 @@
 need to vote/review. If the CC'ed user votes they are moved to reviewers.
 +
 
-- [[attention-set]]Attention set:
+- [[attention-set]]link:user-attention-set.html[Attention set]:
 +
 Users in attention set are marked by "chevron" symbol (see screenshot above).
 The mark indicates that there are actions their attention is required on the
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index e12c27c..32cb639 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -85,11 +85,6 @@
 means 'everything older than 2 days' while `-age:2d` means
 'everything with an age of at most 2 days'.
 
-[[assignee]]
-assignee:'USER'::
-+
-Changes assigned to the given user.
-
 [[attention]]
 attention:'USER'::
 +
@@ -485,20 +480,12 @@
 
 
 [[is]]
-is:assigned::
-+
-True if the change has an assignee.
-
 [[is-starred]]
 is:starred::
 +
 Same as 'has:star', true if the change has been starred by the
 current user with the default label.
 
-is:unassigned::
-+
-True if the change does not have an assignee.
-
 is:attention::
 +
 True if the change has attention by the current user.
diff --git a/java/com/google/gerrit/common/PageLinks.java b/java/com/google/gerrit/common/PageLinks.java
index 38de5b1..836eb32 100644
--- a/java/com/google/gerrit/common/PageLinks.java
+++ b/java/com/google/gerrit/common/PageLinks.java
@@ -106,10 +106,6 @@
     return toChangeQuery(op("owner", fullname) + " " + status(status));
   }
 
-  public static String toAssigneeQuery(String fullname) {
-    return toChangeQuery(op("assignee", fullname));
-  }
-
   public static String toCustomDashboard(String params) {
     return "/dashboard/?" + params;
   }
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index d029fad..6a50711 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -35,7 +35,6 @@
   public static final String DELETE = "delete";
   public static final String DELETE_CHANGES = "deleteChanges";
   public static final String DELETE_OWN_CHANGES = "deleteOwnChanges";
-  public static final String EDIT_ASSIGNEE = "editAssignee";
   public static final String EDIT_HASHTAGS = "editHashtags";
   public static final String EDIT_TOPIC_NAME = "editTopicName";
   public static final String FORGE_AUTHOR = "forgeAuthor";
@@ -73,7 +72,6 @@
     NAMES_LC.add(DELETE.toLowerCase());
     NAMES_LC.add(DELETE_CHANGES.toLowerCase());
     NAMES_LC.add(DELETE_OWN_CHANGES.toLowerCase());
-    NAMES_LC.add(EDIT_ASSIGNEE.toLowerCase());
     NAMES_LC.add(EDIT_HASHTAGS.toLowerCase());
     NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
     NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
diff --git a/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java b/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java
deleted file mode 100644
index e17e1c9..0000000
--- a/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.api.changes;
-
-import com.google.gerrit.extensions.restapi.DefaultInput;
-
-public class AssigneeInput {
-  @DefaultInput public String assignee;
-}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 0ebb859..ef61b68 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -347,22 +347,6 @@
   /** Adds a user to the attention set. */
   AccountInfo addToAttentionSet(AttentionSetInput input) throws RestApiException;
 
-  /** Set the assignee of a change. */
-  AccountInfo setAssignee(AssigneeInput input) throws RestApiException;
-
-  /** Get the assignee of a change. */
-  AccountInfo getAssignee() throws RestApiException;
-
-  /** Get all past assignees. */
-  List<AccountInfo> getPastAssignees() throws RestApiException;
-
-  /**
-   * Delete the assignee of a change.
-   *
-   * @return the assignee that was deleted, or null if there was no assignee.
-   */
-  AccountInfo deleteAssignee() throws RestApiException;
-
   /**
    * Get all published comments on a change.
    *
@@ -746,26 +730,6 @@
     }
 
     @Override
-    public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public AccountInfo getAssignee() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public List<AccountInfo> getPastAssignees() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public AccountInfo deleteAssignee() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     @Deprecated
     public Map<String, List<CommentInfo>> comments() throws RestApiException {
       throw new NotImplementedException();
diff --git a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
index fc09b49..0142e01 100644
--- a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -22,6 +22,5 @@
   public int updateDelay;
   public Boolean submitWholeTopic;
   public String mergeabilityComputationBehavior;
-  public Boolean enableAttentionSet;
   public Boolean enableAssignee;
 }
diff --git a/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java b/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java
deleted file mode 100644
index 7fc0f03..0000000
--- a/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.events;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
-
-/** Notified whenever a change assignee is changed. */
-@ExtensionPoint
-public interface AssigneeChangedListener {
-  interface Event extends ChangeEvent {
-    @Nullable
-    AccountInfo getOldAssignee();
-  }
-
-  void onAssigneeChanged(Event event);
-}
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 5cf63d9..bb3b6d5 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -58,25 +58,21 @@
   // polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
   public static final String DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY = "has:draft limit:10";
   public static final String YOUR_TURN = "attention:${user} limit:25";
-  public static final String DASHBOARD_ASSIGNED_QUERY =
-      "assignee:${user} (-is:wip OR " + "owner:self OR assignee:self) is:open limit:25";
   public static final String DASHBOARD_WORK_IN_PROGRESS_QUERY =
       "is:open owner:${user} is:wip limit:25";
   public static final String DASHBOARD_OUTGOING_QUERY = "is:open owner:${user} -is:wip limit:25";
   public static final String DASHBOARD_INCOMING_QUERY =
-      "is:open -owner:${user} -is:wip (reviewer:${user} OR assignee:${user}) limit:25";
+      "is:open -owner:${user} -is:wip reviewer:${user} limit:25";
   public static final String CC_QUERY = "is:open -is:wip cc:${user} limit:10";
   public static final String DASHBOARD_RECENTLY_CLOSED_QUERY =
       "is:closed (-is:wip OR owner:self) "
-          + "(owner:${user} OR reviewer:${user} OR assignee:${user} "
-          + "OR cc:${user}) -age:4w limit:10";
+          + "(owner:${user} OR reviewer:${user} OR cc:${user}) "
+          + "-age:4w limit:10";
   public static final String NEW_USER = "owner:${user} limit:1";
 
   public static final String SELF_DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY =
       DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY.replaceAll("\\$\\{user}", "self");
   public static final String SELF_YOUR_TURN = YOUR_TURN.replaceAll("\\$\\{user}", "self");
-  public static final String SELF_DASHBOARD_ASSIGNED_QUERY =
-      DASHBOARD_ASSIGNED_QUERY.replaceAll("\\$\\{user}", "self");
   public static final ImmutableList<String> SELF_DASHBOARD_QUERIES =
       Stream.of(
               DASHBOARD_WORK_IN_PROGRESS_QUERY,
@@ -195,31 +191,11 @@
   public static List<String> computeDashboardQueryList(Server serverApi) throws RestApiException {
     List<String> queryList = new ArrayList<>();
     queryList.add(SELF_DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY);
-    if (isEnabledAttentionSet(serverApi)) {
-      queryList.add(SELF_YOUR_TURN);
-    }
-    if (isEnabledAssignee(serverApi)) {
-      queryList.add(SELF_DASHBOARD_ASSIGNED_QUERY);
-    }
-
+    queryList.add(SELF_YOUR_TURN);
     queryList.addAll(SELF_DASHBOARD_QUERIES);
 
     return queryList;
   }
 
-  private static boolean isEnabledAttentionSet(Server serverApi) throws RestApiException {
-    return serverApi.getInfo() != null
-        && serverApi.getInfo().change != null
-        && serverApi.getInfo().change.enableAttentionSet != null
-        && serverApi.getInfo().change.enableAttentionSet;
-  }
-
-  private static boolean isEnabledAssignee(Server serverApi) throws RestApiException {
-    return serverApi.getInfo() != null
-        && serverApi.getInfo().change != null
-        && serverApi.getInfo().change.enableAssignee != null
-        && serverApi.getInfo().change.enableAssignee;
-  }
-
   private IndexPreloadingUtil() {}
 }
diff --git a/java/com/google/gerrit/mail/MailHeader.java b/java/com/google/gerrit/mail/MailHeader.java
index 2700f81..6933140 100644
--- a/java/com/google/gerrit/mail/MailHeader.java
+++ b/java/com/google/gerrit/mail/MailHeader.java
@@ -17,7 +17,6 @@
 /** Variables used by emails to hold data */
 public enum MailHeader {
   // Gerrit metadata holders
-  ASSIGNEE("Gerrit-Assignee"),
   ATTENTION("Gerrit-Attention"),
   BRANCH("Gerrit-Branch"),
   CC("Gerrit-CC"),
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 236d185..a057e66 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -144,8 +144,6 @@
     extractMailExample("RestoredHtml.soy");
     extractMailExample("Reverted.soy");
     extractMailExample("RevertedHtml.soy");
-    extractMailExample("SetAssignee.soy");
-    extractMailExample("SetAssigneeHtml.soy");
 
     if (!ui.isBatch()) {
       System.err.println();
diff --git a/java/com/google/gerrit/server/AssigneeStatusUpdate.java b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
deleted file mode 100644
index 812aad1..0000000
--- a/java/com/google/gerrit/server/AssigneeStatusUpdate.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import com.google.auto.value.AutoValue;
-import com.google.gerrit.entities.Account;
-import java.time.Instant;
-import java.util.Optional;
-
-/** Change to an assignee's status. */
-@AutoValue
-public abstract class AssigneeStatusUpdate {
-  public static AssigneeStatusUpdate create(
-      Instant ts, Account.Id updatedBy, Optional<Account.Id> currentAssignee) {
-    return new AutoValue_AssigneeStatusUpdate(ts, updatedBy, currentAssignee);
-  }
-
-  public abstract Instant date();
-
-  public abstract Account.Id updatedBy();
-
-  public abstract Optional<Account.Id> currentAssignee();
-}
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index c84c0e7..400da58 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -40,8 +40,6 @@
   public static final String TAG_ABANDON = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "abandon";
   public static final String TAG_CHERRY_PICK_CHANGE =
       AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "cherryPickChange";
-  public static final String TAG_DELETE_ASSIGNEE =
-      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteAssignee";
   public static final String TAG_DELETE_REVIEWER =
       AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteReviewer";
   public static final String TAG_DELETE_VOTE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteVote";
@@ -49,7 +47,6 @@
   public static final String TAG_MOVE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "move";
   public static final String TAG_RESTORE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "restore";
   public static final String TAG_REVERT = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "revert";
-  public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setAssignee";
   public static final String TAG_UPDATE_ATTENTION_SET =
       AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "updateAttentionSet";
   public static final String TAG_SET_DESCRIPTION =
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 66a845a..4fba660 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.ApplyPatchPatchSetInput;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
 import com.google.gerrit.extensions.api.changes.AttentionSetApi;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -78,14 +77,11 @@
 import com.google.gerrit.server.restapi.change.Check;
 import com.google.gerrit.server.restapi.change.CheckSubmitRequirement;
 import com.google.gerrit.server.restapi.change.CreateMergePatchSet;
-import com.google.gerrit.server.restapi.change.DeleteAssignee;
 import com.google.gerrit.server.restapi.change.DeleteChange;
 import com.google.gerrit.server.restapi.change.DeletePrivate;
-import com.google.gerrit.server.restapi.change.GetAssignee;
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.GetHashtags;
 import com.google.gerrit.server.restapi.change.GetMetaDiff;
-import com.google.gerrit.server.restapi.change.GetPastAssignees;
 import com.google.gerrit.server.restapi.change.GetPureRevert;
 import com.google.gerrit.server.restapi.change.GetTopic;
 import com.google.gerrit.server.restapi.change.Index;
@@ -97,7 +93,6 @@
 import com.google.gerrit.server.restapi.change.PostHashtags;
 import com.google.gerrit.server.restapi.change.PostPrivate;
 import com.google.gerrit.server.restapi.change.PostReviewers;
-import com.google.gerrit.server.restapi.change.PutAssignee;
 import com.google.gerrit.server.restapi.change.PutMessage;
 import com.google.gerrit.server.restapi.change.PutTopic;
 import com.google.gerrit.server.restapi.change.Rebase;
@@ -159,10 +154,6 @@
   private final AttentionSet attentionSet;
   private final AttentionSetApiImpl.Factory attentionSetApi;
   private final AddToAttentionSet addToAttentionSet;
-  private final PutAssignee putAssignee;
-  private final GetAssignee getAssignee;
-  private final GetPastAssignees getPastAssignees;
-  private final DeleteAssignee deleteAssignee;
   private final Provider<ListChangeComments> listCommentsProvider;
   private final ListChangeRobotComments listChangeRobotComments;
   private final Provider<ListChangeDrafts> listDraftsProvider;
@@ -213,10 +204,6 @@
       AttentionSet attentionSet,
       AttentionSetApiImpl.Factory attentionSetApi,
       AddToAttentionSet addToAttentionSet,
-      PutAssignee putAssignee,
-      GetAssignee getAssignee,
-      GetPastAssignees getPastAssignees,
-      DeleteAssignee deleteAssignee,
       Provider<ListChangeComments> listCommentsProvider,
       ListChangeRobotComments listChangeRobotComments,
       Provider<ListChangeDrafts> listDraftsProvider,
@@ -265,10 +252,6 @@
     this.attentionSet = attentionSet;
     this.attentionSetApi = attentionSetApi;
     this.addToAttentionSet = addToAttentionSet;
-    this.putAssignee = putAssignee;
-    this.getAssignee = getAssignee;
-    this.getPastAssignees = getPastAssignees;
-    this.deleteAssignee = deleteAssignee;
     this.listCommentsProvider = listCommentsProvider;
     this.listChangeRobotComments = listChangeRobotComments;
     this.listDraftsProvider = listDraftsProvider;
@@ -603,46 +586,6 @@
   }
 
   @Override
-  public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
-    try {
-      return putAssignee.apply(change, input).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot set assignee", e);
-    }
-  }
-
-  @Nullable
-  @Override
-  public AccountInfo getAssignee() throws RestApiException {
-    try {
-      Response<AccountInfo> r = getAssignee.apply(change);
-      return r.isNone() ? null : r.value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot get assignee", e);
-    }
-  }
-
-  @Override
-  public List<AccountInfo> getPastAssignees() throws RestApiException {
-    try {
-      return getPastAssignees.apply(change).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot get past assignees", e);
-    }
-  }
-
-  @Nullable
-  @Override
-  public AccountInfo deleteAssignee() throws RestApiException {
-    try {
-      Response<AccountInfo> r = deleteAssignee.apply(change, null);
-      return r.isNone() ? null : r.value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot delete assignee", e);
-    }
-  }
-
-  @Override
   public CommentsRequest commentsRequest() {
     return new CommentsRequest() {
       @Override
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
deleted file mode 100644
index fd3e972..0000000
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.AssigneeChanged;
-import com.google.gerrit.server.mail.send.MessageIdGenerator;
-import com.google.gerrit.server.mail.send.SetAssigneeSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.PostUpdateContext;
-import com.google.gerrit.server.util.AccountTemplateUtil;
-import com.google.gerrit.server.validators.AssigneeValidationListener;
-import com.google.gerrit.server.validators.ValidationException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-
-public class SetAssigneeOp implements BatchUpdateOp {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  public interface Factory {
-    SetAssigneeOp create(IdentifiedUser assignee);
-  }
-
-  private final ChangeMessagesUtil cmUtil;
-  private final PluginSetContext<AssigneeValidationListener> validationListeners;
-  private final IdentifiedUser newAssignee;
-  private final AssigneeChanged assigneeChanged;
-  private final SetAssigneeSender.Factory setAssigneeSenderFactory;
-  private final Provider<IdentifiedUser> user;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final MessageIdGenerator messageIdGenerator;
-
-  private Change change;
-  private IdentifiedUser oldAssignee;
-
-  @Inject
-  SetAssigneeOp(
-      ChangeMessagesUtil cmUtil,
-      PluginSetContext<AssigneeValidationListener> validationListeners,
-      AssigneeChanged assigneeChanged,
-      SetAssigneeSender.Factory setAssigneeSenderFactory,
-      Provider<IdentifiedUser> user,
-      IdentifiedUser.GenericFactory userFactory,
-      MessageIdGenerator messageIdGenerator,
-      @Assisted IdentifiedUser newAssignee) {
-    this.cmUtil = cmUtil;
-    this.validationListeners = validationListeners;
-    this.assigneeChanged = assigneeChanged;
-    this.setAssigneeSenderFactory = setAssigneeSenderFactory;
-    this.user = user;
-    this.userFactory = userFactory;
-    this.messageIdGenerator = messageIdGenerator;
-    this.newAssignee = requireNonNull(newAssignee, "assignee");
-  }
-
-  @Override
-  public boolean updateChange(ChangeContext ctx) throws RestApiException {
-    change = ctx.getChange();
-    if (newAssignee.getAccountId().equals(change.getAssignee())) {
-      return false;
-    }
-    try {
-      validationListeners.runEach(
-          l -> l.validateAssignee(change, newAssignee.getAccount()), ValidationException.class);
-    } catch (ValidationException e) {
-      throw new ResourceConflictException(e.getMessage(), e);
-    }
-
-    if (change.getAssignee() != null) {
-      oldAssignee = userFactory.create(change.getAssignee());
-    }
-
-    ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
-    // notedb
-    update.setAssignee(newAssignee.getAccountId());
-    // reviewdb
-    change.setAssignee(newAssignee.getAccountId());
-    addMessage(ctx);
-    return true;
-  }
-
-  private void addMessage(ChangeContext ctx) {
-    StringBuilder msg = new StringBuilder();
-    msg.append("Assignee ");
-    if (oldAssignee == null) {
-      msg.append("added: ");
-      msg.append(AccountTemplateUtil.getAccountTemplate(newAssignee.getAccountId()));
-    } else {
-      msg.append("changed from: ");
-      msg.append(AccountTemplateUtil.getAccountTemplate(oldAssignee.getAccountId()));
-      msg.append(" to: ");
-      msg.append(AccountTemplateUtil.getAccountTemplate(newAssignee.getAccountId()));
-    }
-    cmUtil.setChangeMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_SET_ASSIGNEE);
-  }
-
-  @Override
-  public void postUpdate(PostUpdateContext ctx) {
-    try {
-      SetAssigneeSender emailSender =
-          setAssigneeSenderFactory.create(
-              change.getProject(), change.getId(), newAssignee.getAccountId());
-      emailSender.setFrom(user.get().getAccountId());
-      emailSender.setMessageId(
-          messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-      emailSender.send();
-    } catch (Exception err) {
-      logger.atSevere().withCause(err).log(
-          "Cannot send email to new assignee of change %s", change.getId());
-    }
-    assigneeChanged.fire(
-        ctx.getChangeData(change),
-        ctx.getAccount(),
-        oldAssignee != null ? oldAssignee.state() : null,
-        ctx.getWhen());
-  }
-}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index f194434..ae6dcfd 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.extensions.events.AccountActivationListener;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.AgreementSignupListener;
-import com.google.gerrit.extensions.events.AssigneeChangedListener;
 import com.google.gerrit.extensions.events.AttentionSetListener;
 import com.google.gerrit.extensions.events.ChangeAbandonedListener;
 import com.google.gerrit.extensions.events.ChangeDeletedListener;
@@ -222,7 +221,6 @@
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.validators.AccountActivationValidationListener;
-import com.google.gerrit.server.validators.AssigneeValidationListener;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
 import com.google.gerrit.server.validators.HashtagValidationListener;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
@@ -360,7 +358,6 @@
     DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), GitBatchRefUpdateListener.class);
-    DynamicSet.setOf(binder(), AssigneeChangedListener.class);
     DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
     DynamicSet.setOf(binder(), ChangeDeletedListener.class);
     DynamicSet.setOf(binder(), CommentAddedListener.class);
@@ -441,7 +438,6 @@
     DynamicSet.setOf(binder(), AccountExternalIdCreator.class);
     DynamicSet.setOf(binder(), WebUiPlugin.class);
     DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
-    DynamicSet.setOf(binder(), AssigneeValidationListener.class);
     DynamicSet.setOf(binder(), ActionVisitor.class);
     DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
     DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
diff --git a/java/com/google/gerrit/server/data/ChangeAttribute.java b/java/com/google/gerrit/server/data/ChangeAttribute.java
index ec27c0c..cdc982f 100644
--- a/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -32,7 +32,6 @@
   public int number;
   public String subject;
   public AccountAttribute owner;
-  public AccountAttribute assignee;
   public String url;
   public String commitMessage;
   public List<String> hashtags;
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 903a4c0..ad0dd8b 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -34,12 +34,14 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
 import com.google.gerrit.server.edit.tree.DeleteFileModification;
 import com.google.gerrit.server.edit.tree.RenameFileModification;
 import com.google.gerrit.server.edit.tree.RestoreFileModification;
 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.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
@@ -108,15 +110,15 @@
       PermissionBackend permissionBackend,
       ChangeEditUtil changeEditUtil,
       PatchSetUtil patchSetUtil,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      GitReferenceUpdated gitReferenceUpdated) {
     this.currentUser = currentUser;
     this.permissionBackend = permissionBackend;
     this.zoneId = gerritIdent.getZoneId();
     this.changeEditUtil = changeEditUtil;
     this.patchSetUtil = patchSetUtil;
     this.projectCache = projectCache;
-
-    noteDbEdits = new NoteDbEdits(zoneId, indexer, currentUser);
+    noteDbEdits = new NoteDbEdits(gitReferenceUpdated, zoneId, indexer, currentUser);
   }
 
   /**
@@ -173,10 +175,14 @@
               notes.getChangeId(), currentPatchSet.id()));
     }
 
-    rebase(repository, changeEdit, currentPatchSet);
+    rebase(notes.getProjectName(), repository, changeEdit, currentPatchSet);
   }
 
-  private void rebase(Repository repository, ChangeEdit changeEdit, PatchSet currentPatchSet)
+  private void rebase(
+      Project.NameKey project,
+      Repository repository,
+      ChangeEdit changeEdit,
+      PatchSet currentPatchSet)
       throws IOException, MergeConflictException, InvalidChangeOperationException {
     RevCommit currentEditCommit = changeEdit.getEditCommit();
     if (currentEditCommit.getParentCount() == 0) {
@@ -194,7 +200,13 @@
         createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
 
     noteDbEdits.baseEditOnDifferentPatchset(
-        repository, changeEdit, currentPatchSet, currentEditCommit, newEditCommitId, nowTimestamp);
+        project,
+        repository,
+        changeEdit,
+        currentPatchSet,
+        currentEditCommit,
+        newEditCommitId,
+        nowTimestamp);
   }
 
   /**
@@ -719,11 +731,17 @@
     private final ZoneId zoneId;
     private final ChangeIndexer indexer;
     private final Provider<CurrentUser> currentUser;
+    private final GitReferenceUpdated gitReferenceUpdated;
 
-    NoteDbEdits(ZoneId zoneId, ChangeIndexer indexer, Provider<CurrentUser> currentUser) {
+    NoteDbEdits(
+        GitReferenceUpdated gitReferenceUpdated,
+        ZoneId zoneId,
+        ChangeIndexer indexer,
+        Provider<CurrentUser> currentUser) {
       this.zoneId = zoneId;
       this.indexer = indexer;
       this.currentUser = currentUser;
+      this.gitReferenceUpdated = gitReferenceUpdated;
     }
 
     ChangeEdit createEdit(
@@ -753,6 +771,10 @@
       return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchset.id());
     }
 
+    private AccountState getUpdater() {
+      return currentUser.get().asIdentifiedUser().state();
+    }
+
     ChangeEdit updateEdit(
         Project.NameKey projectName,
         Repository repository,
@@ -795,9 +817,11 @@
           throw new IOException(message);
         }
       }
+      gitReferenceUpdated.fire(projectName, ru, getUpdater());
     }
 
     void baseEditOnDifferentPatchset(
+        Project.NameKey project,
         Repository repository,
         ChangeEdit changeEdit,
         PatchSet currentPatchSet,
@@ -807,6 +831,7 @@
         throws IOException {
       String newEditRefName = getEditRefName(changeEdit.getChange(), currentPatchSet);
       updateReferenceWithNameChange(
+          project,
           repository,
           changeEdit.getRefName(),
           currentEditCommit,
@@ -817,6 +842,7 @@
     }
 
     private void updateReferenceWithNameChange(
+        Project.NameKey projectName,
         Repository repository,
         String currentRefName,
         ObjectId currentObjectId,
@@ -838,6 +864,7 @@
           throw new IOException("failed: " + cmd);
         }
       }
+      gitReferenceUpdated.fire(projectName, batchRefUpdate, getUpdater());
     }
 
     static RevCommit lookupCommit(Repository repository, ObjectId commitId) throws IOException {
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 74834ab..3474590 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -70,6 +71,8 @@
   private final ChangeKindCache changeKindCache;
   private final PatchSetUtil psUtil;
 
+  private final GitReferenceUpdated gitReferenceUpdated;
+
   @Inject
   ChangeEditUtil(
       GitRepositoryManager gitManager,
@@ -77,13 +80,15 @@
       ChangeIndexer indexer,
       Provider<CurrentUser> userProvider,
       ChangeKindCache changeKindCache,
-      PatchSetUtil psUtil) {
+      PatchSetUtil psUtil,
+      GitReferenceUpdated gitReferenceUpdated) {
     this.gitManager = gitManager;
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.indexer = indexer;
     this.userProvider = userProvider;
     this.changeKindCache = changeKindCache;
     this.psUtil = psUtil;
+    this.gitReferenceUpdated = gitReferenceUpdated;
   }
 
   /**
@@ -237,7 +242,7 @@
     return writeSquashedCommit(rw, inserter, parent, edit);
   }
 
-  private static void deleteRef(Repository repo, ChangeEdit edit) throws IOException {
+  private void deleteRef(Repository repo, ChangeEdit edit) throws IOException {
     String refName = edit.getRefName();
     RefUpdate ru = repo.updateRef(refName, true);
     ru.setExpectedOldObjectId(edit.getEditCommit());
@@ -261,6 +266,10 @@
       default:
         throw new IOException(String.format("Failed to delete ref %s: %s", refName, result));
     }
+    gitReferenceUpdated.fire(
+        edit.getChange().getProject(),
+        ru,
+        /* updater= */ userProvider.get().asIdentifiedUser().state());
   }
 
   private static RevCommit writeSquashedCommit(
diff --git a/java/com/google/gerrit/server/events/AssigneeChangedEvent.java b/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
deleted file mode 100644
index 490d6d14..0000000
--- a/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.events;
-
-import com.google.common.base.Supplier;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.data.AccountAttribute;
-
-public class AssigneeChangedEvent extends ChangeEvent {
-  static final String TYPE = "assignee-changed";
-  public Supplier<AccountAttribute> changer;
-  public Supplier<AccountAttribute> oldAssignee;
-
-  public AssigneeChangedEvent(Change change) {
-    super(TYPE, change);
-  }
-}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index cd7e29a..14aadf47 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -133,7 +133,6 @@
     a.subject = change.getSubject();
     a.url = getChangeUrl(change);
     a.owner = asAccountAttribute(change.getOwner(), accountLoader);
-    a.assignee = asAccountAttribute(change.getAssignee(), accountLoader);
     a.status = change.getStatus();
     a.createdOn = change.getCreatedOn().getEpochSecond();
     a.wip = change.isWorkInProgress() ? true : null;
diff --git a/java/com/google/gerrit/server/events/EventTypes.java b/java/com/google/gerrit/server/events/EventTypes.java
index 229ef86..e24bbd2 100644
--- a/java/com/google/gerrit/server/events/EventTypes.java
+++ b/java/com/google/gerrit/server/events/EventTypes.java
@@ -23,7 +23,6 @@
   private static final Map<String, Class<?>> typesByString = new HashMap<>();
 
   static {
-    register(AssigneeChangedEvent.TYPE, AssigneeChangedEvent.class);
     register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class);
     register(ChangeDeletedEvent.TYPE, ChangeDeletedEvent.class);
     register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class);
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 18f3d7a..50c15b7 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.extensions.events.AssigneeChangedListener;
 import com.google.gerrit.extensions.events.ChangeAbandonedListener;
 import com.google.gerrit.extensions.events.ChangeDeletedListener;
 import com.google.gerrit.extensions.events.ChangeMergedListener;
@@ -73,8 +72,7 @@
 
 @Singleton
 public class StreamEventsApiListener
-    implements AssigneeChangedListener,
-        ChangeAbandonedListener,
+    implements ChangeAbandonedListener,
         ChangeDeletedListener,
         ChangeMergedListener,
         ChangeRestoredListener,
@@ -95,7 +93,6 @@
   public static class StreamEventsApiListenerModule extends AbstractModule {
     @Override
     protected void configure() {
-      DynamicSet.bind(binder(), AssigneeChangedListener.class).to(StreamEventsApiListener.class);
       DynamicSet.bind(binder(), ChangeAbandonedListener.class).to(StreamEventsApiListener.class);
       DynamicSet.bind(binder(), ChangeDeletedListener.class).to(StreamEventsApiListener.class);
       DynamicSet.bind(binder(), ChangeMergedListener.class).to(StreamEventsApiListener.class);
@@ -244,23 +241,6 @@
   }
 
   @Override
-  public void onAssigneeChanged(AssigneeChangedListener.Event ev) {
-    try {
-      ChangeNotes notes = getNotes(ev.getChange());
-      Change change = notes.getChange();
-      AssigneeChangedEvent event = new AssigneeChangedEvent(change);
-
-      event.change = changeAttributeSupplier(change, notes);
-      event.changer = accountAttributeSupplier(ev.getWho());
-      event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
-
-      dispatcher.run(d -> d.postEvent(change, event));
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("Failed to dispatch event");
-    }
-  }
-
-  @Override
   public void onTopicEdited(TopicEditedListener.Event ev) {
     try {
       ChangeNotes notes = getNotes(ev.getChange());
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
deleted file mode 100644
index 8e4d1e2..0000000
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.extensions.events;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.events.AssigneeChangedListener;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.plugincontext.PluginSetContext;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.time.Instant;
-
-/** Helper class to fire an event when a user has been set as assignee on a change. */
-@Singleton
-public class AssigneeChanged {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final PluginSetContext<AssigneeChangedListener> listeners;
-  private final EventUtil util;
-
-  @Inject
-  AssigneeChanged(PluginSetContext<AssigneeChangedListener> listeners, EventUtil util) {
-    this.listeners = listeners;
-    this.util = util;
-  }
-
-  public void fire(
-      ChangeData changeData, AccountState accountState, AccountState oldAssignee, Instant when) {
-    if (listeners.isEmpty()) {
-      return;
-    }
-    try {
-      Event event =
-          new Event(
-              util.changeInfo(changeData),
-              util.accountInfo(accountState),
-              util.accountInfo(oldAssignee),
-              when);
-      listeners.runEach(l -> l.onAssigneeChanged(event));
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("Couldn't fire event");
-    }
-  }
-
-  /** Event to be fired when a user has been set as assignee on a change. */
-  private static class Event extends AbstractChangeEvent implements AssigneeChangedListener.Event {
-    private final AccountInfo oldAssignee;
-
-    Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, Instant when) {
-      super(change, editor, when, NotifyHandling.ALL);
-      this.oldAssignee = oldAssignee;
-    }
-
-    @Override
-    public AccountInfo getOldAssignee() {
-      return oldAssignee;
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 2045dba..8e443f82 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -504,9 +504,9 @@
           ATTENTION_SET_FULL_FIELD.storedOnly(ChangeQueryBuilder.FIELD_ATTENTION_SET_FULL);
 
   /** The user assigned to the change. */
+  // The getter always returns NO_ASSIGNEE, since assignee field is deprecated.
   public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD =
-      IndexedField.<ChangeData>integerBuilder("Assignee")
-          .build(changeGetter(c -> c.getAssignee() != null ? c.getAssignee().get() : NO_ASSIGNEE));
+      IndexedField.<ChangeData>integerBuilder("Assignee").build(changeGetter(c -> NO_ASSIGNEE));
 
   public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC =
       ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE);
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 895c4d8..6ddf7a3 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -232,12 +232,21 @@
           .build();
 
   /** Add prefixsubject field. */
+  @Deprecated
   static final Schema<ChangeData> V81 =
       new Schema.Builder<ChangeData>()
           .add(V80)
           .addSearchSpecs(ChangeField.PREFIX_SUBJECT_SPEC)
           .build();
 
+  /** Remove assignee field. */
+  static final Schema<ChangeData> V82 =
+      new Schema.Builder<ChangeData>()
+          .add(V81)
+          .remove(ChangeField.ASSIGNEE_SPEC)
+          .remove(ChangeField.ASSIGNEE_FIELD)
+          .build();
+
   /**
    * Name of the change index to be used when contacting index backends or loading configurations.
    */
diff --git a/java/com/google/gerrit/server/mail/EmailModule.java b/java/com/google/gerrit/server/mail/EmailModule.java
index c659b5f..50f26bb 100644
--- a/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/java/com/google/gerrit/server/mail/EmailModule.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.mail.send.RestoredSender;
 import com.google.gerrit.server.mail.send.RevertedSender;
-import com.google.gerrit.server.mail.send.SetAssigneeSender;
 
 public class EmailModule extends FactoryModule {
   @Override
@@ -50,7 +49,6 @@
     factory(ReplacePatchSetSender.Factory.class);
     factory(RestoredSender.Factory.class);
     factory(RevertedSender.Factory.class);
-    factory(SetAssigneeSender.Factory.class);
     factory(AddToAttentionSetSender.Factory.class);
     factory(RemoveFromAttentionSetSender.Factory.class);
   }
diff --git a/java/com/google/gerrit/server/mail/EmailSettings.java b/java/com/google/gerrit/server/mail/EmailSettings.java
index 15b61d0..c411af5 100644
--- a/java/com/google/gerrit/server/mail/EmailSettings.java
+++ b/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -38,7 +38,6 @@
   public final Encryption encryption;
   public final long fetchInterval; // in milliseconds
   public final boolean sendNewPatchsetEmails;
-  public final boolean isAttentionSetEnabled;
 
   @Inject
   EmailSettings(@GerritServerConfig Config cfg) {
@@ -61,6 +60,5 @@
             TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS),
             TimeUnit.MILLISECONDS);
     sendNewPatchsetEmails = cfg.getBoolean("change", null, "sendNewPatchsetEmails", true);
-    isAttentionSetEnabled = cfg.getBoolean("change", null, "enableAttentionSet", true);
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 8be5548..7bbee2a 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -546,9 +546,6 @@
     footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + change.getChangeId());
     footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.number());
     footers.add(MailHeader.OWNER.withDelimiter() + getNameEmailFor(change.getOwner()));
-    if (change.getAssignee() != null) {
-      footers.add(MailHeader.ASSIGNEE.withDelimiter() + getNameEmailFor(change.getAssignee()));
-    }
     for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) {
       footers.add(MailHeader.REVIEWER.withDelimiter() + reviewer);
     }
@@ -558,8 +555,7 @@
     for (Account.Id attentionUser : currentAttentionSet) {
       footers.add(MailHeader.ATTENTION.withDelimiter() + getNameEmailFor(attentionUser));
     }
-    // Since this would be user visible, only show it if attention set is enabled
-    if (args.settings.isAttentionSetEnabled && !currentAttentionSet.isEmpty()) {
+    if (!currentAttentionSet.isEmpty()) {
       // We need names rather than account ids / emails to make it user readable.
       soyContext.put(
           "attentionSet",
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index 9d75abd..ce54708 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -88,8 +88,6 @@
     "RestoredHtml.soy",
     "Reverted.soy",
     "RevertedHtml.soy",
-    "SetAssignee.soy",
-    "SetAssigneeHtml.soy",
   };
 
   private static final SoySauce DEFAULT = getDefault().build().compileTemplates();
diff --git a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
deleted file mode 100644
index 29f4c69..0000000
--- a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.mail.send;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-/**
- * Sender that informs a user by email that they were set as assignee on a change.
- *
- * <p>In contrast to other change emails this email is not sent to the change authors (owner, patch
- * set uploader, author). This is why this class extends {@link ChangeEmail} directly, instead of
- * extending {@link ReplyToChangeSender}.
- */
-public class SetAssigneeSender extends ChangeEmail {
-  public interface Factory {
-    SetAssigneeSender create(Project.NameKey project, Change.Id changeId, Account.Id assignee);
-  }
-
-  private final Account.Id assignee;
-
-  @Inject
-  public SetAssigneeSender(
-      EmailArguments args,
-      @Assisted Project.NameKey project,
-      @Assisted Change.Id changeId,
-      @Assisted Account.Id assignee) {
-    super(args, "setassignee", newChangeData(args, project, changeId));
-    this.assignee = assignee;
-  }
-
-  @Override
-  protected void init() throws EmailException {
-    super.init();
-
-    add(RecipientType.TO, assignee);
-  }
-
-  @Override
-  protected void formatChange() throws EmailException {
-    appendText(textTemplate("SetAssignee"));
-    if (useHtml()) {
-      appendHtml(soyHtmlTemplate("SetAssigneeHtml"));
-    }
-  }
-
-  @Override
-  protected void setupSoyContext() {
-    super.setupSoyContext();
-    soyContextEmailData.put("assigneeName", getNameFor(assignee));
-  }
-}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
index 771d72b..3be55ea 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
@@ -19,7 +19,6 @@
 /** Footers, that can be set in NoteDb commits. */
 public class ChangeNoteFooters {
   public static final FooterKey FOOTER_ATTENTION = new FooterKey("Attention");
-  public static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
   public static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
   public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
   public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 52f540d..c75fd29 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static java.util.Comparator.comparing;
 
@@ -29,7 +28,6 @@
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
@@ -53,7 +51,6 @@
 import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRequirementResult;
-import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -498,26 +495,6 @@
     return state.submitRequirementsResult();
   }
 
-  /**
-   * Returns an ImmutableSet of Account.Ids of all users that have been assigned to this change. The
-   * order of the set is the order in which they were assigned.
-   */
-  public ImmutableSet<Account.Id> getPastAssignees() {
-    return Lists.reverse(state.assigneeUpdates()).stream()
-        .map(AssigneeStatusUpdate::currentAssignee)
-        .filter(Optional::isPresent)
-        .map(Optional::get)
-        .collect(toImmutableSet());
-  }
-
-  /**
-   * Returns an ImmutableList of AssigneeStatusUpdate of all the updates to the assignee field to
-   * this change. The order of the list is from most recent updates to least recent.
-   */
-  public ImmutableList<AssigneeStatusUpdate> getAssigneeUpdates() {
-    return state.assigneeUpdates();
-  }
-
   /** Returns an ImmutableSet of all hashtags for this change sorted in alphabetical order. */
   public ImmutableSet<String> getHashtags() {
     return ImmutableSortedSet.copyOf(state.hashtags());
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 0f2c877..2d3902c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -181,8 +181,6 @@
           + P
           + list(state.reviewerUpdates(), 4 * O + K + K + P)
           + P
-          + list(state.assigneeUpdates(), 4 * O + K + K)
-          + P
           + set(state.attentionSet(), 4 * O + K + I + str(15))
           + P
           + list(state.allAttentionSetUpdates(), 4 * O + K + I + str(15))
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 467095c..0ee0689 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
@@ -75,7 +74,6 @@
 import com.google.gerrit.entities.SubmitRecord.Label.Status;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -160,7 +158,6 @@
   /** Holds all updates to attention set. */
   private final List<AttentionSetUpdate> allAttentionSetUpdates;
 
-  private final List<AssigneeStatusUpdate> assigneeUpdates;
   private final List<SubmitRecord> submitRecords;
   private final ListMultimap<ObjectId, HumanComment> humanComments;
   private final List<SubmitRequirementResult> submitRequirementResults;
@@ -228,7 +225,6 @@
     reviewerUpdates = new ArrayList<>();
     latestAttentionStatus = new HashMap<>();
     allAttentionSetUpdates = new ArrayList<>();
-    assigneeUpdates = new ArrayList<>();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
     allChangeMessages = new ArrayList<>();
     humanComments = MultimapBuilder.hashKeys().arrayListValues().build();
@@ -301,7 +297,6 @@
         buildReviewerUpdates(),
         ImmutableSet.copyOf(latestAttentionStatus.values()),
         allAttentionSetUpdates,
-        assigneeUpdates,
         submitRecords,
         buildAllMessages(),
         humanComments,
@@ -496,7 +491,6 @@
 
     parseHashtags(commit);
     parseAttentionSetUpdates(commit);
-    parseAssigneeUpdates(commitTimestamp, commit);
 
     parseSubmission(commit, commitTimestamp);
 
@@ -745,22 +739,6 @@
     }
   }
 
-  private void parseAssigneeUpdates(Instant ts, ChangeNotesCommit commit)
-      throws ConfigInvalidException {
-    String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
-    if (assigneeValue != null) {
-      Optional<Account.Id> parsedAssignee;
-      if (assigneeValue.equals("")) {
-        // Empty footer found, assignee deleted
-        parsedAssignee = Optional.empty();
-      } else {
-        PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
-        parsedAssignee = Optional.ofNullable(parseIdent(ident));
-      }
-      assigneeUpdates.add(AssigneeStatusUpdate.create(ts, ownerId, parsedAssignee));
-    }
-  }
-
   private void parseTag(ChangeNotesCommit commit) throws ConfigInvalidException {
     tag = null;
     List<String> tagLines = commit.getFooterLineValues(FOOTER_TAG);
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index b0079d7..1715b43 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -50,12 +50,10 @@
 import com.google.gerrit.entities.converter.PatchSetProtoConverter;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
@@ -68,7 +66,6 @@
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -124,7 +121,6 @@
       List<ReviewerStatusUpdate> reviewerUpdates,
       Set<AttentionSetUpdate> attentionSetUpdates,
       List<AttentionSetUpdate> allAttentionSetUpdates,
-      List<AssigneeStatusUpdate> assigneeUpdates,
       List<SubmitRecord> submitRecords,
       List<ChangeMessage> changeMessages,
       ListMultimap<ObjectId, HumanComment> publishedComments,
@@ -178,7 +174,6 @@
         .reviewerUpdates(reviewerUpdates)
         .attentionSet(attentionSetUpdates)
         .allAttentionSetUpdates(allAttentionSetUpdates)
-        .assigneeUpdates(assigneeUpdates)
         .submitRecords(submitRecords)
         .changeMessages(changeMessages)
         .publishedComments(publishedComments)
@@ -320,8 +315,6 @@
   /** Returns all attention set updates. */
   abstract ImmutableList<AttentionSetUpdate> allAttentionSetUpdates();
 
-  abstract ImmutableList<AssigneeStatusUpdate> assigneeUpdates();
-
   abstract ImmutableList<SubmitRecord> submitRecords();
 
   abstract ImmutableList<ChangeMessage> changeMessages();
@@ -369,9 +362,6 @@
     change.setTopic(Strings.emptyToNull(c.topic()));
     change.setLastUpdatedOn(c.lastUpdatedOn());
     change.setSubmissionId(c.submissionId());
-    if (!assigneeUpdates().isEmpty()) {
-      change.setAssignee(assigneeUpdates().get(0).currentAssignee().orElse(null));
-    }
     change.setPrivate(c.isPrivate());
     change.setWorkInProgress(c.workInProgress());
     change.setReviewStarted(c.reviewStarted());
@@ -404,7 +394,6 @@
           .reviewerUpdates(ImmutableList.of())
           .attentionSet(ImmutableSet.of())
           .allAttentionSetUpdates(ImmutableList.of())
-          .assigneeUpdates(ImmutableList.of())
           .submitRecords(ImmutableList.of())
           .changeMessages(ImmutableList.of())
           .publishedComments(ImmutableListMultimap.of())
@@ -442,8 +431,6 @@
 
     abstract Builder allAttentionSetUpdates(List<AttentionSetUpdate> attentionSetUpdates);
 
-    abstract Builder assigneeUpdates(List<AssigneeStatusUpdate> assigneeUpdates);
-
     abstract Builder submitRecords(List<SubmitRecord> submitRecords);
 
     abstract Builder changeMessages(List<ChangeMessage> changeMessages);
@@ -519,7 +506,6 @@
       object
           .allAttentionSetUpdates()
           .forEach(u -> b.addAllAttentionSetUpdate(toAttentionSetUpdateProto(u)));
-      object.assigneeUpdates().forEach(u -> b.addAssigneeUpdate(toAssigneeStatusUpdateProto(u)));
       object
           .submitRecords()
           .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
@@ -616,17 +602,6 @@
           .build();
     }
 
-    private static AssigneeStatusUpdateProto toAssigneeStatusUpdateProto(AssigneeStatusUpdate u) {
-      AssigneeStatusUpdateProto.Builder builder =
-          AssigneeStatusUpdateProto.newBuilder()
-              .setTimestampMillis(u.date().toEpochMilli())
-              .setUpdatedBy(u.updatedBy().get())
-              .setHasCurrentAssignee(u.currentAssignee().isPresent());
-
-      u.currentAssignee().ifPresent(assignee -> builder.setCurrentAssignee(assignee.get()));
-      return builder.build();
-    }
-
     @Override
     public ChangeNotesState deserialize(byte[] in) {
       ChangeNotesStateProto proto = Protos.parseUnchecked(ChangeNotesStateProto.parser(), in);
@@ -659,7 +634,6 @@
               .attentionSet(toAttentionSetUpdates(proto.getAttentionSetUpdateList()))
               .allAttentionSetUpdates(
                   toAllAttentionSetUpdates(proto.getAllAttentionSetUpdateList()))
-              .assigneeUpdates(toAssigneeStatusUpdateList(proto.getAssigneeUpdateList()))
               .submitRecords(
                   proto.getSubmitRecordList().stream()
                       .map(r -> GSON.fromJson(r, StoredSubmitRecord.class).toSubmitRecord())
@@ -783,20 +757,5 @@
       }
       return b.build();
     }
-
-    private static ImmutableList<AssigneeStatusUpdate> toAssigneeStatusUpdateList(
-        List<AssigneeStatusUpdateProto> protos) {
-      ImmutableList.Builder<AssigneeStatusUpdate> b = ImmutableList.builder();
-      for (AssigneeStatusUpdateProto proto : protos) {
-        b.add(
-            AssigneeStatusUpdate.create(
-                Instant.ofEpochMilli(proto.getTimestampMillis()),
-                Account.id(proto.getUpdatedBy()),
-                proto.getHasCurrentAssignee()
-                    ? Optional.of(Account.id(proto.getCurrentAssignee()))
-                    : Optional.empty()));
-      }
-      return b.build();
-    }
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 5d43e28..ef62f2e 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -18,7 +18,6 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.entities.RefNames.changeMetaRef;
-import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_BRANCH;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
@@ -160,7 +159,6 @@
   private String commit;
   private Map<Account.Id, AttentionSetUpdate> plannedAttentionSetUpdates;
   private boolean ignoreFurtherAttentionSetUpdates;
-  private Optional<Account.Id> assignee;
   private Set<String> hashtags;
   private String changeMessage;
   private String tag;
@@ -510,15 +508,6 @@
     return attentionSetUpdatesBuilder.build();
   }
 
-  public void setAssignee(Account.Id assignee) {
-    checkArgument(assignee != null, "use removeAssignee");
-    this.assignee = Optional.of(assignee);
-  }
-
-  public void removeAssignee() {
-    this.assignee = Optional.empty();
-  }
-
   public Map<Account.Id, ReviewerStateInternal> getReviewers() {
     return reviewers;
   }
@@ -765,15 +754,6 @@
       addFooter(msg, FOOTER_COMMIT, commit);
     }
 
-    if (assignee != null) {
-      if (assignee.isPresent()) {
-        addFooter(msg, FOOTER_ASSIGNEE);
-        noteUtil.appendAccountIdIdentString(msg, assignee.get()).append('\n');
-      } else {
-        addFooter(msg, FOOTER_ASSIGNEE).append('\n');
-      }
-    }
-
     Joiner comma = Joiner.on(',');
     if (hashtags != null) {
       addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags));
@@ -1101,7 +1081,7 @@
             // remove users that are currently being removed from the attention set.
             .filter(
                 a ->
-                    plannedAttentionSetUpdates.getOrDefault(a, /*defaultValue= */ null) == null
+                    plannedAttentionSetUpdates.getOrDefault(a, /* defaultValue= */ null) == null
                         || plannedAttentionSetUpdates.get(a).operation().equals(Operation.REMOVE))
             // remove users that are still active on the change.
             .filter(a -> !isActiveOnChange(currentReviewers, a))
@@ -1173,7 +1153,6 @@
         && status == null
         && submissionId == null
         && submitRecords == null
-        && assignee == null
         && hashtags == null
         && topic == null
         && commit == null
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index a67dc07..4d71d84 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -15,7 +15,6 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ASSIGNEE;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_ATTENTION;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
@@ -89,6 +88,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -110,6 +110,10 @@
 @UsedAt(UsedAt.Project.GOOGLE)
 @Singleton
 public class CommitRewriter {
+  // Reading and Writing assignee footer no longer supported. We keep the definition here to be able
+  // to rewrite older commit messages.
+  public static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
+
   /** Options to run {@link #backfillProject}. */
   public static class RunOptions implements Serializable {
     private static final long serialVersionUID = 1L;
@@ -365,7 +369,9 @@
       }
     }
     accounts.addAll(changeNotes.getAllPastReviewers());
-    accounts.addAll(changeNotes.getPastAssignees());
+    // Change Notes class can no longer read or write assignees, we skip assignee accounts at
+    // verifyCommit stage.
+    // accounts.addAll(changeNotes.getPastAssignees());
     changeNotes
         .getAttentionSetUpdates()
         .forEach(attentionSetUpdate -> accounts.add(attentionSetUpdate.account()));
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index b963361..993c68d 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -133,16 +133,6 @@
     return false;
   }
 
-  /** Is this user assigned to this change? */
-  private boolean isAssignee() {
-    Account.Id currentAssignee = getChange().getAssignee();
-    if (currentAssignee != null && getUser().isIdentifiedUser()) {
-      Account.Id id = getUser().getAccountId();
-      return id.equals(currentAssignee);
-    }
-    return false;
-  }
-
   /** Is this user a reviewer for the change? */
   private boolean isReviewer(ChangeData cd) {
     if (getUser().isIdentifiedUser()) {
@@ -184,13 +174,6 @@
     return false;
   }
 
-  private boolean canEditAssignee() {
-    return isOwner()
-        || getProjectControl().isOwner()
-        || refControl.canPerform(Permission.EDIT_ASSIGNEE)
-        || isAssignee();
-  }
-
   /** Can this user edit the hashtag name? */
   private boolean canEditHashtags() {
     return isOwner() // owner (aka creator) of the change can edit hashtags
@@ -275,8 +258,6 @@
             return getProjectControl().isAdmin() || refControl.canDeleteChanges(isOwner());
           case ADD_PATCH_SET:
             return canAddPatchSet();
-          case EDIT_ASSIGNEE:
-            return canEditAssignee();
           case EDIT_DESCRIPTION:
             return canEditDescription();
           case EDIT_HASHTAGS:
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index c456bf8..7741adac 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -37,7 +37,6 @@
    * change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
    */
   ABANDON,
-  EDIT_ASSIGNEE,
   EDIT_DESCRIPTION,
   EDIT_HASHTAGS,
   EDIT_TOPIC_NAME,
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index 89f0493..da24dcd 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -90,7 +90,6 @@
       ImmutableBiMap.<ChangePermission, String>builder()
           .put(ChangePermission.READ, Permission.READ)
           .put(ChangePermission.ABANDON, Permission.ABANDON)
-          .put(ChangePermission.EDIT_ASSIGNEE, Permission.EDIT_ASSIGNEE)
           .put(ChangePermission.EDIT_HASHTAGS, Permission.EDIT_HASHTAGS)
           .put(ChangePermission.EDIT_TOPIC_NAME, Permission.EDIT_TOPIC_NAME)
           .put(ChangePermission.REMOVE_REVIEWER, Permission.REMOVE_REVIEWER)
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 5f909a1..9c340c4 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -46,14 +46,6 @@
   }
 
   /**
-   * Returns a predicate that matches changes that are assigned to the provided {@link
-   * com.google.gerrit.entities.Account.Id}.
-   */
-  public static Predicate<ChangeData> assignee(Account.Id id) {
-    return new ChangeIndexPredicate(ChangeField.ASSIGNEE_SPEC, id.toString());
-  }
-
-  /**
    * Returns a predicate that matches changes that are a revert of the provided {@link
    * com.google.gerrit.entities.Change.Id}.
    */
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 03cdfaa..738eab3 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -153,7 +153,7 @@
   public static final String FIELD_ATTENTION_SET_USERS = "attentionusers";
   public static final String FIELD_ATTENTION_SET_USERS_COUNT = "attentionuserscount";
   public static final String FIELD_ATTENTION_SET_FULL = "attentionfull";
-  public static final String FIELD_ASSIGNEE = "assignee";
+  @Deprecated public static final String FIELD_ASSIGNEE = "assignee";
   public static final String FIELD_AUTHOR = "author";
   public static final String FIELD_EXACTAUTHOR = "exactauthor";
 
@@ -714,14 +714,6 @@
       return new IsAttentionPredicate();
     }
 
-    if ("assigned".equalsIgnoreCase(value)) {
-      return Predicate.not(ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE)));
-    }
-
-    if ("unassigned".equalsIgnoreCase(value)) {
-      return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
-    }
-
     if ("pure-revert".equalsIgnoreCase(value)) {
       checkOperatorAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
       return ChangePredicates.pureRevert("1");
@@ -1269,20 +1261,6 @@
   }
 
   @Operator
-  public Predicate<ChangeData> assignee(String who)
-      throws QueryParseException, IOException, ConfigInvalidException {
-    return assignee(parseAccount(who, (AccountState s) -> true));
-  }
-
-  private Predicate<ChangeData> assignee(Set<Account.Id> who) {
-    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
-    for (Account.Id id : who) {
-      p.add(ChangePredicates.assignee(id));
-    }
-    return Predicate.or(p);
-  }
-
-  @Operator
   public Predicate<ChangeData> ownerin(String group) throws QueryParseException, IOException {
     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
     if (g == null) {
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index 698628e..5632c14 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -25,6 +25,8 @@
 import com.google.gerrit.server.submitrequirement.predicate.FileEditsPredicate.FileEditsArgs;
 import com.google.gerrit.server.submitrequirement.predicate.HasSubmoduleUpdatePredicate;
 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.inject.Inject;
 import java.util.List;
 import java.util.Locale;
@@ -60,17 +62,20 @@
   private static final Splitter SUBMODULE_UPDATE_SPLITTER = Splitter.on(",");
 
   private final FileEditsPredicate.Factory fileEditsPredicateFactory;
+  private final RegexUploaderEmailPredicateFactory regexUploaderEmailPredicateFactory;
 
   @Inject
   SubmitRequirementChangeQueryBuilder(
       Arguments args,
       DistinctVotersPredicate.Factory distinctVotersPredicateFactory,
       FileEditsPredicate.Factory fileEditsPredicateFactory,
-      HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory) {
+      HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory,
+      RegexUploaderEmailPredicateFactory regexUploaderEmailPredicateFactory) {
     super(def, args);
     this.distinctVotersPredicateFactory = distinctVotersPredicateFactory;
     this.fileEditsPredicateFactory = fileEditsPredicateFactory;
     this.hasSubmoduleUpdateFactory = hasSubmoduleUpdateFactory;
+    this.regexUploaderEmailPredicateFactory = regexUploaderEmailPredicateFactory;
   }
 
   @Override
@@ -129,6 +134,16 @@
   }
 
   @Operator
+  public Predicate<ChangeData> committerEmail(String who) throws QueryParseException {
+    return new RegexCommitterEmailPredicate(who);
+  }
+
+  @Operator
+  public Predicate<ChangeData> uploaderEmail(String who) throws QueryParseException {
+    return regexUploaderEmailPredicateFactory.create(who);
+  }
+
+  @Operator
   public Predicate<ChangeData> distinctvoters(String value) throws QueryParseException {
     return distinctVotersPredicateFactory.create(value);
   }
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index f49ee7f..33e6342 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -42,7 +42,6 @@
 import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
 import com.google.gerrit.server.change.ReviewerResource;
-import com.google.gerrit.server.change.SetAssigneeOp;
 import com.google.gerrit.server.change.SetCherryPickOp;
 import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.change.SetPrivateOp;
@@ -91,10 +90,6 @@
     delete(ATTENTION_SET_ENTRY_KIND).to(RemoveFromAttentionSet.class);
     post(ATTENTION_SET_ENTRY_KIND, "delete").to(RemoveFromAttentionSet.class);
     postOnCollection(ATTENTION_SET_ENTRY_KIND).to(AddToAttentionSet.class);
-    get(CHANGE_KIND, "assignee").to(GetAssignee.class);
-    get(CHANGE_KIND, "past_assignees").to(GetPastAssignees.class);
-    put(CHANGE_KIND, "assignee").to(PutAssignee.class);
-    delete(CHANGE_KIND, "assignee").to(DeleteAssignee.class);
     get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
     get(CHANGE_KIND, "comments").to(ListChangeComments.class);
     get(CHANGE_KIND, "robotcomments").to(ListChangeRobotComments.class);
@@ -220,7 +215,6 @@
     factory(PreviewFix.Factory.class);
     factory(RebaseChangeOp.Factory.class);
     factory(ReviewerResource.Factory.class);
-    factory(SetAssigneeOp.Factory.class);
     factory(SetCherryPickOp.Factory.class);
     factory(SetHashtagsOp.Factory.class);
     factory(SetTopicOp.Factory.class);
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
deleted file mode 100644
index 66171c4..0000000
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ /dev/null
@@ -1,120 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.Input;
-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.ChangeMessagesUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.extensions.events.AssigneeChanged;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.permissions.ChangePermission;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.PostUpdateContext;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gerrit.server.util.AccountTemplateUtil;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-@Singleton
-public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
-  private final BatchUpdate.Factory updateFactory;
-  private final ChangeMessagesUtil cmUtil;
-  private final AssigneeChanged assigneeChanged;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final AccountLoader.Factory accountLoaderFactory;
-
-  @Inject
-  DeleteAssignee(
-      BatchUpdate.Factory updateFactory,
-      ChangeMessagesUtil cmUtil,
-      AssigneeChanged assigneeChanged,
-      IdentifiedUser.GenericFactory userFactory,
-      AccountLoader.Factory accountLoaderFactory) {
-    this.updateFactory = updateFactory;
-    this.cmUtil = cmUtil;
-    this.assigneeChanged = assigneeChanged;
-    this.userFactory = userFactory;
-    this.accountLoaderFactory = accountLoaderFactory;
-  }
-
-  @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, UpdateException, PermissionBackendException {
-    rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
-
-    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      Op op = new Op();
-      bu.addOp(rsrc.getChange().getId(), op);
-      bu.execute();
-      Account.Id deletedAssignee = op.getDeletedAssignee();
-      return deletedAssignee == null
-          ? Response.none()
-          : Response.ok(accountLoaderFactory.create(true).fillOne(deletedAssignee));
-    }
-  }
-
-  private class Op implements BatchUpdateOp {
-    private Change change;
-    private AccountState deletedAssignee;
-
-    @Override
-    public boolean updateChange(ChangeContext ctx) throws RestApiException {
-      change = ctx.getChange();
-      ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
-      Account.Id currentAssigneeId = change.getAssignee();
-      if (currentAssigneeId == null) {
-        return false;
-      }
-      IdentifiedUser deletedAssigneeUser = userFactory.create(currentAssigneeId);
-      deletedAssignee = deletedAssigneeUser.state();
-      update.removeAssignee();
-      addMessage(ctx, deletedAssigneeUser);
-      return true;
-    }
-
-    @Nullable
-    public Account.Id getDeletedAssignee() {
-      return deletedAssignee != null ? deletedAssignee.account().id() : null;
-    }
-
-    private void addMessage(ChangeContext ctx, IdentifiedUser deletedAssignee) {
-      cmUtil.setChangeMessage(
-          ctx,
-          "Assignee deleted: "
-              + AccountTemplateUtil.getAccountTemplate(deletedAssignee.getAccountId()),
-          ChangeMessagesUtil.TAG_DELETE_ASSIGNEE);
-    }
-
-    @Override
-    public void postUpdate(PostUpdateContext ctx) {
-      assigneeChanged.fire(
-          ctx.getChangeData(change), ctx.getAccount(), deletedAssignee, ctx.getWhen());
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/GetAssignee.java b/java/com/google/gerrit/server/restapi/change/GetAssignee.java
deleted file mode 100644
index a5820bf..0000000
--- a/java/com/google/gerrit/server/restapi/change/GetAssignee.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.Optional;
-
-@Singleton
-public class GetAssignee implements RestReadView<ChangeResource> {
-  private final AccountLoader.Factory accountLoaderFactory;
-
-  @Inject
-  GetAssignee(AccountLoader.Factory accountLoaderFactory) {
-    this.accountLoaderFactory = accountLoaderFactory;
-  }
-
-  @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc) throws PermissionBackendException {
-    Optional<Account.Id> assignee = Optional.ofNullable(rsrc.getChange().getAssignee());
-    if (assignee.isPresent()) {
-      return Response.ok(accountLoaderFactory.create(true).fillOne(assignee.get()));
-    }
-    return Response.none();
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java b/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
deleted file mode 100644
index c1c9a34..0000000
--- a/java/com/google/gerrit/server/restapi/change/GetPastAssignees.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import static java.util.stream.Collectors.toList;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-@Singleton
-public class GetPastAssignees implements RestReadView<ChangeResource> {
-  private final AccountLoader.Factory accountLoaderFactory;
-
-  @Inject
-  GetPastAssignees(AccountLoader.Factory accountLoaderFactory) {
-    this.accountLoaderFactory = accountLoaderFactory;
-  }
-
-  @Override
-  public Response<List<AccountInfo>> apply(ChangeResource rsrc) throws PermissionBackendException {
-
-    Set<Account.Id> pastAssignees = rsrc.getNotes().load().getPastAssignees();
-    if (pastAssignees == null) {
-      return Response.ok(Collections.emptyList());
-    }
-
-    AccountLoader accountLoader = accountLoaderFactory.create(true);
-    List<AccountInfo> infos = pastAssignees.stream().map(accountLoader::get).collect(toList());
-    accountLoader.fill();
-    return Response.ok(infos);
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
deleted file mode 100644
index d41620e..0000000
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.ReviewerInput;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.approval.ApprovalsUtil;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.ReviewerModifier;
-import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
-import com.google.gerrit.server.change.SetAssigneeOp;
-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.update.BatchUpdate;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-@Singleton
-public class PutAssignee
-    implements RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> {
-
-  private final BatchUpdate.Factory updateFactory;
-  private final AccountResolver accountResolver;
-  private final SetAssigneeOp.Factory assigneeFactory;
-  private final ReviewerModifier reviewerModifier;
-  private final AccountLoader.Factory accountLoaderFactory;
-  private final PermissionBackend permissionBackend;
-  private final ApprovalsUtil approvalsUtil;
-
-  @Inject
-  PutAssignee(
-      BatchUpdate.Factory updateFactory,
-      AccountResolver accountResolver,
-      SetAssigneeOp.Factory assigneeFactory,
-      ReviewerModifier reviewerModifier,
-      AccountLoader.Factory accountLoaderFactory,
-      PermissionBackend permissionBackend,
-      ApprovalsUtil approvalsUtil) {
-    this.updateFactory = updateFactory;
-    this.accountResolver = accountResolver;
-    this.assigneeFactory = assigneeFactory;
-    this.reviewerModifier = reviewerModifier;
-    this.accountLoaderFactory = accountLoaderFactory;
-    this.permissionBackend = permissionBackend;
-    this.approvalsUtil = approvalsUtil;
-  }
-
-  @Override
-  public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input)
-      throws RestApiException, UpdateException, IOException, PermissionBackendException,
-          ConfigInvalidException {
-    rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
-
-    input.assignee = Strings.nullToEmpty(input.assignee).trim();
-    if (input.assignee.isEmpty()) {
-      throw new BadRequestException("missing assignee field");
-    }
-
-    IdentifiedUser assignee = accountResolver.resolve(input.assignee).asUniqueUser();
-    try {
-      permissionBackend
-          .absentUser(assignee.getAccountId())
-          .change(rsrc.getNotes())
-          .check(ChangePermission.READ);
-    } catch (AuthException e) {
-      throw new AuthException("read not permitted for " + input.assignee, e);
-    }
-
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
-      SetAssigneeOp op = assigneeFactory.create(assignee);
-      bu.addOp(rsrc.getId(), op);
-
-      ReviewerSet currentReviewers = approvalsUtil.getReviewers(rsrc.getNotes());
-      if (!currentReviewers.all().contains(assignee.getAccountId())) {
-        ReviewerModification reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
-        reviewersAddition.op.suppressEmail();
-        bu.addOp(rsrc.getId(), reviewersAddition.op);
-      }
-
-      bu.execute();
-      return Response.ok(accountLoaderFactory.create(true).fillOne(assignee.getAccountId()));
-    }
-  }
-
-  private ReviewerModification addAssigneeAsCC(ChangeResource rsrc, String assignee)
-      throws IOException, PermissionBackendException, ConfigInvalidException {
-    ReviewerInput reviewerInput = new ReviewerInput();
-    reviewerInput.reviewer = assignee;
-    reviewerInput.state = ReviewerState.CC;
-    reviewerInput.confirmed = true;
-    reviewerInput.notify = NotifyHandling.NONE;
-    return reviewerModifier.prepare(rsrc.getNotes(), rsrc.getUser(), reviewerInput, false);
-  }
-
-  @Override
-  public UiAction.Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Edit Assignee")
-        .setVisible(rsrc.permissions().testCond(ChangePermission.EDIT_ASSIGNEE));
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index 3d9d588..b262b46 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -259,10 +259,13 @@
 
   /**
    * Bots don't process automatic rules, the only attention set change they do is this rule: Add
-   * owner and uploader when a bot votes negatively.
+   * owner and uploader when a bot votes negatively, but only if the change is open.
    */
   private void botsWithNegativeLabelsAddOwnerAndUploader(
       BatchUpdate bu, ChangeNotes changeNotes, ReviewInput input) {
+    if (changeNotes.getChange().isClosed()) {
+      return;
+    }
     if (input.labels != null && input.labels.values().stream().anyMatch(vote -> vote < 0)) {
       Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
       Account.Id owner = changeNotes.getChange().getOwner();
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 103a5ac..66536dd 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -232,10 +232,7 @@
         toBoolean(this.config.getBoolean("change", null, "disablePrivateChanges", false));
     info.mergeabilityComputationBehavior =
         MergeabilityComputationBehavior.fromConfig(config).name();
-    info.enableAttentionSet =
-        toBoolean(this.config.getBoolean("change", null, "enableAttentionSet", true));
-    info.enableAssignee =
-        toBoolean(this.config.getBoolean("change", null, "enableAssignee", false));
+    info.enableAssignee = false;
     return info;
   }
 
diff --git a/java/com/google/gerrit/server/submitrequirement/predicate/RegexCommitterEmailPredicate.java b/java/com/google/gerrit/server/submitrequirement/predicate/RegexCommitterEmailPredicate.java
new file mode 100644
index 0000000..f991d31
--- /dev/null
+++ b/java/com/google/gerrit/server/submitrequirement/predicate/RegexCommitterEmailPredicate.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.submitrequirement.predicate;
+
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+/**
+ * A submit requirement predicate that matches with changes having the committer email's address
+ * matching a specific regular expression pattern.
+ */
+public class RegexCommitterEmailPredicate extends SubmitRequirementPredicate {
+  protected final RunAutomaton committerEmailPattern;
+
+  public RegexCommitterEmailPredicate(String pattern) throws QueryParseException {
+    super("committeremail", pattern);
+
+    if (pattern.startsWith("^")) {
+      pattern = pattern.substring(1);
+    }
+
+    if (pattern.endsWith("$") && !pattern.endsWith("\\$")) {
+      pattern = pattern.substring(0, pattern.length() - 1);
+    }
+
+    try {
+      this.committerEmailPattern = new RunAutomaton(new RegExp(pattern).toAutomaton());
+    } catch (IllegalArgumentException e) {
+      throw new QueryParseException(String.format("invalid regular expression: %s", pattern), e);
+    }
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return committerEmailPattern.run(cd.getCommitter().getEmailAddress());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/server/submitrequirement/predicate/RegexUploaderEmailPredicate.java b/java/com/google/gerrit/server/submitrequirement/predicate/RegexUploaderEmailPredicate.java
new file mode 100644
index 0000000..9566546
--- /dev/null
+++ b/java/com/google/gerrit/server/submitrequirement/predicate/RegexUploaderEmailPredicate.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.submitrequirement.predicate;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+import java.util.Optional;
+
+/**
+ * A submit requirement predicate that matches with changes having the uploader's email address
+ * matching a specific regular expression pattern.
+ */
+@AutoFactory
+public class RegexUploaderEmailPredicate extends SubmitRequirementPredicate {
+  protected final RunAutomaton uploaderEmailPattern;
+  private final AccountCache accountCache;
+
+  public RegexUploaderEmailPredicate(@Provided AccountCache accountCache, String pattern)
+      throws QueryParseException {
+    super("uploaderemail", pattern);
+    this.accountCache = accountCache;
+
+    if (pattern.startsWith("^")) {
+      pattern = pattern.substring(1);
+    }
+
+    if (pattern.endsWith("$") && !pattern.endsWith("\\$")) {
+      pattern = pattern.substring(0, pattern.length() - 1);
+    }
+
+    try {
+      this.uploaderEmailPattern = new RunAutomaton(new RegExp(pattern).toAutomaton());
+    } catch (IllegalArgumentException e) {
+      throw new QueryParseException(String.format("invalid regular expression: %s", pattern), e);
+    }
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    Optional<AccountState> accountState = accountCache.get(cd.currentPatchSet().uploader());
+    if (!accountState.isPresent()) {
+      return false;
+    }
+    String email = accountState.get().account().preferredEmail();
+    return email == null ? false : uploaderEmailPattern.run(email);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/java/com/google/gerrit/server/validators/AssigneeValidationListener.java b/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
deleted file mode 100644
index 514125f..0000000
--- a/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.validators;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.extensions.annotations.ExtensionPoint;
-
-/** Listener to provide validation of assignees. */
-@ExtensionPoint
-public interface AssigneeValidationListener {
-  /**
-   * Invoked by Gerrit before the assignee of a change is modified.
-   *
-   * @param change the change on which the assignee is changed
-   * @param assignee the new assignee. Null if removed
-   * @throws ValidationException if validation fails
-   */
-  void validateAssignee(Change change, Account assignee) throws ValidationException;
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 2d663df..27bd6b9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -65,10 +65,6 @@
           RestCall.get("/changes/%s/drafts"),
           RestCall.get("/changes/%s/attention"),
           RestCall.post("/changes/%s/attention"),
-          RestCall.get("/changes/%s/assignee"),
-          RestCall.get("/changes/%s/past_assignees"),
-          RestCall.put("/changes/%s/assignee"),
-          RestCall.delete("/changes/%s/assignee"),
           RestCall.post("/changes/%s/private"),
           RestCall.post("/changes/%s/private.delete"),
           RestCall.delete("/changes/%s/private"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
deleted file mode 100644
index be94cdf..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.rest.change;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
-import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-
-import com.google.common.collect.Iterables;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.UseClockStep;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.entities.Permission;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
-import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.inject.Inject;
-import java.util.Iterator;
-import java.util.List;
-import org.eclipse.jgit.transport.RefSpec;
-import org.junit.Test;
-
-@NoHttpd
-@UseClockStep
-public class AssigneeIT extends AbstractDaemonTest {
-  @Inject private ProjectOperations projectOperations;
-  @Inject private RequestScopeOperations requestScopeOperations;
-
-  @Test
-  public void getNoAssignee() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(getAssignee(r)).isNull();
-  }
-
-  @Test
-  public void addGetAssignee() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-    assertThat(getAssignee(r)._accountId).isEqualTo(user.id().get());
-
-    assertThat(sender.getMessages()).hasSize(1);
-    Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(user.getNameEmail());
-  }
-
-  @Test
-  public void setNewAssigneeWhenExists() throws Exception {
-    PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email());
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-  }
-
-  @Test
-  public void getPastAssignees() throws Exception {
-    PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email());
-    setAssignee(r, admin.email());
-    List<AccountInfo> assignees = getPastAssignees(r);
-    assertThat(assignees).hasSize(2);
-    Iterator<AccountInfo> itr = assignees.iterator();
-    assertThat(itr.next()._accountId).isEqualTo(user.id().get());
-    assertThat(itr.next()._accountId).isEqualTo(admin.id().get());
-  }
-
-  @Test
-  public void assigneeAddedAsCc() throws Exception {
-    PushOneCommit.Result r = createChange();
-    Iterable<AccountInfo> reviewers = getReviewers(r, ReviewerState.CC);
-    assertThat(reviewers).isNull();
-
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-    reviewers = getReviewers(r, ReviewerState.CC);
-    assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getFirst(reviewers, null)._accountId).isEqualTo(user.id().get());
-    assertThat(getReviewers(r, ReviewerState.REVIEWER)).isNull();
-  }
-
-  @Test
-  public void assigneeStaysReviewer() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
-    Iterable<AccountInfo> reviewers = getReviewers(r, ReviewerState.REVIEWER);
-    assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getFirst(reviewers, null)._accountId).isEqualTo(user.id().get());
-    assertThat(getReviewers(r, ReviewerState.CC)).isNull();
-
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-    reviewers = getReviewers(r, ReviewerState.REVIEWER);
-    assertThat(reviewers).hasSize(1);
-    assertThat(Iterables.getFirst(reviewers, null)._accountId).isEqualTo(user.id().get());
-    assertThat(getReviewers(r, ReviewerState.CC)).isNull();
-  }
-
-  @Test
-  public void setAlreadyExistingAssignee() throws Exception {
-    PushOneCommit.Result r = createChange();
-    setAssignee(r, user.email());
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-  }
-
-  @Test
-  public void deleteAssignee() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-    assertThat(deleteAssignee(r)._accountId).isEqualTo(user.id().get());
-    assertThat(getAssignee(r)).isNull();
-  }
-
-  @Test
-  public void deleteAssigneeWhenNoAssignee() throws Exception {
-    PushOneCommit.Result r = createChange();
-    assertThat(deleteAssignee(r)).isNull();
-  }
-
-  @Test
-  public void setAssigneeToInactiveUser() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.accounts().id(user.id().get()).setActive(false);
-    UnresolvableAccountException thrown =
-        assertThrows(UnresolvableAccountException.class, () -> setAssignee(r, user.email()));
-    assertThat(thrown)
-        .hasMessageThat()
-        .isEqualTo(
-            "Account '"
-                + user.email()
-                + "' only matches inactive accounts. To use an inactive account, retry with one"
-                + " of the following exact account IDs:\n"
-                + user.id()
-                + ": User1 <user1@example.com>");
-  }
-
-  @Test
-  public void setAssigneeToInactiveUserById() throws Exception {
-    PushOneCommit.Result r = createChange();
-    gApi.accounts().id(user.id().get()).setActive(false);
-    setAssignee(r, user.id().toString());
-    assertThat(getAssignee(r)._accountId).isEqualTo(user.id().get());
-  }
-
-  @Test
-  public void setAssigneeForNonVisibleChange() throws Exception {
-    git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
-    testRepo.reset(RefNames.REFS_CONFIG);
-    PushOneCommit.Result r = createChange("refs/for/refs/meta/config");
-    AuthException thrown = assertThrows(AuthException.class, () -> setAssignee(r, user.email()));
-    assertThat(thrown).hasMessageThat().contains("read not permitted");
-  }
-
-  @Test
-  public void setAssigneeNotAllowedWithoutPermission() throws Exception {
-    PushOneCommit.Result r = createChange();
-    requestScopeOperations.setApiUser(user.id());
-    AuthException thrown = assertThrows(AuthException.class, () -> setAssignee(r, user.email()));
-    assertThat(thrown).hasMessageThat().contains("not permitted");
-  }
-
-  @Test
-  public void setAssigneeAllowedWithPermission() throws Exception {
-    PushOneCommit.Result r = createChange();
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(allow(Permission.EDIT_ASSIGNEE).ref("refs/heads/master").group(REGISTERED_USERS))
-        .update();
-    requestScopeOperations.setApiUser(user.id());
-    assertThat(setAssignee(r, user.email())._accountId).isEqualTo(user.id().get());
-  }
-
-  private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
-    return change(r).getAssignee();
-  }
-
-  private List<AccountInfo> getPastAssignees(PushOneCommit.Result r) throws Exception {
-    return change(r).getPastAssignees();
-  }
-
-  private Iterable<AccountInfo> getReviewers(PushOneCommit.Result r, ReviewerState state)
-      throws Exception {
-    return get(r.getChangeId(), DETAILED_LABELS).reviewers.get(state);
-  }
-
-  private AccountInfo setAssignee(PushOneCommit.Result r, String identifieer) throws Exception {
-    AssigneeInput input = new AssigneeInput();
-    input.assignee = identifieer;
-    return change(r).setAssignee(input);
-  }
-
-  private AccountInfo deleteAssignee(PushOneCommit.Result r) throws Exception {
-    return change(r).deleteAssignee();
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index ea52690..1d2ddc2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -1488,6 +1488,64 @@
   }
 
   @Test
+  public void robotReviewWithNegativeLabelDoesntAddOwnerIfChangeIsMerged() throws Exception {
+    TestAccount robot =
+        accountCreator.create(
+            "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS);
+
+    PushOneCommit.Result r = createChange();
+
+    // The robot votes with Code-Review-1 on patch set 1.
+    // Without this vote the robot cannot (re-)apply a negative vote on the change after it was
+    // merged change later.
+    requestScopeOperations.setApiUser(robot.id());
+    change(r).revision(1).review(ReviewInput.dislike());
+
+    // Amend the change so that patch set 2 gets created.
+    requestScopeOperations.setApiUser(admin.id());
+    amendChange(r.getChangeId()).assertOkStatus();
+
+    // Approve the change.
+    approve(r.getChangeId());
+
+    // User adds a comment so that the admin user is added to the attention set.
+    // This has to be a comment from a user, since comments from robots do not trigger attention set
+    // updates.
+    requestScopeOperations.setApiUser(user.id());
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.message = "A comment";
+    change(r).current().review(reviewInput);
+
+    // Verify that the admin user was added to the attention set.
+    AttentionSetUpdate attentionSet =
+        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+    assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change");
+
+    // Submit the change.
+    requestScopeOperations.setApiUser(admin.id());
+    change(r).current().submit();
+
+    // Verify that the attention set was cleared on submit.
+    attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE);
+    assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted");
+
+    // Re-apply the negative robot vote on patch set 1.
+    // Note it's possible to a apply a negative vote on merged changes if it wasn't already present
+    // since we disallow downgrading votes on merged changes (e.g. downgrade from not present aka 0
+    // to -1 is not allowed).
+    requestScopeOperations.setApiUser(robot.id());
+    change(r).revision(1).review(ReviewInput.dislike());
+
+    // Verify that re-applying the negative robot vote on patch set 1 didn't add the admin user
+    // back to the attention set.
+    attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin));
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE);
+    assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted");
+  }
+
+  @Test
   public void robotCommentDoesNotAddOwnerOnClosedChanges() throws Exception {
     TestAccount robot =
         accountCreator.create(
@@ -1613,7 +1671,6 @@
   }
 
   @Test
-  @GerritConfig(name = "change.enableAttentionSet", value = "true")
   public void attentionSetEmailHeader() throws Exception {
     PushOneCommit.Result r = createChange();
     TestAccount user2 = accountCreator.user2();
@@ -1654,21 +1711,6 @@
   }
 
   @Test
-  @GerritConfig(name = "change.enableAttentionSet", value = "false")
-  public void noReferenceToAttentionSetInEmailsWhenDisabled() throws Exception {
-    PushOneCommit.Result r = createChange();
-    // Add user and to the attention set.
-    change(r).addReviewer(user.id().toString());
-
-    // Attention set is not referenced.
-    assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
-        .doesNotContain("Attention is currently required");
-    assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody())
-        .doesNotContain("Attention is currently required");
-    sender.clear();
-  }
-
-  @Test
   public void attentionSetWithEmailFilter() throws Exception {
     PushOneCommit.Result r = createChange();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 8131352..1236afd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -60,8 +60,6 @@
   // change
   @GerritConfig(name = "change.updateDelay", value = "50s")
   @GerritConfig(name = "change.disablePrivateChanges", value = "true")
-  @GerritConfig(name = "change.enableAttentionSet", value = "true")
-  @GerritConfig(name = "change.enableAssignee", value = "true")
 
   // download
   @GerritConfig(
@@ -102,8 +100,6 @@
     // change
     assertThat(i.change.updateDelay).isEqualTo(50);
     assertThat(i.change.disablePrivateChanges).isTrue();
-    assertThat(i.change.enableAttentionSet).isTrue();
-    assertThat(i.change.enableAssignee).isTrue();
 
     // download
     assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index b68afc5..e44bfcf 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -45,7 +45,6 @@
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -2452,154 +2451,6 @@
   }
 
   /*
-   * SetAssigneeSender tests.
-   */
-
-  @Test
-  public void setAssigneeOnReviewableChange() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    assign(sc, sc.owner, sc.assignee);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void setAssigneeOnReviewableChangeByOwnerCcingSelf() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    assign(sc, sc.owner, sc.assignee, CC_ON_OWN_COMMENTS);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(sc.owner)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void setAssigneeOnReviewableChangeByAdmin() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    assign(sc, admin, sc.assignee);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void setAssigneeOnReviewableChangeByAdminCcingSelf() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    assign(sc, admin, sc.assignee, CC_ON_OWN_COMMENTS);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(admin)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void setAssigneeToSelfOnReviewableChange() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    assign(sc, sc.owner, sc.owner);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void changeAssigneeOnReviewableChange() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    TestAccount other = accountCreator.create("other", "other@example.com", "other", null);
-    assign(sc, sc.owner, other);
-    sender.clear();
-    assign(sc, sc.owner, sc.assignee);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void changeAssigneeToSelfOnReviewableChange() throws Exception {
-    StagedChange sc = stageReviewableChange();
-    assign(sc, sc.owner, sc.assignee);
-    sender.clear();
-    assign(sc, sc.owner, sc.owner);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void setAssigneeOnReviewableWipChange() throws Exception {
-    StagedChange sc = stageReviewableWipChange();
-    assign(sc, sc.owner, sc.assignee);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  @Test
-  public void setAssigneeOnWipChange() throws Exception {
-    StagedChange sc = stageWipChange();
-    assign(sc, sc.owner, sc.assignee);
-    assertThat(sender)
-        .sent("setassignee", sc)
-        .cc(
-            StagedUsers.REVIEWER_BY_EMAIL,
-            StagedUsers.CC_BY_EMAIL) // TODO(logan): This is probably not intended!
-        .to(sc.assignee)
-        .noOneElse();
-    assertThat(sender).didNotSend();
-  }
-
-  private void assign(StagedChange sc, TestAccount by, TestAccount to) throws Exception {
-    assign(sc, by, to, ENABLED);
-  }
-
-  private void assign(StagedChange sc, TestAccount by, TestAccount to, EmailStrategy emailStrategy)
-      throws Exception {
-    setEmailStrategy(by, emailStrategy);
-    requestScopeOperations.setApiUser(by.id());
-    AssigneeInput in = new AssigneeInput();
-    in.assignee = to.email();
-    gApi.changes().id(sc.changeId).setAssignee(in);
-  }
-
-  /*
    * Start review and WIP tests.
    */
 
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index d3c4949..9e27e93 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -493,6 +493,74 @@
         SubmitRequirementResult.Status.UNSATISFIED);
   }
 
+  @Test
+  public void byCommitterEmail() throws Exception {
+    TestAccount user2 =
+        accountCreator.create("Foo", "user@example.com", "User", /* displayName = */ null);
+    requestScopeOperations.setApiUser(user2.id());
+    ChangeInfo info =
+        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get();
+    ChangeData cd =
+        changeQueryProvider
+            .get()
+            .byLegacyChangeId(Change.Id.tryParse(Integer.toString(info._number)).get())
+            .get(0);
+
+    // Match by email works
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "committeremail:\"^.*@example\\.com\"",
+        SubmitRequirementResult.Status.SATISFIED);
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "committeremail:\"^user@.*\\.com\"",
+        SubmitRequirementResult.Status.SATISFIED);
+
+    // Match by name does not work
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "committeremail:\"^Foo$\"",
+        SubmitRequirementResult.Status.UNSATISFIED);
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "committeremail:\"^User$\"",
+        SubmitRequirementResult.Status.UNSATISFIED);
+  }
+
+  @Test
+  public void byUploaderEmail() throws Exception {
+    TestAccount user2 =
+        accountCreator.create("Foo", "user@example.com", "User", /* displayName = */ null);
+    requestScopeOperations.setApiUser(user2.id());
+    ChangeInfo info =
+        gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get();
+    ChangeData cd =
+        changeQueryProvider
+            .get()
+            .byLegacyChangeId(Change.Id.tryParse(Integer.toString(info._number)).get())
+            .get(0);
+
+    // Match by email works
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "uploaderemail:\"^.*@example\\.com\"",
+        SubmitRequirementResult.Status.SATISFIED);
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "uploaderemail:\"^user@.*\\.com\"",
+        SubmitRequirementResult.Status.SATISFIED);
+
+    // Match by name does not work
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "uploaderemail:\"^Foo$\"",
+        SubmitRequirementResult.Status.UNSATISFIED);
+    checkSubmitRequirementResult(
+        cd,
+        /* submittabilityExpr= */ "uploaderemail:\"^User$\"",
+        SubmitRequirementResult.Status.UNSATISFIED);
+  }
+
   private void checkSubmitRequirementResult(
       ChangeData cd, String submittabilityExpr, SubmitRequirementResult.Status expectedStatus) {
     SubmitRequirement sr =
diff --git a/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java b/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
index 13a9e0c..f02d244 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/StreamEventsIT.java
@@ -65,7 +65,7 @@
         () ->
             pollEventsContaining("ref-updated", refName.substring(0, refName.lastIndexOf('/')))
                     .size()
-                == 2);
+                == 1);
   }
 
   private void waitForEvent(Supplier<Boolean> waitCondition) throws InterruptedException {
diff --git a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
index 00b92b4..390aa84 100644
--- a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
+++ b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -69,22 +69,6 @@
   }
 
   @Test
-  public void assigneeChangedEvent() {
-    Change change = newChange();
-    AssigneeChangedEvent orig = new AssigneeChangedEvent(change);
-    orig.change = asChangeAttribute(change);
-    orig.changer = newAccount("changer");
-    orig.oldAssignee = newAccount("oldAssignee");
-
-    AssigneeChangedEvent e = roundTrip(orig);
-
-    assertThat(e).isNotNull();
-    assertSameChangeEvent(e, orig);
-    assertSameAccount(e.changer, orig.changer);
-    assertSameAccount(e.oldAssignee, orig.oldAssignee);
-  }
-
-  @Test
   public void changeDeletedEvent() {
     Change change = newChange();
     ChangeDeletedEvent orig = new ChangeDeletedEvent(change);
diff --git a/javatests/com/google/gerrit/server/events/EventJsonTest.java b/javatests/com/google/gerrit/server/events/EventJsonTest.java
index c2b67c3..3f8519e 100644
--- a/javatests/com/google/gerrit/server/events/EventJsonTest.java
+++ b/javatests/com/google/gerrit/server/events/EventJsonTest.java
@@ -153,51 +153,6 @@
   }
 
   @Test
-  public void assigneeChangedEvent() {
-    Change change = newChange();
-    AssigneeChangedEvent event = new AssigneeChangedEvent(change);
-    event.change = asChangeAttribute(change);
-    event.changer = newAccount("changer");
-    event.oldAssignee = newAccount("oldAssignee");
-
-    assertThatJsonMap(event)
-        .isEqualTo(
-            ImmutableMap.builder()
-                .put(
-                    "changer",
-                    ImmutableMap.builder()
-                        .put("name", event.changer.get().name)
-                        .put("email", event.changer.get().email)
-                        .put("username", event.changer.get().username)
-                        .build())
-                .put(
-                    "oldAssignee",
-                    ImmutableMap.builder()
-                        .put("name", event.oldAssignee.get().name)
-                        .put("email", event.oldAssignee.get().email)
-                        .put("username", event.oldAssignee.get().username)
-                        .build())
-                .put(
-                    "change",
-                    ImmutableMap.builder()
-                        .put("project", PROJECT)
-                        .put("branch", BRANCH)
-                        .put("id", CHANGE_ID)
-                        .put("number", CHANGE_NUM_DOUBLE)
-                        .put("url", URL)
-                        .put("commitMessage", COMMIT_MESSAGE)
-                        .put("createdOn", TS1)
-                        .put("status", NEW.name())
-                        .build())
-                .put("project", PROJECT)
-                .put("refName", REF)
-                .put("changeKey", map("id", CHANGE_ID))
-                .put("type", "assignee-changed")
-                .put("eventCreatedOn", TS2)
-                .build());
-  }
-
-  @Test
   public void changeDeletedEvent() {
     Change change = newChange();
     ChangeDeletedEvent event = new ChangeDeletedEvent(change);
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 4543b50..8d7066f 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -166,7 +166,8 @@
             + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
             + "Patch-set: 1\n"
             + "Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7\n"
-            + "Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 2 <2@gerrit>\n"
+            + "Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 2"
+            + " <2@gerrit>\n"
             + "Label: Label1=0, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 2 <2@gerrit>\n"
             + "Subject: This is a test change\n");
 
@@ -183,14 +184,24 @@
     assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1=+1, \n");
     assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1=+1,\n");
     assertParseFails(
-        "Update change\n\nPatch-set: 1\nLabel: Label1=-1,  577fb248e474018276351785930358ec0450e9f7 Gerrit User 2 <2@gerrit>\n");
+        "Update change\n\n"
+            + "Patch-set: 1\n"
+            + "Label: Label1=-1,  577fb248e474018276351785930358ec0450e9f7 Gerrit User 2"
+            + " <2@gerrit>\n");
     assertParseFails(
-        "Update change\n\nPatch-set: 1\nLabel: Label1=-1,  577fb248e474018276351785930358ec0450e9f7\n");
+        "Update change\n\n"
+            + "Patch-set: 1\n"
+            + "Label: Label1=-1,  577fb248e474018276351785930358ec0450e9f7\n");
     // UUID for removals is not supported.
     assertParseFails(
-        "Update change\n\nPatch-set: 1\nLabel: -Label1, 577fb248e474018276351785930358ec0450e9f7\n");
+        "Update change\n\n"
+            + "Patch-set: 1\n"
+            + "Label: -Label1, 577fb248e474018276351785930358ec0450e9f7\n");
     assertParseFails(
-        "Update change\n\nPatch-set: 1\nLabel: -Label1, 577fb248e474018276351785930358ec0450e9f7 Other Account <2@gerrit>\n");
+        "Update change\n\n"
+            + "Patch-set: 1\n"
+            + "Label: -Label1, 577fb248e474018276351785930358ec0450e9f7 Other Account"
+            + " <2@gerrit>\n");
   }
 
   @Test
@@ -234,13 +245,20 @@
             + "Branch: refs/heads/master\n"
             + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
             + "Patch-set: 1\n"
-            + "Copied-Label: Label2=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>\n"
-            + "Copied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit>\n"
-            + "Copied-Label: Label3=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag\"\n"
-            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit> :\"tag with characters %^#@^( *::!\"\n"
-            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit> :\"tag with uuid delimiter , \"\n"
-            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with characters %^#@^( *::!\"\n"
-            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with uuid delimiter , \"\n"
+            + "Copied-Label: Label2=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit>\n"
+            + "Copied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit>,Gerrit User 2 <2@gerrit>\n"
+            + "Copied-Label: Label3=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag\"\n"
+            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit> :\"tag with uuid delimiter , \"\n"
+            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+            + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with uuid delimiter , \"\n"
             + "Subject: This is a test change\n");
 
     assertParseSucceeds(
@@ -250,23 +268,34 @@
             + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
             + "Patch-set: 1\n"
             + "Copied-Label: Label2=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>\n"
-            + "Copied-Label: Label1=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit>\n"
-            + "Copied-Label: Label3=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag\"\n"
-            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit> :\"tag with characters %^#@^( *::!\"\n"
-            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit> :\"tag with uuid delimiter , \"\n"
-            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with characters %^#@^( *::!\"\n"
-            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with uuid delimiter , \"\n"
+            + "Copied-Label: Label1=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2"
+            + " <2@gerrit>\n"
+            + "Copied-Label: Label3=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2"
+            + " <2@gerrit> :\"tag\"\n"
+            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit> :\"tag with"
+            + " characters %^#@^( *::!\"\n"
+            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit> :\"tag with uuid"
+            + " delimiter , \"\n"
+            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2"
+            + " <2@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+            + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2"
+            + " <2@gerrit> :\"tag with uuid delimiter , \"\n"
             + "Subject: This is a test change\n");
 
     assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1,\n");
     assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1,\n");
     assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1 ,\n");
     assertParseFails(
-        "Copied-Label: Label1=+1,  577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit>\n\n");
+        "Copied-Label: Label1=+1,  577fb248e474018276351785930358ec0450e9f7 Gerrit User 1"
+            + " <1@gerrit>,Gerrit User 2 <2@gerrit>\n\n");
     assertParseFails(
-        "Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7");
+        "Update change\n\n"
+            + "Patch-set: 1\n"
+            + "Copied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7");
     assertParseFails(
-        "Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 :\"tag\"\n");
+        "Update change\n\n"
+            + "Patch-set: 1\n"
+            + "Copied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 :\"tag\"\n");
 
     // UUID for removals is not supported.
     assertParseFails(
@@ -350,26 +379,6 @@
   }
 
   @Test
-  public void parseAssignee() throws Exception {
-    assertParseSucceeds(
-        "Update change\n"
-            + "\n"
-            + "Branch: refs/heads/master\n"
-            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-            + "Patch-set: 1\n"
-            + "Assignee: Change Owner <1@gerrit>\n"
-            + "Subject: This is a test change\n");
-    assertParseSucceeds(
-        "Update change\n"
-            + "\n"
-            + "Branch: refs/heads/master\n"
-            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
-            + "Patch-set: 2\n"
-            + "Assignee:\n"
-            + "Subject: This is a test change\n");
-  }
-
-  @Test
   public void parseTopic() throws Exception {
     assertParseSucceeds(
         "Update change\n"
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index c654828..9a29230 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -45,12 +45,10 @@
 import com.google.gerrit.entities.converter.PatchSetProtoConverter;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.Protos;
-import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
-import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AssigneeStatusUpdateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
@@ -806,37 +804,6 @@
   }
 
   @Test
-  public void serializeAssigneeUpdates() throws Exception {
-    assertRoundTrip(
-        newBuilder()
-            .assigneeUpdates(
-                ImmutableList.of(
-                    AssigneeStatusUpdate.create(
-                        Instant.ofEpochMilli(1212L),
-                        Account.id(1000),
-                        Optional.of(Account.id(2001))),
-                    AssigneeStatusUpdate.create(
-                        Instant.ofEpochMilli(3434L), Account.id(1000), Optional.empty())))
-            .build(),
-        ChangeNotesStateProto.newBuilder()
-            .setMetaId(SHA_BYTES)
-            .setChangeId(ID.get())
-            .setColumns(colsProto)
-            .addAssigneeUpdate(
-                AssigneeStatusUpdateProto.newBuilder()
-                    .setTimestampMillis(1212L)
-                    .setUpdatedBy(1000)
-                    .setCurrentAssignee(2001)
-                    .setHasCurrentAssignee(true))
-            .addAssigneeUpdate(
-                AssigneeStatusUpdateProto.newBuilder()
-                    .setTimestampMillis(3434L)
-                    .setUpdatedBy(1000)
-                    .setHasCurrentAssignee(false))
-            .build());
-  }
-
-  @Test
   public void serializeSubmitRecords() throws Exception {
     SubmitRecord sr1 = new SubmitRecord();
     sr1.status = SubmitRecord.Status.OK;
@@ -971,9 +938,6 @@
                 .put(
                     "allAttentionSetUpdates",
                     new TypeLiteral<ImmutableList<AttentionSetUpdate>>() {}.getType())
-                .put(
-                    "assigneeUpdates",
-                    new TypeLiteral<ImmutableList<AssigneeStatusUpdate>>() {}.getType())
                 .put("submitRecords", new TypeLiteral<ImmutableList<SubmitRecord>>() {}.getType())
                 .put("changeMessages", new TypeLiteral<ImmutableList<ChangeMessage>>() {}.getType())
                 .put(
@@ -1088,19 +1052,6 @@
   }
 
   @Test
-  public void assigneeStatusUpdateMethods() throws Exception {
-    assertThatSerializedClass(AssigneeStatusUpdate.class)
-        .hasAutoValueMethods(
-            ImmutableMap.of(
-                "date",
-                Instant.class,
-                "updatedBy",
-                Account.Id.class,
-                "currentAssignee",
-                new TypeLiteral<Optional<Account.Id>>() {}.getType()));
-  }
-
-  @Test
   public void submitRecordFields() throws Exception {
     assertThatSerializedClass(SubmitRecord.class)
         .hasFields(
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 9cd002e..cf739f6 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -53,7 +53,6 @@
 import com.google.gerrit.entities.SubmissionId;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerSet;
@@ -1474,91 +1473,6 @@
   }
 
   @Test
-  public void assigneeCommit() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setAssignee(otherUserId);
-    ObjectId result = update.commit();
-    assertThat(result).isNotNull();
-    try (RevWalk rw = new RevWalk(repo)) {
-      RevCommit commit = rw.parseCommit(update.getResult());
-      rw.parseBody(commit);
-      String strIdent = "Gerrit User " + otherUserId + " <" + otherUserId + "@" + serverId + ">";
-      assertThat(commit.getFullMessage()).contains("Assignee: " + strIdent);
-    }
-  }
-
-  @Test
-  public void assigneeChangeNotes() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setAssignee(otherUserId);
-    update.commit();
-
-    ChangeNotes notes = newNotes(c);
-    assertThat(notes.getChange().getAssignee()).isEqualTo(otherUserId);
-
-    update = newUpdate(c, changeOwner);
-    update.setAssignee(changeOwner.getAccountId());
-    update.commit();
-
-    notes = newNotes(c);
-    assertThat(notes.getChange().getAssignee()).isEqualTo(changeOwner.getAccountId());
-  }
-
-  @Test
-  public void pastAssigneesChangeNotes() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setAssignee(otherUserId);
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setAssignee(changeOwner.getAccountId());
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setAssignee(otherUserId);
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.removeAssignee();
-    update.commit();
-
-    ChangeNotes notes = newNotes(c);
-    assertThat(notes.getPastAssignees()).hasSize(2);
-  }
-
-  @Test
-  public void assigneeStatusUpdateChangeNotes() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, otherUser);
-    update.setAssignee(otherUserId);
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.removeAssignee();
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setAssignee(changeOwner.getAccountId());
-    update.commit();
-
-    update = newUpdate(c, changeOwner);
-    update.setAssignee(otherUserId);
-    update.commit();
-
-    ChangeNotes notes = newNotes(c);
-    ImmutableList<AssigneeStatusUpdate> statusUpdates = notes.getAssigneeUpdates();
-    assertThat(statusUpdates).hasSize(4);
-    assertThat(statusUpdates.get(3).updatedBy()).isEqualTo(otherUserId);
-    assertThat(statusUpdates.get(3).currentAssignee()).hasValue(otherUserId);
-    assertThat(statusUpdates.get(2).currentAssignee()).isEmpty();
-    assertThat(statusUpdates.get(1).currentAssignee()).hasValue(changeOwner.getAccountId());
-    assertThat(statusUpdates.get(0).currentAssignee()).hasValue(otherUserId);
-  }
-
-  @Test
   public void hashtagCommit() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 527e78e..4f4911a 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -214,37 +214,37 @@
   @Test
   public void maxRefsToUpdate_coversAllInvalid_inMultipleBatches() throws Exception {
     testMaxRefsToUpdate(
-        /*numberOfInvalidChanges=*/ 11,
-        /*numberOfValidChanges=*/ 9,
-        /*maxRefsToUpdate=*/ 12,
-        /*maxRefsInBatch=*/ 2);
+        /* numberOfInvalidChanges= */ 11,
+        /* numberOfValidChanges= */ 9,
+        /* maxRefsToUpdate= */ 12,
+        /* maxRefsInBatch= */ 2);
   }
 
   @Test
   public void maxRefsToUpdate_coversAllInvalid_inSingleBatch() throws Exception {
     testMaxRefsToUpdate(
-        /*numberOfInvalidChanges=*/ 11,
-        /*numberOfValidChanges=*/ 9,
-        /*maxRefsToUpdate=*/ 12,
-        /*maxRefsInBatch=*/ 12);
+        /* numberOfInvalidChanges= */ 11,
+        /* numberOfValidChanges= */ 9,
+        /* maxRefsToUpdate= */ 12,
+        /* maxRefsInBatch= */ 12);
   }
 
   @Test
   public void moreInvalidRefs_thenMaxRefsToUpdate_inMultipleBatches() throws Exception {
     testMaxRefsToUpdate(
-        /*numberOfInvalidChanges=*/ 11,
-        /*numberOfValidChanges=*/ 9,
-        /*maxRefsToUpdate=*/ 10,
-        /*maxRefsInBatch=*/ 2);
+        /* numberOfInvalidChanges= */ 11,
+        /* numberOfValidChanges= */ 9,
+        /* maxRefsToUpdate= */ 10,
+        /* maxRefsInBatch= */ 2);
   }
 
   @Test
   public void moreInvalidRefs_thenMaxRefsToUpdate_inSingleBatch() throws Exception {
     testMaxRefsToUpdate(
-        /*numberOfInvalidChanges=*/ 11,
-        /*numberOfValidChanges=*/ 9,
-        /*maxRefsToUpdate=*/ 10,
-        /*maxRefsInBatch=*/ 10);
+        /* numberOfInvalidChanges= */ 11,
+        /* numberOfValidChanges= */ 9,
+        /* maxRefsToUpdate= */ 10,
+        /* maxRefsInBatch= */ 10);
   }
 
   private void testMaxRefsToUpdate(
@@ -333,7 +333,7 @@
     RevCommit invalidUpdateCommit =
         writeUpdate(
             RefNames.changeMetaRef(c.getId()),
-            getChangeUpdateBody(c, /*changeMessage=*/ null),
+            getChangeUpdateBody(c, /* changeMessage= */ null),
             invalidAuthorIdent);
     ChangeUpdate validUpdate = newUpdate(c, changeOwner);
     validUpdate.setChangeMessage("verification from jenkins");
@@ -472,7 +472,8 @@
                     // valid change message that should not be overwritten
                     getChangeUpdateBody(
                         c,
-                        "Removed cc <GERRIT_ACCOUNT_2> with the following votes:\n\n * Code-Review+2 by <GERRIT_ACCOUNT_2>",
+                        "Removed cc <GERRIT_ACCOUNT_2> with the following votes:\n\n"
+                            + " * Code-Review+2 by <GERRIT_ACCOUNT_2>",
                         "CC: " + reviewerIdentToFix),
                     getAuthorIdent(otherUser.getAccount())))
             .add(
@@ -677,7 +678,7 @@
                     RefNames.changeMetaRef(c.getId()),
                     getChangeUpdateBody(
                         c,
-                        /*changeMessage=*/ null,
+                        /* changeMessage= */ null,
                         "Label: -Verified " + approverIdentToFix,
                         "Label: Custom-Label-1=-1 " + approverIdentToFix,
                         "Label: Verified=+1",
@@ -690,7 +691,7 @@
                     RefNames.changeMetaRef(c.getId()),
                     getChangeUpdateBody(
                         c,
-                        /*changeMessage=*/ null,
+                        /* changeMessage= */ null,
                         "Label: -Verified " + changeOwnerIdentToFix,
                         "Label: Custom-Label-1=+1"),
                     getAuthorIdent(otherUser.getAccount())))
@@ -812,7 +813,7 @@
                     RefNames.changeMetaRef(c.getId()),
                     getChangeUpdateBody(
                         c,
-                        /*changeMessage=*/ "Removed Code-Review+2 by " + otherUser.getNameEmail(),
+                        /* changeMessage= */ "Removed Code-Review+2 by " + otherUser.getNameEmail(),
                         "Label: -Code-Review " + approverIdentToFix),
                     getAuthorIdent(changeOwner.getAccount())))
             .add(
@@ -820,7 +821,8 @@
                     RefNames.changeMetaRef(c.getId()),
                     getChangeUpdateBody(
                         c,
-                        /*changeMessage=*/ "Removed Custom-Label-1 by " + otherUser.getNameEmail(),
+                        /* changeMessage= */ "Removed Custom-Label-1 by "
+                            + otherUser.getNameEmail(),
                         "Label: -Custom-Label " + getValidIdentAsString(otherUser.getAccount())),
                     getAuthorIdent(changeOwner.getAccount())))
             .add(
@@ -828,7 +830,7 @@
                     RefNames.changeMetaRef(c.getId()),
                     getChangeUpdateBody(
                         c,
-                        /*changeMessage=*/ "Removed Verified+2 by " + changeOwner.getNameEmail(),
+                        /* changeMessage= */ "Removed Verified+2 by " + changeOwner.getNameEmail(),
                         "Label: -Verified"),
                     getAuthorIdent(changeOwner.getAccount())))
             .build();
@@ -928,7 +930,7 @@
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
             c,
-            /*changeMessage=*/ "Removed Verified+2 by " + otherUser.getNameEmail(),
+            /* changeMessage= */ "Removed Verified+2 by " + otherUser.getNameEmail(),
             "Label: -Verified"),
         invalidAuthorIdent);
 
@@ -961,19 +963,19 @@
         RefNames.changeMetaRef(c.getId()),
 
         // Even though footer is missing, accounts are matched among the account in change updates.
-        getChangeUpdateBody(c, /*changeMessage=*/ "Removed Verified-1 by Other Account (0002)"),
+        getChangeUpdateBody(c, /* changeMessage= */ "Removed Verified-1 by Other Account (0002)"),
         getAuthorIdent(changeOwner.getAccount()));
 
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
-            c, /*changeMessage=*/ "Removed Verified+2 by " + changeOwner.getNameEmail()),
+            c, /* changeMessage= */ "Removed Verified+2 by " + changeOwner.getNameEmail()),
         getAuthorIdent(changeOwner.getAccount()));
 
     // No rewrite for default
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
-        getChangeUpdateBody(c, /*changeMessage=*/ "Removed Verified+2 by Gerrit Account"),
+        getChangeUpdateBody(c, /* changeMessage= */ "Removed Verified+2 by Gerrit Account"),
         getAuthorIdent(changeOwner.getAccount()));
 
     RunOptions options = new RunOptions();
@@ -1006,7 +1008,8 @@
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
-            c, /*changeMessage=*/ "Removed Verified+2 by Renamed Change Owner <change@owner.com>"),
+            c,
+            /* changeMessage= */ "Removed Verified+2 by Renamed Change Owner <change@owner.com>"),
         getAuthorIdent(changeOwner.getAccount()));
 
     RunOptions options = new RunOptions();
@@ -1035,7 +1038,7 @@
     approvalUpdate.commit();
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
-        getChangeUpdateBody(c, /*changeMessage=*/ "Removed Verified+2 by Change Owner"),
+        getChangeUpdateBody(c, /* changeMessage= */ "Removed Verified+2 by Change Owner"),
         getAuthorIdent(changeOwner.getAccount()));
 
     RunOptions options = new RunOptions();
@@ -1072,17 +1075,17 @@
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
-            c, /*changeMessage=*/ "Removed Verified+2 by Change Owner <other@test.com>"),
+            c, /* changeMessage= */ "Removed Verified+2 by Change Owner <other@test.com>"),
         getAuthorIdent(changeOwner.getAccount()));
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
-            c, /*changeMessage=*/ "Removed Verified+2 by Change Owner <change@owner.com>"),
+            c, /* changeMessage= */ "Removed Verified+2 by Change Owner <change@owner.com>"),
         getAuthorIdent(changeOwner.getAccount()));
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
-            c, /*changeMessage=*/ "Removed Verified-1 by Change Owner <other@test.com>"),
+            c, /* changeMessage= */ "Removed Verified-1 by Change Owner <other@test.com>"),
         getAuthorIdent(changeOwner.getAccount()));
 
     RunOptions options = new RunOptions();
@@ -1121,7 +1124,7 @@
         // Even though footer is missing, accounts are matched among the account in change updates.
         getChangeUpdateBody(
             c,
-            /*changeMessage=*/ "Removed the following votes:\n"
+            /* changeMessage= */ "Removed the following votes:\n"
                 + String.format("* Verified-1 by %s\n", otherUser.getNameEmail())),
         getAuthorIdent(changeOwner.getAccount()));
 
@@ -1129,7 +1132,7 @@
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
             c,
-            /*changeMessage=*/ "Removed the following votes:\n"
+            /* changeMessage= */ "Removed the following votes:\n"
                 + String.format("* Verified+2 by %s\n", changeOwner.getNameEmail())
                 + String.format("* Verified-1 by %s\n", changeOwner.getNameEmail())
                 + String.format("* Code-Review by %s\n", otherUser.getNameEmail())),
@@ -1140,7 +1143,7 @@
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
             c,
-            /*changeMessage=*/ "Removed the following votes:\n"
+            /* changeMessage= */ "Removed the following votes:\n"
                 + "* Verified+2 by Gerrit Account\n"
                 + "* Verified-1 by <GERRIT_ACCOUNT_2>\n"),
         getAuthorIdent(changeOwner.getAccount()));
@@ -1200,7 +1203,7 @@
             RefNames.changeMetaRef(c.getId()),
             getChangeUpdateBody(
                 c,
-                /*changeMessage=*/ null,
+                /* changeMessage= */ null,
                 // Only 'person_ident' fix is required
                 "Attention: "
                     + gson.toJson(
@@ -1354,27 +1357,51 @@
     assertThat(commitHistoryDiff.get(0))
         .isEqualTo(
             "@@ -8 +8 @@\n"
-                + "-Attention: {\"person_ident\":\"Gerrit User 2 \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Other Account using the hovercard menu\"}\n"
-                + "+Attention: {\"person_ident\":\"Gerrit User 2 \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone using the hovercard menu\"}\n");
+                + "-Attention: {\"person_ident\":\"Gerrit User 2"
+                + " \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by Other"
+                + " Account using the hovercard menu\"}\n"
+                + "+Attention: {\"person_ident\":\"Gerrit User 2"
+                + " \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone"
+                + " using the hovercard menu\"}\n");
     assertThat(Arrays.asList(commitHistoryDiff.get(1).split("\n")))
         .containsExactly(
             "@@ -7,2 +7,2 @@",
-            "-Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Other Account replied on the change\"}",
-            "-Attention: {\"person_ident\":\"Gerrit User 2 \\u003c2@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by Other Account using the hovercard menu\"}",
-            "+Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Someone replied on the change\"}",
-            "+Attention: {\"person_ident\":\"Gerrit User 2 \\u003c2@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by someone using the hovercard menu\"}");
+            "-Attention: {\"person_ident\":\"Gerrit User 1"
+                + " \\u003c1@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Other Account"
+                + " replied on the change\"}",
+            "-Attention: {\"person_ident\":\"Gerrit User 2"
+                + " \\u003c2@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by Other"
+                + " Account using the hovercard menu\"}",
+            "+Attention: {\"person_ident\":\"Gerrit User 1"
+                + " \\u003c1@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Someone replied on"
+                + " the change\"}",
+            "+Attention: {\"person_ident\":\"Gerrit User 2"
+                + " \\u003c2@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by"
+                + " someone using the hovercard menu\"}");
     assertThat(Arrays.asList(commitHistoryDiff.get(2).split("\n")))
         .containsExactly(
             "@@ -7,2 +7,2 @@",
-            "-Attention: {\"person_ident\":\"Other Account \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone using the hovercard menu\"}",
-            "-Attention: {\"person_ident\":\"Change Owner \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Other Account replied on the change\"}",
-            "+Attention: {\"person_ident\":\"Gerrit User 2 \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone using the hovercard menu\"}",
-            "+Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Someone replied on the change\"}");
+            "-Attention: {\"person_ident\":\"Other Account"
+                + " \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone"
+                + " using the hovercard menu\"}",
+            "-Attention: {\"person_ident\":\"Change Owner"
+                + " \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Other Account"
+                + " replied on the change\"}",
+            "+Attention: {\"person_ident\":\"Gerrit User 2"
+                + " \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone"
+                + " using the hovercard menu\"}",
+            "+Attention: {\"person_ident\":\"Gerrit User 1"
+                + " \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Someone replied"
+                + " on the change\"}");
     assertThat(commitHistoryDiff.get(3))
         .isEqualTo(
             "@@ -7 +7 @@\n"
-                + "-Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by Other Account by clicking the attention icon\"}\n"
-                + "+Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by someone by clicking the attention icon\"}\n");
+                + "-Attention: {\"person_ident\":\"Gerrit User 1"
+                + " \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by Other"
+                + " Account by clicking the attention icon\"}\n"
+                + "+Attention: {\"person_ident\":\"Gerrit User 1"
+                + " \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by"
+                + " someone by clicking the attention icon\"}\n");
     BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
     assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -1483,13 +1510,15 @@
     commitsToFix.add(invalidCherryPickedMessageUpdate.commit());
     ChangeUpdate invalidRebasedMessageUpdate = newUpdate(c, changeOwner);
     invalidRebasedMessageUpdate.setChangeMessage(
-        "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b by "
+        "Change has been successfully rebased and submitted as"
+            + " e40dc1a50dc7f457a37579e2755374f3e1a5413b by "
             + changeOwner.getName());
 
     commitsToFix.add(invalidRebasedMessageUpdate.commit());
     ChangeUpdate validSubmitMessageUpdate = newUpdate(c, changeOwner);
     validSubmitMessageUpdate.setChangeMessage(
-        "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b");
+        "Change has been successfully rebased and submitted as"
+            + " e40dc1a50dc7f457a37579e2755374f3e1a5413b");
     validSubmitMessageUpdate.commit();
 
     Ref metaRefBeforeRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
@@ -1512,15 +1541,21 @@
     assertThat(changeMessages(notesBeforeRewrite))
         .containsExactly(
             "Change has been successfully merged by Change Owner",
-            "Change has been successfully cherry-picked as e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner",
-            "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner",
-            "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b");
+            "Change has been successfully cherry-picked as e40dc1a50dc7f457a37579e2755374f3e1a5413b"
+                + " by Change Owner",
+            "Change has been successfully rebased and submitted as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner",
+            "Change has been successfully rebased and submitted as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b");
     assertThat(changeMessages(notesAfterRewrite))
         .containsExactly(
             "Change has been successfully merged",
-            "Change has been successfully cherry-picked as e40dc1a50dc7f457a37579e2755374f3e1a5413b",
-            "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b",
-            "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b");
+            "Change has been successfully cherry-picked as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b",
+            "Change has been successfully rebased and submitted as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b",
+            "Change has been successfully rebased and submitted as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b");
 
     Ref metaRefAfterRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
     assertThat(metaRefAfterRewrite.getObjectId()).isNotEqualTo(metaRefBeforeRewrite.getObjectId());
@@ -1536,11 +1571,15 @@
                 + "-Change has been successfully merged by Change Owner\n"
                 + "+Change has been successfully merged\n",
             "@@ -6 +6 @@\n"
-                + "-Change has been successfully cherry-picked as e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner\n"
-                + "+Change has been successfully cherry-picked as e40dc1a50dc7f457a37579e2755374f3e1a5413b\n",
+                + "-Change has been successfully cherry-picked as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner\n"
+                + "+Change has been successfully cherry-picked as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b\n",
             "@@ -6 +6 @@\n"
-                + "-Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner\n"
-                + "+Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b\n");
+                + "-Change has been successfully rebased and submitted as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner\n"
+                + "+Change has been successfully rebased and submitted as"
+                + " e40dc1a50dc7f457a37579e2755374f3e1a5413b\n");
     BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
     assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -1610,7 +1649,7 @@
             RefNames.changeMetaRef(c.getId()),
             getChangeUpdateBody(
                 c,
-                /*changeMessage=*/ null,
+                /* changeMessage= */ null,
                 "Label: SUBM=+1",
                 "Submission-id: 5271-1496917120975-10a10df9",
                 "Submitted-with: NOT_READY",
@@ -1876,59 +1915,72 @@
     ChangeUpdate invalidOnReviewUpdate = newUpdate(c, changeOwner);
     invalidOnReviewUpdate.setChangeMessage(
         "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
-            + "By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
+            + "By voting Code-Review+2 the following files are now code-owner approved by Change"
+            + " Owner:\n"
             + "   * file1.java\n"
             + "   * file2.ts\n"
-            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
-            + "By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n");
+            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change"
+            + " Owner\n"
+            + "By voting Other-Label+2 the code-owners submit requirement is still overridden by"
+            + " Change Owner\n");
     commitsToFix.add(invalidOnReviewUpdate.commit());
 
     ChangeUpdate invalidOnReviewUpdateAnyOrder = newUpdate(c, changeOwner);
     invalidOnReviewUpdateAnyOrder.setChangeMessage(
         "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
-            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
-            + "By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n"
-            + "By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
+            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change"
+            + " Owner\n"
+            + "By voting Other-Label+2 the code-owners submit requirement is still overridden by"
+            + " Change Owner\n"
+            + "By voting Code-Review+2 the following files are now code-owner approved by Change"
+            + " Owner:\n"
             + "   * file1.java\n"
             + "   * file2.ts\n");
     commitsToFix.add(invalidOnReviewUpdateAnyOrder.commit());
     ChangeUpdate invalidOnApprovalUpdate = newUpdate(c, otherUser);
     invalidOnApprovalUpdate.setChangeMessage(
         "Patch Set 1: -Code-Review\n\n"
-            + "By removing the Code-Review+2 vote the following files are no longer explicitly code-owner approved by Other Account:\n"
+            + "By removing the Code-Review+2 vote the following files are no longer explicitly"
+            + " code-owner approved by Other Account:\n"
             + "   * file1.java\n"
             + "   * file2.ts\n"
-            + "\nThe listed files are still implicitly approved by Other Account.\n");
+            + "\n"
+            + "The listed files are still implicitly approved by Other Account.\n");
     commitsToFix.add(invalidOnApprovalUpdate.commit());
 
     ChangeUpdate invalidOnOverrideUpdate = newUpdate(c, changeOwner);
     invalidOnOverrideUpdate.setChangeMessage(
         "Patch Set 1: -Owners-Override\n\n"
             + "(1 comment)\n\n"
-            + "By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by Change Owner\n");
+            + "By removing the Owners-Override+1 vote the code-owners submit requirement is no"
+            + " longer overridden by Change Owner\n");
 
     commitsToFix.add(invalidOnOverrideUpdate.commit());
 
     ChangeUpdate partiallyValidOnReviewUpdate = newUpdate(c, changeOwner);
     partiallyValidOnReviewUpdate.setChangeMessage(
         "Patch Set 1: Any-Label+2 Code-Review+2\n\n"
-            + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+            + "By voting Code-Review+2 the following files are now code-owner approved by"
+            + " <GERRIT_ACCOUNT_1>:\n"
             + "   * file1.java\n"
             + "   * file2.ts\n"
-            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n");
+            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change"
+            + " Owner\n");
     commitsToFix.add(partiallyValidOnReviewUpdate.commit());
 
     ChangeUpdate validOnApprovalUpdate = newUpdate(c, changeOwner);
     validOnApprovalUpdate.setChangeMessage(
         "Patch Set 1: Code-Review-2\n\n"
-            + "By voting Code-Review-2 the following files are no longer explicitly code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+            + "By voting Code-Review-2 the following files are no longer explicitly code-owner"
+            + " approved by <GERRIT_ACCOUNT_1>:\n"
             + "   * file4.java\n");
     validOnApprovalUpdate.commit();
 
     ChangeUpdate validOnOverrideUpdate = newUpdate(c, changeOwner);
     validOnOverrideUpdate.setChangeMessage(
         "Patch Set 1: Owners-Override+1\n\n"
-            + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n");
+            + "By voting Owners-Override+1 the code-owners submit requirement is still overridden"
+            + " by <GERRIT_ACCOUNT_1>\n");
     validOnOverrideUpdate.commit();
 
     Ref metaRefBeforeRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
@@ -1952,39 +2004,52 @@
     assertThat(changeMessages(notesAfterRewrite))
         .containsExactly(
             "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
-                + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "By voting Code-Review+2 the following files are now code-owner approved by"
+                + " <GERRIT_ACCOUNT_1>:\n"
                 + "   * file1.java\n"
                 + "   * file2.ts\n"
-                + "By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
-                + "By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n",
+                + "By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " <GERRIT_ACCOUNT_1>\n"
+                + "By voting Other-Label+2 the code-owners submit requirement is still overridden"
+                + " by <GERRIT_ACCOUNT_1>\n",
             "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
-                + "By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
-                + "By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n"
-                + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " <GERRIT_ACCOUNT_1>\n"
+                + "By voting Other-Label+2 the code-owners submit requirement is still overridden"
+                + " by <GERRIT_ACCOUNT_1>\n"
+                + "By voting Code-Review+2 the following files are now code-owner approved by"
+                + " <GERRIT_ACCOUNT_1>:\n"
                 + "   * file1.java\n"
                 + "   * file2.ts\n",
             "Patch Set 1: -Code-Review\n"
                 + "\n"
-                + "By removing the Code-Review+2 vote the following files are no longer explicitly code-owner approved by <GERRIT_ACCOUNT_2>:\n"
+                + "By removing the Code-Review+2 vote the following files are no longer explicitly"
+                + " code-owner approved by <GERRIT_ACCOUNT_2>:\n"
                 + "   * file1.java\n"
                 + "   * file2.ts\n"
-                + "\nThe listed files are still implicitly approved by <GERRIT_ACCOUNT_2>.\n",
+                + "\n"
+                + "The listed files are still implicitly approved by <GERRIT_ACCOUNT_2>.\n",
             "Patch Set 1: -Owners-Override\n"
                 + "\n"
                 + "(1 comment)\n"
                 + "\n"
-                + "By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by <GERRIT_ACCOUNT_1>\n",
+                + "By removing the Owners-Override+1 vote the code-owners submit requirement is no"
+                + " longer overridden by <GERRIT_ACCOUNT_1>\n",
             "Patch Set 1: Any-Label+2 Code-Review+2\n\n"
-                + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "By voting Code-Review+2 the following files are now code-owner approved by"
+                + " <GERRIT_ACCOUNT_1>:\n"
                 + "   * file1.java\n"
                 + "   * file2.ts\n"
-                + "By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n",
+                + "By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " <GERRIT_ACCOUNT_1>\n",
             "Patch Set 1: Code-Review-2\n\n"
-                + "By voting Code-Review-2 the following files are no longer explicitly code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "By voting Code-Review-2 the following files are no longer explicitly code-owner"
+                + " approved by <GERRIT_ACCOUNT_1>:\n"
                 + "   * file4.java\n",
             "Patch Set 1: Owners-Override+1\n"
                 + "\n"
-                + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n");
+                + "By voting Owners-Override+1 the code-owners submit requirement is still"
+                + " overridden by <GERRIT_ACCOUNT_1>\n");
 
     Ref metaRefAfterRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
     assertThat(metaRefAfterRewrite.getObjectId()).isNotEqualTo(metaRefBeforeRewrite.getObjectId());
@@ -1997,32 +2062,50 @@
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -8 +8 @@\n"
-                + "-By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
-                + "+By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "-By voting Code-Review+2 the following files are now code-owner approved by"
+                + " Change Owner:\n"
+                + "+By voting Code-Review+2 the following files are now code-owner approved by"
+                + " <GERRIT_ACCOUNT_1>:\n"
                 + "@@ -11,2 +11,2 @@\n"
-                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
-                + "-By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n"
-                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
-                + "+By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n",
+                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " Change Owner\n"
+                + "-By voting Other-Label+2 the code-owners submit requirement is still overridden"
+                + " by Change Owner\n"
+                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " <GERRIT_ACCOUNT_1>\n"
+                + "+By voting Other-Label+2 the code-owners submit requirement is still overridden"
+                + " by <GERRIT_ACCOUNT_1>\n",
             "@@ -8,3 +8,3 @@\n"
-                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
-                + "-By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n"
-                + "-By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
-                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
-                + "+By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n"
-                + "+By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n",
+                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " Change Owner\n"
+                + "-By voting Other-Label+2 the code-owners submit requirement is still overridden"
+                + " by Change Owner\n"
+                + "-By voting Code-Review+2 the following files are now code-owner approved by"
+                + " Change Owner:\n"
+                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " <GERRIT_ACCOUNT_1>\n"
+                + "+By voting Other-Label+2 the code-owners submit requirement is still overridden"
+                + " by <GERRIT_ACCOUNT_1>\n"
+                + "+By voting Code-Review+2 the following files are now code-owner approved by"
+                + " <GERRIT_ACCOUNT_1>:\n",
             "@@ -8 +8 @@\n"
-                + "-By removing the Code-Review+2 vote the following files are no longer explicitly code-owner approved by Other Account:\n"
-                + "+By removing the Code-Review+2 vote the following files are no longer explicitly code-owner approved by <GERRIT_ACCOUNT_2>:\n"
+                + "-By removing the Code-Review+2 vote the following files are no longer explicitly"
+                + " code-owner approved by Other Account:\n"
+                + "+By removing the Code-Review+2 vote the following files are no longer explicitly"
+                + " code-owner approved by <GERRIT_ACCOUNT_2>:\n"
                 + "@@ -12 +12 @@\n"
                 + "-The listed files are still implicitly approved by Other Account.\n"
                 + "+The listed files are still implicitly approved by <GERRIT_ACCOUNT_2>.\n",
             "@@ -10 +10 @@\n"
-                + "-By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by Change Owner\n"
-                + "+By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by <GERRIT_ACCOUNT_1>\n",
+                + "-By removing the Owners-Override+1 vote the code-owners submit requirement is no"
+                + " longer overridden by Change Owner\n"
+                + "+By removing the Owners-Override+1 vote the code-owners submit requirement is no"
+                + " longer overridden by <GERRIT_ACCOUNT_1>\n",
             "@@ -11 +11 @@\n"
-                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
-                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n");
+                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " Change Owner\n"
+                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by"
+                + " <GERRIT_ACCOUNT_1>\n");
     BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
     assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
@@ -2039,34 +2122,30 @@
             getChangeUpdateBody(c, "Assignee added", "Assignee: " + assigneeIdentToFix),
             getAuthorIdent(changeOwner.getAccount()));
 
-    ChangeUpdate changeAssigneeUpdate = newUpdate(c, changeOwner);
-    changeAssigneeUpdate.setAssignee(otherUserId);
-    changeAssigneeUpdate.commit();
-
-    ChangeUpdate removeAssigneeUpdate = newUpdate(c, changeOwner);
-    removeAssigneeUpdate.removeAssignee();
-    removeAssigneeUpdate.commit();
+    // Valid commits
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c,
+            "Assignee added: <GERRIT_ACCOUNT_2>",
+            "Assignee: " + getValidIdentAsString(otherUser.getAccount())),
+        getAuthorIdent(changeOwner.getAccount()));
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(c, "Assignee deleted: <GERRIT_ACCOUNT_2>", "Assignee:"),
+        getAuthorIdent(changeOwner.getAccount()));
 
     Ref metaRefBeforeRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
 
     ImmutableList<RevCommit> commitsBeforeRewrite = logMetaRef(repo, metaRefBeforeRewrite);
 
     int invalidCommitIndex = commitsBeforeRewrite.indexOf(invalidUpdateCommit);
-    ChangeNotes notesBeforeRewrite = newNotes(c);
 
     RunOptions options = new RunOptions();
     options.dryRun = false;
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    ChangeNotes notesAfterRewrite = newNotes(c);
-    assertThat(notesBeforeRewrite.getPastAssignees())
-        .containsExactly(changeOwner.getAccountId(), otherUser.getAccountId());
-    assertThat(notesBeforeRewrite.getChange().getAssignee()).isNull();
-    assertThat(notesAfterRewrite.getPastAssignees())
-        .containsExactly(changeOwner.getAccountId(), otherUser.getAccountId());
-    assertThat(notesAfterRewrite.getChange().getAssignee()).isNull();
-
     Ref metaRefAfterRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
     assertThat(metaRefAfterRewrite.getObjectId()).isNotEqualTo(metaRefBeforeRewrite.getObjectId());
 
@@ -2145,18 +2224,13 @@
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
     ChangeNotes notesAfterRewrite = newNotes(c);
-    assertThat(notesBeforeRewrite.getPastAssignees())
-        .containsExactly(changeOwner.getAccountId(), otherUser.getAccountId());
-    assertThat(notesBeforeRewrite.getChange().getAssignee()).isNull();
     assertThat(changeMessages(notesBeforeRewrite))
         .containsExactly(
             "Assignee added: Change Owner <change@owner.com>",
-            "Assignee changed from: Change Owner <change@owner.com> to: Other Account <other@account.com>",
+            "Assignee changed from: Change Owner <change@owner.com> to: Other Account"
+                + " <other@account.com>",
             "Assignee deleted: Other Account <other@account.com>");
 
-    assertThat(notesAfterRewrite.getPastAssignees())
-        .containsExactly(changeOwner.getAccountId(), otherUser.getAccountId());
-    assertThat(notesAfterRewrite.getChange().getAssignee()).isNull();
     assertThat(changeMessages(notesAfterRewrite))
         .containsExactly(
             "Assignee added: " + AccountTemplateUtil.getAccountTemplate(changeOwner.getAccountId()),
@@ -2181,7 +2255,8 @@
                 + "-Assignee added: Change Owner <change@owner.com>\n"
                 + "+Assignee added: <GERRIT_ACCOUNT_1>\n",
             "@@ -6 +6 @@\n"
-                + "-Assignee changed from: Change Owner <change@owner.com> to: Other Account <other@account.com>\n"
+                + "-Assignee changed from: Change Owner <change@owner.com> to: Other Account"
+                + " <other@account.com>\n"
                 + "+Assignee changed from: <GERRIT_ACCOUNT_1> to: <GERRIT_ACCOUNT_2>\n",
             "@@ -6 +6 @@\n"
                 + "-Assignee deleted: Other Account <other@account.com>\n"
@@ -2244,7 +2319,8 @@
                 + "-Assignee added: Change Owner\n"
                 + "+Assignee added: <GERRIT_ACCOUNT_1>\n",
             "@@ -6 +6 @@\n"
-                + "-Assignee changed from: Change Owner <change@owner.com> to: Other Account <other@account.com>\n"
+                + "-Assignee changed from: Change Owner <change@owner.com> to: Other Account"
+                + " <other@account.com>\n"
                 + "+Assignee changed from: <GERRIT_ACCOUNT_1> to: <GERRIT_ACCOUNT_2>\n",
             "@@ -6 +6 @@\n"
                 + "-Assignee deleted: Other Account\n"
@@ -2280,17 +2356,12 @@
     ImmutableList<RevCommit> commitsBeforeRewrite = logMetaRef(repo, metaRefBeforeRewrite);
 
     int invalidCommitIndex = commitsBeforeRewrite.indexOf(invalidUpdateCommit);
-    ChangeNotes notesBeforeRewrite = newNotes(c);
 
     RunOptions options = new RunOptions();
     options.dryRun = false;
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    ChangeNotes notesAfterRewrite = newNotes(c);
-    assertThat(notesBeforeRewrite.getChange().getAssignee()).isEqualTo(otherUserId);
-    assertThat(notesAfterRewrite.getChange().getAssignee()).isEqualTo(otherUserId);
-
     Ref metaRefAfterRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
     assertThat(metaRefAfterRewrite.getObjectId()).isNotEqualTo(metaRefBeforeRewrite.getObjectId());
 
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 153c62c..96a8dea 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -70,7 +70,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
@@ -3456,7 +3455,6 @@
     private boolean wip;
     private boolean abandoned;
     @Nullable private Account.Id mergedBy;
-    @Nullable private Account.Id assigneeId;
 
     @Nullable Change.Id id;
 
@@ -3468,11 +3466,6 @@
       deleteDraftCommentBy = new ArrayList<>();
     }
 
-    DashboardChangeState assignTo(Account.Id assigneeId) {
-      this.assigneeId = assigneeId;
-      return this;
-    }
-
     DashboardChangeState wip() {
       wip = true;
       return this;
@@ -3513,11 +3506,6 @@
       Change change = insert("repo", newChange(repo), ownerId);
       id = change.getId();
       ChangeApi cApi = gApi.changes().id(change.getChangeId());
-      if (assigneeId != null) {
-        AssigneeInput in = new AssigneeInput();
-        in.assignee = "" + assigneeId;
-        cApi.setAssignee(in);
-      }
       if (wip) {
         cApi.setWorkInProgress();
       }
@@ -3595,33 +3583,6 @@
   }
 
   @Test
-  public void dashboardAssignedReviews() throws Exception {
-    repo = createAndOpenProject("repo");
-    Account.Id otherAccountId = createAccount("other");
-    DashboardChangeState otherOpenWip =
-        new DashboardChangeState(otherAccountId).wip().assignTo(user.getAccountId()).create(repo);
-    DashboardChangeState selfOpenWip =
-        new DashboardChangeState(user.getAccountId())
-            .wip()
-            .assignTo(user.getAccountId())
-            .create(repo);
-
-    // Create changes that should not be returned by query.
-    new DashboardChangeState(user.getAccountId()).assignTo(user.getAccountId()).abandon();
-    new DashboardChangeState(user.getAccountId())
-        .assignTo(user.getAccountId())
-        .mergeBy(user.getAccountId());
-
-    assertDashboardQuery(
-        "self", IndexPreloadingUtil.DASHBOARD_ASSIGNED_QUERY, selfOpenWip, otherOpenWip);
-
-    // Viewing another user's dashboard.
-    requestContext.setContext(newRequestContext(otherAccountId));
-    assertDashboardQuery(
-        userId.toString(), IndexPreloadingUtil.DASHBOARD_ASSIGNED_QUERY, otherOpenWip);
-  }
-
-  @Test
   public void dashboardWorkInProgressReviews() throws Exception {
     repo = createAndOpenProject("repo");
     DashboardChangeState ownedOpenWip =
@@ -3662,12 +3623,9 @@
     Account.Id otherAccountId = createAccount("other");
     DashboardChangeState reviewingReviewable =
         new DashboardChangeState(otherAccountId).addReviewer(user.getAccountId()).create(repo);
-    DashboardChangeState assignedReviewable =
-        new DashboardChangeState(otherAccountId).assignTo(user.getAccountId()).create(repo);
 
     // Create changes that should not be returned by any queries in this test.
     new DashboardChangeState(otherAccountId).wip().addReviewer(user.getAccountId()).create(repo);
-    new DashboardChangeState(otherAccountId).wip().assignTo(user.getAccountId()).create(repo);
     new DashboardChangeState(otherAccountId).addReviewer(otherAccountId).create(repo);
     new DashboardChangeState(otherAccountId)
         .addReviewer(user.getAccountId())
@@ -3675,19 +3633,12 @@
         .create(repo);
 
     // Viewing one's own dashboard.
-    assertDashboardQuery(
-        "self",
-        IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY,
-        assignedReviewable,
-        reviewingReviewable);
+    assertDashboardQuery("self", IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY, reviewingReviewable);
 
     // Viewing another user's dashboard.
     requestContext.setContext(newRequestContext(otherAccountId));
     assertDashboardQuery(
-        userId.toString(),
-        IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY,
-        assignedReviewable,
-        reviewingReviewable);
+        userId.toString(), IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY, reviewingReviewable);
   }
 
   @Test
@@ -3706,11 +3657,6 @@
             .addCc(user.getAccountId())
             .mergeBy(user.getAccountId())
             .create(repo);
-    DashboardChangeState mergedAssigned =
-        new DashboardChangeState(otherAccountId)
-            .assignTo(user.getAccountId())
-            .mergeBy(user.getAccountId())
-            .create(repo);
     DashboardChangeState abandonedOwned =
         new DashboardChangeState(user.getAccountId()).abandon().create(repo);
     DashboardChangeState abandonedOwnedWip =
@@ -3720,17 +3666,6 @@
             .addReviewer(user.getAccountId())
             .abandon()
             .create(repo);
-    DashboardChangeState abandonedAssigned =
-        new DashboardChangeState(otherAccountId)
-            .assignTo(user.getAccountId())
-            .abandon()
-            .create(repo);
-    DashboardChangeState abandonedAssignedWip =
-        new DashboardChangeState(otherAccountId)
-            .assignTo(user.getAccountId())
-            .wip()
-            .abandon()
-            .create(repo);
 
     // Create changes that should not be returned by any queries in this test.
     new DashboardChangeState(otherAccountId)
@@ -3743,11 +3678,9 @@
     assertDashboardQuery(
         "self",
         IndexPreloadingUtil.DASHBOARD_RECENTLY_CLOSED_QUERY,
-        abandonedAssigned,
         abandonedReviewing,
         abandonedOwnedWip,
         abandonedOwned,
-        mergedAssigned,
         mergedCced,
         mergedReviewing,
         mergedOwned);
@@ -3757,11 +3690,8 @@
     assertDashboardQuery(
         userId.toString(),
         IndexPreloadingUtil.DASHBOARD_RECENTLY_CLOSED_QUERY,
-        abandonedAssignedWip,
-        abandonedAssigned,
         abandonedReviewing,
         abandonedOwned,
-        mergedAssigned,
         mergedCced,
         mergedReviewing,
         mergedOwned);
@@ -3822,24 +3752,6 @@
     assertThat(changeInfo.attentionSet.get(user2Id.get()).reason).isEqualTo("reason 2");
   }
 
-  @Test
-  public void assignee() throws Exception {
-    repo = createAndOpenProject("repo");
-    Change change1 = insert("repo", newChange(repo));
-    Change change2 = insert("repo", newChange(repo));
-
-    AssigneeInput input = new AssigneeInput();
-    input.assignee = user.getAccountId().toString();
-    gApi.changes().id(change1.getChangeId()).setAssignee(input);
-
-    assertQuery("is:assigned", change1);
-    assertQuery("-is:assigned", change2);
-    assertQuery("is:unassigned", change2);
-    assertQuery("-is:unassigned", change1);
-    assertQuery("assignee:" + user.getAccountId(), change1);
-    assertQuery("-assignee:" + user.getAccountId(), change2);
-  }
-
   @GerritConfig(name = "accounts.visibility", value = "NONE")
   @Test
   public void userDestination() throws Exception {
@@ -3986,14 +3898,6 @@
   }
 
   @Test
-  public void byOwnerInvalidQuery() throws Exception {
-    repo = createAndOpenProject("repo");
-    insert("repo", newChange(repo), userId);
-    String nameEmail = user.asIdentifiedUser().getNameEmail();
-    assertQuery("owner: \"" + nameEmail + "\"\\");
-  }
-
-  @Test
   public void byDeletedChange() throws Exception {
     repo = createAndOpenProject("repo");
     Change change = insert("repo", newChange(repo));
@@ -4067,7 +3971,7 @@
 
   @Test
   public void selfFailsForAnonymousUser() throws Exception {
-    for (String query : ImmutableList.of("assignee:self", "has:star", "is:starred")) {
+    for (String query : ImmutableList.of("has:star", "is:starred")) {
       assertQuery(query);
       RequestContext oldContext = requestContext.setContext(anonymousUserProvider::get);
 
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 5cae012..7f383f9 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -59,16 +59,9 @@
   }
 
   @Test
-  @Override
-  public void byOwnerInvalidQuery() throws Exception {
-    repo = createAndOpenProject("repo");
-    Change change1 = insert("repo", newChange(repo), userId);
-    String nameEmail = user.asIdentifiedUser().getNameEmail();
-
+  public void invalidQuery() throws Exception {
     BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () -> assertQuery("owner: \"" + nameEmail + "\"\\", change1));
+        assertThrows(BadRequestException.class, () -> newQuery("\\").get());
     assertThat(thrown).hasMessageThat().contains("Cannot create full-text query with value: \\");
   }
 
diff --git a/package.json b/package.json
index ae1bb2f..362b9dc 100644
--- a/package.json
+++ b/package.json
@@ -35,8 +35,8 @@
   "scripts": {
     "setup": "yarn && yarn --cwd=polygerrit-ui && yarn --cwd=polygerrit-ui/app",
     "clean": "git clean -fdx && bazel clean --expunge",
-    "compile:local": "tsc --project ./polygerrit-ui/app/tsconfig.json",
-    "compile:watch": "npm run compile:local -- --preserveWatchOutput --watch",
+    "compile": "tsc --project ./polygerrit-ui/app/tsconfig.json",
+    "compile:watch": "npm run compile -- --preserveWatchOutput --watch",
     "start": "run-p -rl compile:watch start:server",
     "start:server": "web-dev-server",
     "test": "yarn --cwd=polygerrit-ui test",
@@ -50,7 +50,8 @@
     "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
-    "litlint": "npm run safe_bazelisk run polygerrit-ui/app:lit_analysis"
+    "litlint": "npm run safe_bazelisk run polygerrit-ui/app:lit_analysis",
+    "lint": "eslint -c polygerrit-ui/app/.eslintrc.js --ignore-path polygerrit-ui/app/.eslintignore polygerrit-ui/app"
   },
   "repository": {
     "type": "git",
diff --git a/plugins/package.json b/plugins/package.json
index 331a417..b7e373a 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -3,7 +3,7 @@
   "description": "Gerrit Code Review - frontend plugin dependencies, each plugin may depend on a subset of these",
   "browser": true,
   "dependencies": {
-    "@gerritcodereview/typescript-api": "3.7.0",
+    "@gerritcodereview/typescript-api": "3.8.0",
     "@polymer/decorators": "^3.0.0",
     "@polymer/polymer": "^3.4.1",
     "@open-wc/testing": "^3.1.6",
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 368b3e0..010bf92 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -35,10 +35,10 @@
   dependencies:
     "@types/chai" "^4.2.12"
 
-"@gerritcodereview/typescript-api@3.7.0":
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.7.0.tgz#ae3886b5c4ddc6a02659a11d577e1df0b6158727"
-  integrity sha512-8zeZClN1gur+Isrn02bRXJ0wUjYvK99jQxg36ZbDelrGDglXKddf8QQkZmaH9sYIRcCFDLlh5+ZlRUTcXTuDVA==
+"@gerritcodereview/typescript-api@3.8.0":
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.8.0.tgz#2e418b814d7451c40365b2dc4f88e9965ece0769"
+  integrity sha512-wUkIWUx99Rj1vxRYQISxyzN0nplqu7t5sRDyJ8R3yNNkvALQAMC6Whj63qzCsZsymVFzC5up3y+ZVxaeh7b+xA==
 
 "@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0":
   version "1.4.1"
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 510ce54..a89c0b7 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -189,7 +189,7 @@
 Compiling code:
 ```sh
 # Compile frontend once to check for type errors:
-yarn compile:local
+yarn compile
 
 # Watch mode:
 yarn compile:watch
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index c519465..89259dc 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -399,19 +399,6 @@
       },
     },
     {
-      files: ['test/functional/**/*.js'],
-      // Settings for functional tests. These scripts are node scripts.
-      // Turn off "no-undef" to allow any global variable
-      env: {
-        browser: false,
-        node: true,
-        es6: false,
-      },
-      rules: {
-        'no-undef': 'off',
-      },
-    },
-    {
       files: ['*_html.js', 'gr-icons.js', '*-theme.js', '*-styles.js'],
       rules: {
         'max-len': 'off',
diff --git a/polygerrit-ui/app/api/package.json b/polygerrit-ui/app/api/package.json
index 79c8bb6..7df755b 100644
--- a/polygerrit-ui/app/api/package.json
+++ b/polygerrit-ui/app/api/package.json
@@ -1,9 +1,9 @@
 {
   "name": "@gerritcodereview/typescript-api",
-  "version": "3.7.0",
+  "version": "3.8.0",
   "description": "Gerrit Code Review - TypeScript API",
   "homepage": "https://www.gerritcodereview.com/",
   "browser": true,
   "dependencies": {},
   "license": "Apache-2.0"
-}
+}
\ No newline at end of file
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 312f384..bef3166 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
@@ -513,7 +513,6 @@
 
   test('obsolete column in preferences not visible', () => {
     assert.isTrue(element.isColumnEnabled('Subject'));
-    assert.isFalse(element.isColumnEnabled('Assignee'));
   });
 
   test('showStar and showNumber', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 371e5b5..72a6a3a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -2080,13 +2080,7 @@
   // Private but used in tests.
   viewStateChanged() {
     if (!this.viewState) return;
-
-    if (this.isChangeObsolete()) {
-      // Tell the app element that we are not going to handle the new change
-      // number and that they have to create a new change view.
-      fireEvent(this, EventType.RECREATE_CHANGE_VIEW);
-      return;
-    }
+    if (this.isChangeObsolete()) return;
 
     if (this.viewState.basePatchNum === undefined)
       this.viewState.basePatchNum = PARENT;
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 9b78e64..ac4c1f9 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
@@ -1557,21 +1557,6 @@
     assert.isOk(element.patchRange?.patchNum);
   });
 
-  test('do not handle new change numbers', async () => {
-    const recreateSpy = sinon.spy();
-    element.addEventListener('recreate-change-view', recreateSpy);
-
-    const value: ChangeViewState = createChangeViewState();
-    element.viewState = value;
-    await element.updateComplete;
-    assert.isFalse(recreateSpy.calledOnce);
-
-    value.changeNum = 555111333 as NumericChangeId;
-    element.viewState = {...value};
-    await element.updateComplete;
-    assert.isTrue(recreateSpy.calledOnce);
-  });
-
   test('related changes are updated when loadData is called', async () => {
     await element.updateComplete;
     const relatedChanges = element.shadowRoot!.querySelector(
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 218594b..f3c23aa 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
@@ -588,7 +588,7 @@
         border: 1px solid var(--border-color);
         border-radius: var(--border-radius);
         margin-top: var(--spacing-m);
-        background-color: var(--assignee-highlight-color);
+        background-color: var(--line-item-highlight-color);
       }
       .attentionTip div gr-icon {
         margin-right: var(--spacing-s);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 4f7d56f..997d9d5 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -1310,15 +1310,17 @@
     const repo = ctx.params[0] as RepoName;
     const commentId = ctx.params[2] as UrlEncodedCommentId;
 
-    const [comments, robotComments, change] = await Promise.all([
+    const [comments, robotComments, drafts, change] = await Promise.all([
       this.restApiService.getDiffComments(changeNum),
       this.restApiService.getDiffRobotComments(changeNum),
+      this.restApiService.getDiffDrafts(changeNum),
       this.restApiService.getChangeDetail(changeNum),
     ]);
 
     const comment =
       findComment(addPath(comments), commentId) ??
-      findComment(addPath(robotComments), commentId);
+      findComment(addPath(robotComments), commentId) ??
+      findComment(addPath(drafts), commentId);
     const path = comment?.path;
     const patchsets = computeAllPatchSets(change);
     const latestPatchNum = computeLatestPatchNum(patchsets);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index c74993f..c7d6948 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -53,11 +53,7 @@
 import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
 import {GrApplyFixDialog} from '../gr-apply-fix-dialog/gr-apply-fix-dialog';
 import {CommentMap} from '../../../utils/comment-util';
-import {
-  EventType,
-  OpenFixPreviewEvent,
-  ValueChangedEvent,
-} from '../../../types/events';
+import {OpenFixPreviewEvent, ValueChangedEvent} from '../../../types/events';
 import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
 import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
 import {Key, toggleClass, whenVisible} from '../../../utils/dom-util';
@@ -417,14 +413,12 @@
       changeNum => {
         if (!changeNum || this.changeNum === changeNum) return;
 
-        // We are only setting the changeNum of the diff view once!
+        // We are only setting the changeNum of the diff view once.
         // Everything in the diff view is tied to the change. It seems better to
         // force the re-creation of the diff view when the change number changes.
-        if (!this.changeNum) {
-          this.changeNum = changeNum;
-        } else {
-          fireEvent(this, EventType.RECREATE_DIFF_VIEW);
-        }
+        // The parent element will make sure that a new change view is created
+        // when the change number changes (using the `keyed` directive).
+        if (!this.changeNum) this.changeNum = changeNum;
       }
     );
     subscribe(
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index eceb05e..b096f76 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -32,7 +32,7 @@
 import {navigationToken} from './core/gr-navigation/gr-navigation';
 import {getAppContext} from '../services/app-context';
 import {routerToken} from './core/gr-router/gr-router';
-import {AccountDetailInfo} from '../types/common';
+import {AccountDetailInfo, NumericChangeId} from '../types/common';
 import {
   constructServerErrorMsg,
   GrErrorManager,
@@ -62,6 +62,7 @@
 import {customElement, property, query, state} from 'lit/decorators.js';
 import {Shortcut, ShortcutController} from './lit/shortcut-controller';
 import {cache} from 'lit/directives/cache.js';
+import {keyed} from 'lit/directives/keyed.js';
 import {assertIsDefined} from '../utils/common-util';
 import './gr-css-mixins';
 import {isDarkTheme, prefersDarkColorScheme} from '../utils/theme-util';
@@ -123,6 +124,9 @@
   // TODO: Introduce a wrapper element for CHANGE, DIFF, EDIT view.
   @state() private childView?: ChangeChildView;
 
+  // Used as a key for caching the CHANGE, DIFF, EDIT view.
+  @state() private changeNum?: NumericChangeId;
+
   @state() private lastError?: ErrorInfo;
 
   // private but used in test
@@ -148,12 +152,6 @@
   // (e.g. shortcut dialog) is open
   @state() private mainAriaHidden = false;
 
-  // Triggers dom-if unsetting/setting restamp behaviour in lit
-  @state() private invalidateChangeViewCache = false;
-
-  // Triggers dom-if unsetting/setting restamp behaviour in lit
-  @state() private invalidateDiffViewCache = false;
-
   @state() private theme = AppTheme.AUTO;
 
   readonly getRouter = resolve(this, routerToken);
@@ -189,12 +187,6 @@
     document.addEventListener(EventType.LOCATION_CHANGE, () =>
       this.handleLocationChange()
     );
-    this.addEventListener(EventType.RECREATE_CHANGE_VIEW, () =>
-      this.handleRecreateView()
-    );
-    this.addEventListener(EventType.RECREATE_DIFF_VIEW, () =>
-      this.handleRecreateView()
-    );
     document.addEventListener(EventType.GR_RPC_LOG, e => this.handleRpcLog(e));
     this.shortcuts.addAbstract(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, () =>
       this.showKeyboardShortcuts()
@@ -246,8 +238,13 @@
     subscribe(
       this,
       () => this.getChangeViewModel().childView$,
-      childView => {
-        this.childView = childView;
+      childView => (this.childView = childView)
+    );
+    subscribe(
+      this,
+      () => this.getChangeViewModel().changeNum$,
+      changeNum => {
+        this.changeNum = changeNum;
       }
     );
 
@@ -375,8 +372,19 @@
       ${this.renderHeader()}
       <main ?aria-hidden=${this.mainAriaHidden}>
         ${this.renderMobileSearch()} ${this.renderChangeListView()}
-        ${this.renderDashboardView()} ${this.renderChangeView()}
-        ${this.renderEditorView()} ${this.renderDiffView()}
+        ${this.renderDashboardView()}
+        ${
+          // `keyed(this.changeNum, ...)` makes sure that these views are not
+          // re-used across changes, which is a precaution, because we have run
+          // into issue with that. That could be re-considered at some point.
+          keyed(
+            this.changeNum,
+            html`
+              ${this.renderChangeView()} ${this.renderEditorView()}
+              ${this.renderDiffView()}
+            `
+          )
+        }
         ${this.renderSettingsView()} ${this.renderAdminView()}
         ${this.renderPluginScreen()} ${this.renderCLAView()}
         ${this.renderDocumentationSearch()}
@@ -473,18 +481,15 @@
   }
 
   private renderChangeView() {
-    if (this.invalidateChangeViewCache) {
-      this.updateComplete.then(() => (this.invalidateChangeViewCache = false));
-      return nothing;
-    }
-    return cache(this.isChangeView() ? this.changeViewTemplate() : nothing);
-  }
-
-  // Template as not to create duplicates, for renderChangeView() only.
-  private changeViewTemplate() {
-    return html`
-      <gr-change-view .backPage=${this.lastSearchPage}></gr-change-view>
-    `;
+    // The `cache()` is required for re-using the change view when switching
+    // back and forth between change, diff and editor views.
+    return cache(
+      this.isChangeView()
+        ? html`<gr-change-view
+            .backPage=${this.lastSearchPage}
+          ></gr-change-view>`
+        : nothing
+    );
   }
 
   private isChangeView() {
@@ -494,9 +499,11 @@
     );
   }
 
-  private isDiffView() {
-    return (
-      this.view === GerritView.CHANGE && this.childView === ChangeChildView.DIFF
+  private renderEditorView() {
+    // The `cache()` is required for re-using the editor view when switching
+    // back and forth between change, diff and editor views.
+    return cache(
+      this.isEditorView() ? html`<gr-editor-view></gr-editor-view>` : nothing
     );
   }
 
@@ -506,21 +513,18 @@
     );
   }
 
-  private renderEditorView() {
-    if (!this.isEditorView()) return nothing;
-    return html`<gr-editor-view></gr-editor-view>`;
-  }
-
   private renderDiffView() {
-    if (this.invalidateDiffViewCache) {
-      this.updateComplete.then(() => (this.invalidateDiffViewCache = false));
-      return nothing;
-    }
-    return cache(this.isDiffView() ? this.diffViewTemplate() : nothing);
+    // The `cache()` is required for re-using the diff view when switching
+    // back and forth between change, diff and editor views.
+    return cache(
+      this.isDiffView() ? html`<gr-diff-view></gr-diff-view>` : nothing
+    );
   }
 
-  private diffViewTemplate() {
-    return html`<gr-diff-view></gr-diff-view>`;
+  private isDiffView() {
+    return (
+      this.view === GerritView.CHANGE && this.childView === ChangeChildView.DIFF
+    );
   }
 
   private renderSettingsView() {
@@ -619,15 +623,6 @@
         (this.account && this.account._account_id) || null;
   }
 
-  /**
-   * Throws away the view and re-creates it. The view itself fires an event, if
-   * it wants to be re-created.
-   */
-  private handleRecreateView() {
-    this.invalidateChangeViewCache = true;
-    this.invalidateDiffViewCache = true;
-  }
-
   private async viewChanged() {
     if (
       this.params &&
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 3187ada..cf279ba 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
@@ -69,19 +69,19 @@
     });
 
     test('does not apply rewrites within links', async () => {
-      element.content = 'google.com/LinkRewriteMe';
+      element.content = 'http://google.com/LinkRewriteMe';
       await element.updateComplete;
 
       assert.shadowDom.equal(
         element,
         /* HTML */ `
           <pre class="plaintext">
-            <a
+            http://google.com/<a
               href="http://google.com/LinkRewriteMe"
               rel="noopener"
               target="_blank"
             >
-              google.com/LinkRewriteMe
+              LinkRewriteMe
             </a>
           </pre>
         `
@@ -149,20 +149,18 @@
     });
 
     test('renders text with links and rewrites', async () => {
-      element.content = `text with plain link: google.com
-        \ntext with config link: LinkRewriteMe
-        \ntext with complex link: A Link 12`;
+      element.content = `
+        text with plain link: http://google.com
+        text with config link: LinkRewriteMe
+        text with complex link: A Link 12`;
       await element.updateComplete;
 
       assert.shadowDom.equal(
         element,
         /* HTML */ `
           <pre class="plaintext">
-            text with plain link:
-            <a href="http://google.com" rel="noopener" target="_blank">
-              google.com
-            </a>
-            text with config link:
+          text with plain link: http://google.com
+        text with config link:
             <a
               href="http://google.com/LinkRewriteMe"
               rel="noopener"
@@ -212,7 +210,7 @@
     });
     test('renders text with links and rewrites', async () => {
       element.content = `text
-        \ntext with plain link: google.com
+        \ntext with plain link: http://google.com
         \ntext with config link: LinkRewriteMe
         \ntext without a link: NotA Link 15 cats
         \ntext with complex link: A Link 12`;
@@ -227,7 +225,7 @@
               <p>
                 text with plain link:
                 <a href="http://google.com" rel="noopener" target="_blank">
-                  google.com
+                  http://google.com
                 </a>
               </p>
               <p>
@@ -259,7 +257,7 @@
 
     test('does not render if too long', async () => {
       element.content = `text
-        text with plain link: google.com
+        text with plain link: http://google.com
         text with config link: LinkRewriteMe
         text without a link: NotA Link 15 cats
         text with complex link: A Link 12`;
@@ -271,9 +269,8 @@
         /* HTML */ `
           <pre class="plaintext">
           text
-        text with plain link:
-          <a href="http://google.com" rel="noopener" target="_blank">google.com</a>
-          text with config link:
+        text with plain link: http://google.com
+        text with config link:
           <a
             href="http://google.com/LinkRewriteMe"
             rel="noopener"
@@ -302,7 +299,7 @@
         \n#### h4-heading
         \n##### h5-heading
         \n###### h6-heading
-        \n# heading with plain link: google.com
+        \n# heading with plain link: http://google.com
         \n# heading with config link: LinkRewriteMe`;
       await element.updateComplete;
 
@@ -320,7 +317,7 @@
               <h1>
                 heading with plain link:
                 <a href="http://google.com" rel="noopener" target="_blank">
-                  google.com
+                  http://google.com
                 </a>
               </h1>
               <h1>
@@ -480,7 +477,7 @@
 
     test('renders block quotes with links and rewrites', async () => {
       element.content = `> block quote
-        \n> block quote with plain link: google.com
+        \n> block quote with plain link: http://google.com
         \n> block quote with config link: LinkRewriteMe`;
       await element.updateComplete;
 
@@ -496,7 +493,7 @@
                 <p>
                   block quote with plain link:
                   <a href="http://google.com" rel="noopener" target="_blank">
-                    google.com
+                    http://google.com
                   </a>
                 </p>
               </blockquote>
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index 1e71bbd..837e362 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -74,7 +74,7 @@
       ...createChangeViewState(),
       repo: 'x+/y+/z+/w' as RepoName,
     };
-    assert.equal(createChangeUrl(state), '/c/x%2B/y%2B/z%2B/w/+/42');
+    assert.equal(createChangeUrl(state), '/c/x%252B/y%252B/z%252B/w/+/42');
   });
 
   test('createDiffUrl', () => {
@@ -85,7 +85,7 @@
     };
     assert.equal(
       createDiffUrl(params),
-      '/c/test-project/+/42/12/x%2By/path.cpp'
+      '/c/test-project/+/42/12/x%252By/path.cpp'
     );
 
     window.CANONICAL_PATH = '/base';
@@ -93,10 +93,10 @@
     window.CANONICAL_PATH = undefined;
 
     params.repo = 'test' as RepoName;
-    assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%2By/path.cpp');
+    assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
 
     params.basePatchNum = 6 as BasePatchSetNum;
-    assert.equal(createDiffUrl(params), '/c/test/+/42/6..12/x%2By/path.cpp');
+    assert.equal(createDiffUrl(params), '/c/test/+/42/6..12/x%252By/path.cpp');
 
     params.diffView = {
       path: 'foo bar/my+file.txt%',
@@ -105,7 +105,7 @@
     delete params.basePatchNum;
     assert.equal(
       createDiffUrl(params),
-      '/c/test/+/42/2/foo+bar/my%2Bfile.txt%2525'
+      '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'
     );
 
     params.diffView = {
@@ -129,7 +129,7 @@
       repo: 'x+/y' as RepoName,
       diffView: {path: 'x+y/path.cpp'},
     };
-    assert.equal(createDiffUrl(params), '/c/x%2B/y/+/42/12/x%2By/path.cpp');
+    assert.equal(createDiffUrl(params), '/c/x%252B/y/+/42/12/x%252By/path.cpp');
   });
 
   test('createEditUrl', () => {
@@ -140,7 +140,7 @@
     };
     assert.equal(
       createEditUrl(params),
-      '/c/test-project/+/42/12/x%2By/path.cpp,edit#31'
+      '/c/test-project/+/42/12/x%252By/path.cpp,edit#31'
     );
 
     window.CANONICAL_PATH = '/base';
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index c65e7a31..155dd6c 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -329,14 +329,6 @@
     license: SharedLicenses.Polymer2018,
   },
   {
-    name: 'ba-linkify',
-    license: {
-      name: 'ba-linkify',
-      type: LicenseTypes.Mit,
-      packageLicenseFile: 'LICENSE-MIT',
-    },
-  },
-  {
     name: 'codemirror-minified',
     license: {
       name: 'codemirror-minified',
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 0df1eda..41f4043 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -34,7 +34,6 @@
     "@types/resize-observer-browser": "^0.1.5",
     "@webcomponents/shadycss": "^1.10.2",
     "@webcomponents/webcomponentsjs": "^1.3.3",
-    "ba-linkify": "^1.0.1",
     "codemirror-minified": "^5.65.0",
     "highlight.js": "^11.5.0",
     "highlightjs-closure-templates": "https://github.com/highlightjs/highlightjs-closure-templates",
@@ -50,4 +49,4 @@
   },
   "license": "Apache-2.0",
   "private": true
-}
\ No newline at end of file
+}
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index b81f6d5..06e8204 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -188,13 +188,22 @@
 
 export function initWebVitals(reportingService: ReportingService) {
   function reportWebVitalMetric(name: Timing, metric: Metric) {
+    let score = metric.value;
+    // CLS good score is 0.1 and poor score is 0.25. Logging system
+    // prefers integers, so we multiple by 100;
+    if (name === Timing.CLS) {
+      score *= 100;
+    }
     reportingService.reporter(
       TIMING.TYPE,
       TIMING.CATEGORY.UI_LATENCY,
       name,
-      metric.value,
-      JSON.stringify(metric),
-      false
+      score,
+      {
+        navigationType: metric.navigationType,
+        rating: metric.rating,
+        entries: metric.entries,
+      }
     );
   }
 
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.ts b/polygerrit-ui/app/styles/gr-change-list-styles.ts
index 3763213..34cf872 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.ts
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.ts
@@ -14,11 +14,11 @@
     background-color: var(--selection-background-color);
   }
   gr-change-list-item[highlight] {
-    background-color: var(--assignee-highlight-color);
+    background-color: var(--line-item-highlight-color);
   }
   gr-change-list-item[highlight][selected],
   gr-change-list-item[highlight]:focus {
-    background-color: var(--assignee-highlight-selection-color);
+    background-color: var(--line-item-highlight-selection-color);
   }
   .groupTitle td,
   .cell {
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 659ec9b..107ee16 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -248,10 +248,13 @@
     --table-subheader-background-color: var(--background-color-tertiary);
     --view-background-color: var(--background-color-primary);
     /* unique background colors */
+    /* TODO: Remove assignee colors once references are migrated */
     --assignee-highlight-color: #fcfad6;
-    /* TODO: Find a nicer way to combine the --assignee-highlight-color and the
-       --selection-background-color than to just invent another unique color. */
     --assignee-highlight-selection-color: #f6f4d0;
+    --line-item-highlight-color: #fcfad6;
+    /* TODO: Find a nicer way to combine the --line-item-highlight-color and the
+       --selection-background-color than to just invent another unique color. */
+    --line-item-highlight-selection-color: #f6f4d0;
     --chip-selected-background-color: var(--blue-50);
     --edit-mode-background-color: #ebf5fb;
     --emphasis-color: #fff9c4;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 5e414f0..a183c86 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -123,8 +123,8 @@
     /* directly derived from primary background colors */
     /*   empty, because inheriting from app-theme is just fine
       /* unique background colors */
-    --assignee-highlight-color: #3a361c;
-    --assignee-highlight-selection-color: #423e24;
+    --line-item-highlight-color: #3a361c;
+    --line-item-highlight-selection-color: #423e24;
     --chip-selected-background-color: #3c4455;
     --edit-mode-background-color: #5c0a36;
     --emphasis-color: #383f4a;
diff --git a/polygerrit-ui/app/test/functional/README.md b/polygerrit-ui/app/test/functional/README.md
deleted file mode 100644
index 82c6133..0000000
--- a/polygerrit-ui/app/test/functional/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Functional test suite
-
-## Installing Docker (OSX)
-
-Simplest way to install all of those is to use Homebrew:
-
-```
-brew cask install docker
-```
-
-This will install a Docker in Applications. To run if from the command-line:
-
-```
-open /Applications/Docker.app
-```
-
-It'll require privileged access and will require user password to be entered.
-
-To validate Docker is installed correctly, run hello-world image:
-
-```
-docker run hello-world
-```
-
-## Building a Docker image
-
-Should be done once only for development purposes, run from the Gerrit checkout
-path:
-
-```
-docker build -t gerrit/polygerrit-functional:v1 \
-  polygerrit-ui/app/test/functional/infra
-```
-
-## Running a smoke test
-
-Running a smoke test from Gerrit checkout path:
-
-```
-./polygerrit-ui/app/test/functional/run_functional.sh
-```
-
-The successful output should be something similar to this:
-
-```
-Starting local server..
-Starting Webdriver..
-Started
-.
-
-
-1 spec, 0 failures
-Finished in 2.565 seconds
-```
diff --git a/polygerrit-ui/app/test/functional/infra/Dockerfile b/polygerrit-ui/app/test/functional/infra/Dockerfile
deleted file mode 100644
index e642176..0000000
--- a/polygerrit-ui/app/test/functional/infra/Dockerfile
+++ /dev/null
@@ -1,38 +0,0 @@
-FROM selenium/standalone-chrome-debug
-
-USER root
-
-# nvm environment variables
-ENV NVM_DIR /usr/local/nvm
-ENV NODE_VERSION 9.4.0
-
-# install nvm
-# https://github.com/creationix/nvm#install-script
-RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
-
-# install node and npm
-RUN [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" \
-    && nvm install $NODE_VERSION \
-    && nvm alias default $NODE_VERSION \
-    && nvm use default
-
-ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
-ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
-
-RUN npm install -g jasmine
-RUN npm install -g http-server
-
-USER seluser
-
-RUN mkdir -p /tmp/app
-WORKDIR /tmp/app
-
-RUN npm init -y
-RUN npm install --save selenium-webdriver
-
-EXPOSE 8080
-
-COPY test-infra.js /tmp/app/node_modules
-COPY run.sh /tmp/app/
-
-ENTRYPOINT [ "/tmp/app/run.sh" ]
diff --git a/polygerrit-ui/app/test/functional/infra/run.sh b/polygerrit-ui/app/test/functional/infra/run.sh
deleted file mode 100755
index 4beb3dd..0000000
--- a/polygerrit-ui/app/test/functional/infra/run.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/sh
-echo Starting local server..
-cp /app/polygerrit_ui.zip .
-unzip -q polygerrit_ui.zip
-nohup http-server polygerrit_ui > /tmp/http-server.log 2>&1 &
-
-echo Starting Webdriver..
-nohup /opt/bin/entry_point.sh > /tmp/webdriver.log 2>&1 &
-
-# Wait for servers to start
-sleep 5
-
-cp $@ .
-jasmine $(basename $@)
diff --git a/polygerrit-ui/app/test/functional/infra/test-infra.js b/polygerrit-ui/app/test/functional/infra/test-infra.js
deleted file mode 100644
index 2619694..0000000
--- a/polygerrit-ui/app/test/functional/infra/test-infra.js
+++ /dev/null
@@ -1,24 +0,0 @@
-'use strict';
-
-const {Builder} = require('selenium-webdriver');
-
-let driver;
-
-function setup() {
-  return new Builder()
-      .forBrowser('chrome')
-      .usingServer('http://localhost:4444/wd/hub')
-      .build()
-      .then(d => {
-        driver = d;
-        return driver.get('http://localhost:8080');
-      })
-      .then(() => driver);
-}
-
-function cleanup() {
-  return driver.quit();
-}
-
-exports.setup = setup;
-exports.cleanup = cleanup;
diff --git a/polygerrit-ui/app/test/functional/run_functional.sh b/polygerrit-ui/app/test/functional/run_functional.sh
deleted file mode 100755
index 7ce57b8..0000000
--- a/polygerrit-ui/app/test/functional/run_functional.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env bash
-
-bazel build //polygerrit-ui/app:polygerrit_ui
-
-docker run --rm \
-  -p 5900:5900 \
-  -v `pwd`/polygerrit-ui/app/test/functional:/tests \
-  -v `pwd`/bazel-genfiles/polygerrit-ui/app:/app \
-  -it gerrit/polygerrit-functional:v1 \
-  /tests/test.js
diff --git a/polygerrit-ui/app/test/functional/test.js b/polygerrit-ui/app/test/functional/test.js
deleted file mode 100644
index ae572af..0000000
--- a/polygerrit-ui/app/test/functional/test.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * @fileoverview Minimal viable frontend functional test.
- */
-'use strict';
-
-const {until} = require('selenium-webdriver');
-const {setup, cleanup} = require('test-infra');
-
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
-
-describe('example ', () => {
-  let driver;
-
-  beforeAll(() => setup().then(d => driver = d));
-
-  afterAll(() => cleanup());
-
-  it('should update title', () => driver.wait(
-      until.titleIs('status:open · Gerrit Code Review'), 5000
-  ));
-});
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index f642af7..2cc6742 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -28,8 +28,6 @@
   OPEN_FIX_PREVIEW = 'open-fix-preview',
   CLOSE_FIX_PREVIEW = 'close-fix-preview',
   PAGE_ERROR = 'page-error',
-  RECREATE_CHANGE_VIEW = 'recreate-change-view',
-  RECREATE_DIFF_VIEW = 'recreate-diff-view',
   RELOAD = 'reload',
   REPLY = 'reply',
   SERVER_ERROR = 'server-error',
diff --git a/polygerrit-ui/app/utils/link-util.ts b/polygerrit-ui/app/utils/link-util.ts
index ec1e7e7..48e9c07 100644
--- a/polygerrit-ui/app/utils/link-util.ts
+++ b/polygerrit-ui/app/utils/link-util.ts
@@ -3,7 +3,6 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import 'ba-linkify/ba-linkify';
 import {CommentLinkInfo, CommentLinks} from '../types/common';
 import {getBaseUrl} from './url-util';
 
@@ -16,21 +15,8 @@
   base: string,
   repoCommentLinks: CommentLinks
 ): string {
-  const parts: string[] = [];
-  window.linkify(insertZeroWidthSpace(base), {
-    callback: (text, href) => {
-      if (href) {
-        parts.push(removeZeroWidthSpace(createLinkTemplate(href, text)));
-      } else {
-        const rewriteResults = getRewriteResultsFromConfig(
-          text,
-          repoCommentLinks
-        );
-        parts.push(removeZeroWidthSpace(applyRewrites(text, rewriteResults)));
-      }
-    },
-  });
-  return parts.join('');
+  const rewriteResults = getRewriteResultsFromConfig(base, repoCommentLinks);
+  return applyRewrites(base, rewriteResults);
 }
 
 /**
@@ -141,22 +127,6 @@
   );
 }
 
-/**
- * Some tools are known to look for reviewers/CCs by finding lines such as
- * "R=foo@gmail.com, bar@gmail.com". However, "=" is technically a valid email
- * character, so ba-linkify interprets the entire string "R=foo@gmail.com" as an
- * email address. To fix this, we insert a zero width space character \u200B
- * before linking that prevents ba-linkify from associating the prefix with the
- * email. After linking we remove the zero width space.
- */
-function insertZeroWidthSpace(base: string) {
-  return base.replace(/^(R=|CC=)/g, '$&\u200B');
-}
-
-function removeZeroWidthSpace(base: string) {
-  return base.replace(/\u200B/g, '');
-}
-
 function createLinkTemplate(
   href: string,
   displayText: string,
diff --git a/polygerrit-ui/app/utils/link-util_test.ts b/polygerrit-ui/app/utils/link-util_test.ts
index f5c13e8..e4e719b 100644
--- a/polygerrit-ui/app/utils/link-util_test.ts
+++ b/polygerrit-ui/app/utils/link-util_test.ts
@@ -63,18 +63,6 @@
         `${link('foo', 'foo.gov')} ${link('foo', 'foo.gov')}`
       );
     });
-
-    test('does not apply within normal links', () => {
-      assert.equal(
-        linkifyUrlsAndApplyRewrite('google.com', {
-          ogle: {
-            match: 'ogle',
-            link: 'gerritcodereview.com',
-          },
-        }),
-        link('google.com', 'http://google.com')
-      );
-    });
   });
 
   test('for overlapping rewrites prefer the latest ending', () => {
@@ -165,45 +153,4 @@
       )}`
     );
   });
-
-  suite('normal links', () => {
-    test('links urls', () => {
-      const googleLink = link('google.com', 'http://google.com');
-      const mapsLink = link('maps.google.com', 'http://maps.google.com');
-
-      assert.equal(
-        linkifyUrlsAndApplyRewrite('google.com, maps.google.com', {}),
-        `${googleLink}, ${mapsLink}`
-      );
-    });
-
-    test('links emails without including R= prefix', () => {
-      const fooEmail = link('foo@gmail.com', 'mailto:foo@gmail.com');
-      const barEmail = link('bar@gmail.com', 'mailto:bar@gmail.com');
-      assert.equal(
-        linkifyUrlsAndApplyRewrite('R=foo@gmail.com, bar@gmail.com', {}),
-        `R=${fooEmail}, ${barEmail}`
-      );
-    });
-
-    test('links emails without including CC= prefix', () => {
-      const fooEmail = link('foo@gmail.com', 'mailto:foo@gmail.com');
-      const barEmail = link('bar@gmail.com', 'mailto:bar@gmail.com');
-      assert.equal(
-        linkifyUrlsAndApplyRewrite('CC=foo@gmail.com, bar@gmail.com', {}),
-        `CC=${fooEmail}, ${barEmail}`
-      );
-    });
-
-    test('links emails maintains R= and CC= within addresses', () => {
-      const fooBarBazEmail = link(
-        'fooR=barCC=baz@gmail.com',
-        'mailto:fooR=barCC=baz@gmail.com'
-      );
-      assert.equal(
-        linkifyUrlsAndApplyRewrite('fooR=barCC=baz@gmail.com', {}),
-        fooBarBazEmail
-      );
-    });
-  });
 });
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 5e294cb..af1e32e 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -92,6 +92,9 @@
   // to not double encode *everything* (just for readaiblity and simplicity),
   // but `%` *must* be double encoded.
   let output = url.replaceAll('%', '%25');
+  // `+` also requires double encoding, because `%2B` would be decoded to `+`
+  // and then replaced by ` `.
+  output = output.replaceAll('+', '%2B');
 
   // This escapes ALL characters EXCEPT:
   // A–Z a–z 0–9 - _ . ! ~ * ' ( )
@@ -138,6 +141,10 @@
  * Single decode for URL components. Will decode plus signs ('+') to spaces.
  * Note: because this function decodes once, it is not the inverse of
  * encodeURL.
+ *
+ * This function must only be used for decoding data returned by the REST API.
+ * Don't use it for decoding browser URLs. The only place for decoding browser
+ * URLs must gr-page.ts.
  */
 export function singleDecodeURL(url: string): string {
   const withoutPlus = url.replace(/\+/g, '%20');
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index 16f85dd..7466a90 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -110,6 +110,10 @@
         assert.equal(encodeURL('abc%def'), 'abc%2525def');
       });
 
+      test('double encodes +', () => {
+        assert.equal(encodeURL('abc+def'), 'abc%252Bdef');
+      });
+
       test('does not encode colon and slash', () => {
         assert.equal(encodeURL(':/'), ':/');
       });
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 2edce7e..7f8168e 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -488,11 +488,6 @@
     delegates "^1.0.0"
     readable-stream "^2.0.6"
 
-ba-linkify@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/ba-linkify/-/ba-linkify-1.0.1.tgz#664cf5744947c6e8611f1fbbaf7d9f315f982f4c"
-  integrity sha1-Zkz1dElHxuhhHx+7r32fMV+YL0w=
-
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
diff --git a/proto/cache.proto b/proto/cache.proto
index 0cecdea..7063ee5 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -198,14 +198,7 @@
   string server_id = 20;
   bool has_server_id = 21;
 
-  message AssigneeStatusUpdateProto {
-    // Epoch millis.
-    int64 timestamp_millis = 1;
-    int32 updated_by = 2;
-    int32 current_assignee = 3;
-    bool has_current_assignee = 4;
-  }
-  repeated AssigneeStatusUpdateProto assignee_update = 22;
+  reserved 22;  // assignee_update;
 
   // An update to the attention set of the change. See class AttentionSetUpdate
   // for context.
diff --git a/resources/com/google/gerrit/server/mail/SetAssignee.soy b/resources/com/google/gerrit/server/mail/SetAssignee.soy
deleted file mode 100644
index 83aa580..0000000
--- a/resources/com/google/gerrit/server/mail/SetAssignee.soy
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-{namespace com.google.gerrit.server.mail.template.SetAssignee}
-
-/**
- * The .SetAssignee template will determine the contents of the email related
- * to a user being assigned to a change.
- */
-{template SetAssignee kind="text"}
-  {@param change: ?}
-  {@param email: ?}
-  {@param fromName: ?}
-  {@param patchSet: ?}
-  {@param projectName: ?}
-  Hello{sp}
-  {$email.assigneeName},
-
-  {\n}
-  {\n}
-
-  {$fromName} has assigned a change to you.
-
-  {sp}Please visit
-
-  {\n}
-  {\n}
-
-  {sp}{sp}{sp}{sp}{$email.changeUrl}
-
-  {\n}
-  {\n}
-
-  to view the change.
-
-  {\n}
-  {\n}
-
-  Change subject: {$change.subject}{\n}
-  ......................................................................{\n}
-
-  {\n}
-
-  {$email.changeDetail}{\n}
-
-  {if $email.sshHost}
-    {\n}
-    {sp}{sp}git pull ssh:{print '//'}{$email.sshHost}/{$projectName}
-        {sp}{$patchSet.refName}
-    {\n}
-  {/if}
-
-  {if $email.includeDiff}
-    {\n}
-    {$email.unifiedDiff}
-    {\n}
-  {/if}
-{/template}
diff --git a/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy b/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
deleted file mode 100644
index 5435cab..0000000
--- a/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-{namespace com.google.gerrit.server.mail.template.SetAssigneeHtml}
-
-import * as mailTemplate from 'com/google/gerrit/server/mail/Private.soy';
-
-{template SetAssigneeHtml}
-  {@param diffLines: ?}
-  {@param email: ?}
-  {@param fromName: ?}
-  {@param patchSet: ?}
-  {@param projectName: ?}
-  <p>
-    {$fromName} has <strong>assigned</strong> a change to{sp}
-    {$email.assigneeName}.{sp}
-  </p>
-
-  {if $email.changeUrl}
-    <p>
-      {call mailTemplate.ViewChangeButton data="all" /}
-    </p>
-  {/if}
-
-  {call mailTemplate.Pre}
-    {param content: $email.changeDetail /}
-  {/call}
-
-  {if $email.sshHost}
-    {call mailTemplate.Pre}
-      {param content kind="html"}
-        git pull ssh:{print '//'}{$email.sshHost}/{$projectName}
-        {sp}{$patchSet.refName}
-      {/param}
-    {/call}
-  {/if}
-
-  {if $email.includeDiff}
-    {call mailTemplate.UnifiedDiff}
-      {param diffLines: $diffLines /}
-    {/call}
-  {/if}
-{/template}