Merge "Do not reload page after clicking Cancel in apply-fix dialog"
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index c6d9fb4..8088b66 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -293,12 +293,12 @@
 External IDs are stored as Git Notes in the `All-Users` repository. The
 name of the notes branch is `refs/meta/external-ids`.
 
-As note key the SHA1 of the external ID key is used, for example the key
+As note key the SHA-1 of the external ID key is used, for example the key
 for the external ID `username:jdoe` is `e0b751ae90ef039f320e097d7d212f490e933706`.
 This ensures that an external ID is used only once (e.g. an external ID can
 never be assigned to multiple accounts at a point in time).
 
-The following commands show how to find the SHA1 of an external ID:
+The following commands show how to find the SHA-1 of an external ID:
 
 ----
 $ echo -n 'gerrit:jdoe' | shasum
@@ -310,7 +310,7 @@
 
 [IMPORTANT]
 If the external ID key is changed manually you must adapt the note key
-to the new SHA1, otherwise the external ID becomes inconsistent and is
+to the new SHA-1, otherwise the external ID becomes inconsistent and is
 ignored by Gerrit.
 
 The note content is a Git config file:
@@ -322,7 +322,7 @@
   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
 ----
 
-Once SHA1 of an external ID is known the following command can be used to
+Once SHA-1 of an external ID is known the following command can be used to
 show the content of the note:
 
 ----
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ed4cf5a..3ad4401 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2451,7 +2451,7 @@
 at a specific commit when `gitweb.type` is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit
-and `${commit}` for the SHA1 hash for the commit.
+and `${commit}` for the SHA-1 hash for the commit.
 
 [[gitweb.project]]gitweb.project::
 +
@@ -2483,7 +2483,7 @@
 is set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit
-and `${commit}` for the SHA1 hash for the commit.
+and `${commit}` for the SHA-1 hash for the commit.
 
 [[gitweb.file]]gitweb.file::
 +
@@ -2492,7 +2492,7 @@
 set to `custom`.
 +
 Valid replacements are `${project}` for the project name in Gerrit,
-`${file}` for the file name and `${commit}` for the SHA1 hash for
+`${file}` for the file name and `${commit}` for the SHA-1 hash for
 the commit.
 
 [[gitweb.filehistory]]gitweb.filehistory::
@@ -4196,7 +4196,7 @@
 very CPU-heavy operation. For non public Gerrit-servers this check may
 be overkill.
 +
-Only disable this check if you trust the clients not to forge SHA1
+Only disable this check if you trust the clients not to forge SHA-1
 references to access commits intended to be hidden from the user.
 +
 Default is true.
diff --git a/Documentation/config-groups.txt b/Documentation/config-groups.txt
index afabbfc..0917515 100644
--- a/Documentation/config-groups.txt
+++ b/Documentation/config-groups.txt
@@ -64,7 +64,7 @@
 
 The format of this map is as follows:
 
-* keys are the normal SHA1 of the group name
+* keys are the normal SHA-1 of the group name
 * values are blobs that look like
 +
 ----
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 56d26dc..b6184d7 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -353,7 +353,7 @@
 If true, all scores for the label are copied forward when a new patch
 set is uploaded that has the same parent tree, code delta, and commit
 message as the previous patch set. This means that only the patch
-set SHA1 is different. This can be used to enable sticky
+set SHA-1 is different. This can be used to enable sticky
 approvals, reducing turn-around for this special case.
 It is recommended to leave this enabled for both Verified and
 Code-Review labels.
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 14399a3..37ab0c0 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -44,9 +44,9 @@
 * `http://` -- trust all OpenID providers using the HTTP protocol
 * `https://` -- trust all OpenID providers using the HTTPS protocol
 
-To trust only Yahoo!:
+To trust only Launchpad:
 ----
-  git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://me.yahoo.com
+  git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://login.launchpad.net/+openid
 ----
 
 === Database Schema
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
index 7935f30..15bf785 100644
--- a/Documentation/dev-crafting-changes.txt
+++ b/Documentation/dev-crafting-changes.txt
@@ -306,6 +306,30 @@
     and it also makes `git revert` more useful.
   * Use topics to link your separate changes together.
 
+[[opportunistic-refactoring]]
+== Opportunistic Refactoring
+
+Opportunistic Refactoring is a terminology
+link:https://martinfowler.com/bliki/OpportunisticRefactoring.html[used by Martin Fowler,role=external,window=_blank]
+also known as the "boy scout rule" of the software developer:
+"always leave the code behind in a better state than you found it."
+
+In practice, this rule means you should not add technical debt in the code while
+implementing a new feature or fixing a bug. If you or a reviewer find an
+opportunity to clean up the code during implementation or review of your change,
+take the time to do a little cleanup to improve the overall code base.
+
+When approaching refactoring, keep in mind that changes should do one thing
+(<<change-size,see change size section above>>). If a change you're making
+requires cleanup/refactoring, it is best to do that cleanup in a preparatory and
+separate change. Likewise, if during review for a functional change, an
+opportunity for cleanup/refactoring is discovered, then it is preferable to do
+the cleanup first in a separate change so as to improve the reviewability of the
+functional change.
+
+Reviewers should keep in mind the scope of the change under review and ensure
+suggested refactoring is aligned with that scope.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 203b368..a41c9ea 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -182,7 +182,7 @@
   advertisement if the user lacks read permissions
 
 * Uploads through the git wire protocol must refuse commits that are
-  based on SHA1s for data that the user can't see.
+  based on SHA-1s for data that the user can't see.
 
 * Tags are only visible if their commits are visible to user through a
   non-tag reference.
@@ -195,7 +195,7 @@
 === Indexing
 
 Almost all data is stored as Git, but Git only supports fast lookup by
-SHA1 or by ref (branch) name. Therefore Gerrit also has an indexing
+SHA-1 or by ref (branch) name. Therefore Gerrit also has an indexing
 system (powered by Lucene by default) for other types of queries.
 There are 4 indices:
 
@@ -204,8 +204,8 @@
 * Group index - find groups by name, owner, description etc.
 * Change index - find changes by file, status, modification date etc.
 
-The base entities are characterized by SHA1s. Storing the
-characterizing SHA1s allows detection of stale index entries.
+The base entities are characterized by SHA-1s. Storing the
+characterizing SHA-1s allows detection of stale index entries.
 
 == Plug-in architecture
 
@@ -397,7 +397,7 @@
 * Caching: metadata is stored in Git, which is relatively expensive to
   access. This is sped up by multiple caches. Metadata entities are
   stored in Git, and can therefore be seen as immutable values keyed
-  by SHA1, which is very amenable to caching. All SHA1 keyed caches
+  by SHA-1, which is very amenable to caching. All SHA-1 keyed caches
   can be persisted on local disk.
 
   The size (memory, disk) of these caches should be adapted to the
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 0db6026..fb17e5c 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2230,7 +2230,7 @@
 light-weight plugin that links commits to external
 tools (GitBlit, CGit, company specific resources etc).
 
-PatchSetWebLinks will appear to the right of the commit-SHA1 in the UI.
+PatchSetWebLinks will appear to the right of the commit-SHA-1 in the UI.
 
 [source, java]
 ----
@@ -2256,7 +2256,7 @@
 }
 ----
 
-ParentWebLinks will appear to the right of the SHA1 of the parent
+ParentWebLinks will appear to the right of the SHA-1 of the parent
 revisions in the UI. The implementation should in most use cases direct
 to the same external service as PatchSetWebLink; it is provided as a
 separate interface because not all users want to have links for the
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index a7240e2..0849c56 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -153,7 +153,7 @@
 Tag the plugins:
 
 ----
-  git submodule foreach '[ "$path" == "modules/jgit" ] || git tag -s -m "v$version" "v$version"'
+  git submodule foreach '[ "$sm_path" == "modules/jgit" ] || git tag -s -m "v$version" "v$version"'
 ----
 
 [[build-gerrit]]
@@ -324,7 +324,7 @@
 Push the new Release Tag on the plugins:
 
 ----
-  git submodule foreach git push gerrit-review tag v$version
+  git submodule foreach '[ "$sm_path" == "modules/jgit" ] || git push gerrit-review tag "v$version"'
 ----
 
 [[upload-documentation]]
diff --git a/Documentation/glossary.txt b/Documentation/glossary.txt
new file mode 100644
index 0000000..2b40b5b
--- /dev/null
+++ b/Documentation/glossary.txt
@@ -0,0 +1,50 @@
+:linkattrs:
+= Glossary
+
+[[event]]
+== Event
+
+It refers to the link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/java/com/google/gerrit/server/events/Event.java[com.google.gerrit.server.events.Event]
+base abstract class representing any possible action that is generated or
+received in a Gerrit instance. Actions can be associated with change set status
+updates, project creations, indexing of changes, etc.
+
+[[event-broker]]
+== Event broker
+
+Distributes Gerrit Events to listeners if they are allowed to see them.
+
+[[event-dispatcher]]
+== Event dispatcher
+
+Interface for posting events to the Gerrit event system. Implemented by default
+by link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/java/com/google/gerrit/server/events/EventBroker.java[com.google.gerrit.server.events.EventBroker].
+It can be implemented by plugins and allows to influence how events are managed.
+
+[[event-hierarchy]]
+== Event hierarchy
+
+Hierarchy of events representing anything that can happen in Gerrit.
+
+[[event-listener]]
+== Event listener
+
+API for listening to Gerrit events from plugins, without having any
+visibility restrictions.
+
+[[stream-events]]
+== Stream events
+
+Command that allows a user via CLI or a plugin to receive in a sequential way
+some events that are generated in Gerrit. The consumption of the stream by default
+is available via SSH connection.
+However, plugins can provide an alternative implementation of the event
+brokering by sending them over a reliable messaging queueing system (RabbitMQ)
+or a pub-sub (Kafka).
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 164039b..e56e7ca 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -87,6 +87,7 @@
 . link:concept-changes.html[Changes]
 . link:concept-refs-for-namespace.html[The refs/for Namespace]
 . link:concept-patch-sets.html[Patch Sets]
+. link:glossary.html[Glossary]
 
 == Resources
 * link:licenses.html[Licenses and Notices]
diff --git a/Documentation/intro-gerrit-walkthrough-github.txt b/Documentation/intro-gerrit-walkthrough-github.txt
index f16155b..8f3ff88 100644
--- a/Documentation/intro-gerrit-walkthrough-github.txt
+++ b/Documentation/intro-gerrit-walkthrough-github.txt
@@ -206,8 +206,8 @@
 When you `git commit --amend` to iterate on your change, you might be worried that
 you are changing your previous commit and may thus lose that state of your work.
 However, here the Change-Id appended to your commit message comes into play.
-While the SHA1 hash of your change (the commit ID used by Git) might change, the
-Change-Id stays the same (in fact it is the SHA1 hash of the very first version
+While the SHA-1 hash of your change (the commit ID used by Git) might change, the
+Change-Id stays the same (in fact it is the SHA-1 hash of the very first version
 of that commit). When this amended commit is uploaded to the Gerrit server,
 Gerrit knows that this commit is really an iteration of that previous commit
 (and the associated review) and will preserve both, the old and the new state.
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index a13cbfb..b376d6e 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -36,6 +36,67 @@
 not available in 3.0, so any upgrade from Gerrit 2.x to 3.x must go through
 2.16 to effect the NoteDb upgrade.
 
+== Format
+
+Each review ("change") in Gerrit is numbered. The different revisions
+("patchsets") of a change 12345 are stored under
+----
+  refs/changes/45/12345/${PATCHSET_NUMBER}
+----
+
+The revisions are stored as commits to the main project, ie. if you
+fetch this ref, you can check out the proposed change.
+
+A change 12345 has its review metadata under
+----
+  refs/changes/45/12345/meta
+----
+The metadata is a notes branch. The commit messages on the branch hold
+modifications to global data of the change (votes, global comments). The inline
+comments are in a
+link:https://git.eclipse.org/r/plugins/gitiles/jgit/jgit/\+/master/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java[NoteMap],
+where the key is the commit SHA-1 of the patchset
+that the comment refers to, and the value is JSON data. The format of the
+JSON is in the
+link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/server/notedb/RevisionNoteData.java[RevisionNoteData]
+which contains 
+link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/entities/Comment.java[Comment] entities.
+
+For example:
+----
+   {
+      "key": {
+        "uuid": "c7be1334_47885e36",
+        "filename":
+"java/com/google/gerrit/server/restapi/project/CommitsCollection.java",
+        "patchSetId": 7
+      },
+      "lineNbr": 158,
+      "author": {
+        "id": 1026112
+      },
+      "writtenOn": "2019-11-06T09:00:50Z",
+      "side": 1,
+      "message": "nit: factor this out in a variable, use
+toImmutableList as collector",
+      "range": {
+        "startLine": 156,
+        "startChar": 32,
+        "endLine": 158,
+        "endChar": 66
+      },
+      "revId": "071c601d6ee1a2a9f520415fd9efef8e00f9cf60",
+      "serverId": "173816e5-2b9a-37c3-8a2e-48639d4f1153",
+      "unresolved": true
+    },
+----
+
+Automated systems may post "robot comments" instead of normal
+comments, which are an extension of the previous comment, defined in
+the
+link:https://gerrit.googlesource.com/gerrit/\+/master/java/com/google/gerrit/entities/RobotComment.java[RobotComment]
+class.
+
 [[migration]]
 == Migration
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ccffa44..721900f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -565,10 +565,73 @@
 ----
 
 Historical state of the change can be retrieved by specifying the
-`meta=SHA1` parameter. This will use a historical NoteDb snapshot to
-populate ChangeInfo. If the SHA1 is not reachable as a NoteDb state,
+`meta=SHA-1` parameter. This will use a historical NoteDb snapshot to
+populate ChangeInfo. If the SHA-1 is not reachable as a NoteDb state,
 status code 412 is returned.
 
+----
+
+[[get-meta-diff]]
+=== Get Meta Diff
+--
+'GET /changes/link:#change-id[\{change-id\}]/meta_diff/?old=SHA-1&meta=SHA-1'
+--
+
+Retrieves the difference between two historical states of a change
+by specifying the `old=SHA-1` and the `meta=SHA-1` parameters.
+
+If the `old` parameter is not provided, the parent of the `meta`
+SHA-1 is used. If the `meta` parameter is not provided, the current
+state of the change is used. If neither are provided, the
+difference between the current state of the change and its previous
+state is returned.
+
+Additional fields can be obtained by adding `o` parameters, analogous
+to link:#get-change[Get Change], and the same concerns for Get Change hold for
+this endpoint too. Fields are described in link:#list-changes[Query Changes].
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/meta_diff?old=b083abc34eb6dbdb9e154ba092fc198000e997b4&meta=63b81f2bde703ae07787a940e8fdf9a1828605b1 HTTP/1.0
+----
+
+As a response, two link:#change-info[ChangeInfo] entities are returned
+that describe information added and removed from the `old` change state.
+Only fields that differ between the change's two states are returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "added": {
+      "attention_set": [
+        {
+          "account": {
+            "name": "John Doe"
+          },
+         "last_update": "2013-02-21 11:16:36.775000000",
+         "reason": "reviewer or cc replied"
+        }
+      ]
+      "updated": "2013-02-21 11:16:36.775000000",
+      "topic": "new-topic"
+    },
+    "removed": {
+      "updated": "2013-02-20 12:05:34.111000000",
+      "topic": "old-topic"
+    }
+  }
+----
+
+If the provided SHA-1 for `meta` is not reachable as a NoteDb
+state, the status code 412 is returned. If the SHA-1 for `old`
+is not reachable, the difference between the change at state
+`meta` and an empty change is returned.
+----
 
 [[get-change-detail]]
 === Get Change Detail
@@ -1140,7 +1203,7 @@
 --
 
 Check if the given change is a pure revert of the change it references in `revertOf`.
-Optionally, the query parameter `o` can be passed in to specify a commit (SHA1 in
+Optionally, the query parameter `o` can be passed in to specify a commit (SHA-1 in
 40 digit hex representation) to check against. It takes precedence over `revertOf`.
 If the change has no reference in `revertOf`, the parameter is mandatory.
 
@@ -3513,6 +3576,9 @@
 Deletes a single vote from a change. Note, that even when the last vote of
 a reviewer is removed the reviewer itself is still listed on the change.
 
+If another user removed a user's vote, the user with the deleted vote will be
+added to the attention set.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
@@ -5877,6 +5943,9 @@
 Note, that even when the last vote of a reviewer is removed the reviewer itself
 is still listed on the change.
 
+If another user removed a user's vote, the user with the deleted vote will be
+added to the attention set.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
@@ -6055,6 +6124,7 @@
 * The change is marked ready for review.
 * As an owner/uploader, when someone replies on your change.
 * As a reviewer, when the owner/uploader replies.
+* When the user's vote is deleted by another user.
 
 Users are removed from the attention set if one the following apply:
 
@@ -6478,7 +6548,7 @@
 (in which case it will only contain a key for the current revision) or
 if link:#all-revisions[all revisions] are requested.
 |`meta_rev_id`           |optional|
-The SHA1 of the NoteDb meta ref.
+The SHA-1 of the NoteDb meta ref.
 |`tracking_ids`       |optional|
 A list of link:#tracking-id-info[TrackingIdInfo] entities describing
 references to external tracking systems. Only set if
@@ -6716,7 +6786,7 @@
 Contains the link:rest-api-changes.html#change-message-info[id] of the change
 message that this comment is linked to.
 |`commit_id` |optional|
-Hex commit SHA1 (40 characters string) of the commit of the patchset to which
+Hex commit SHA-1 (40 characters string) of the commit of the patchset to which
 this comment applies.
 |`context_lines` |optional|
 A list of link:#context-line[ContextLine] containing the lines of the source
@@ -7508,7 +7578,7 @@
 |===========================
 |Field Name    ||Description
 |`base`        |optional|
-The new parent revision. This can be a ref or a SHA1 to a concrete patchset. +
+The new parent revision. This can be a ref or a SHA-1 to a concrete patchset. +
 Alternatively, a change number can be specified, in which case the current
 patch set is inferred. +
 Empty string is used for rebasing directly on top of the target branch,
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 4697afc..ea927c7 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -47,6 +47,8 @@
   attention set.
 * For merged and abandoned changes the owner is added only when a human creates
   an unresolved comment.
+* If another user removed a user's vote, the user with the deleted vote will be
+  added to the attention set.
 * Only owner, uploader, reviewers and ccs can be in the attention set.
 * The rules for service accounts are different, see link:#bots[Bots].
 
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index cd26792..4a9d18f 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -38,11 +38,11 @@
    *  Select branch for new change: Specify the destination branch of the
       change.
 
-   *  Provide base commit SHA1 for change: Leave this field blank.
+   *  Provide base commit SHA-1 for change: Leave this field blank.
 
 +
-IMPORTANT: Git uses a unique SHA1 value to identify each and every commit (in
-other words, each Git commit generates a new SHA1 hash). This value differs
+IMPORTANT: Git uses a unique SHA-1 value to identify each and every commit (in
+other words, each Git commit generates a new SHA-1 hash). This value differs
 from a Gerrit Change-Id, which is used by Gerrit to uniquely identify a
 change. The Gerrit Change-Id remains static throughout the life of a Gerrit
 change.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 52c282e..377012a 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -35,7 +35,7 @@
 |=============================================================
 
 For change searches (i.e. those using a numerical id, Change-Id, or commit
-SHA1), if the search results in a single change that change will be
+SHA-1), if the search results in a single change that change will be
 presented instead of a list.
 
 For more predictable results, use explicit search operators as described
@@ -173,9 +173,9 @@
 Changes that have been, or need to be, reviewed by a user in 'GROUP'.
 
 [[commit]]
-commit:'SHA1'::
+commit:'SHA-1'::
 +
-Changes where 'SHA1' is one of the patch sets of the change.
+Changes where 'SHA-1' is one of the patch sets of the change.
 
 [[project]]
 project:'PROJECT', p:'PROJECT'::
diff --git a/WORKSPACE b/WORKSPACE
index cffbc8d..8dad0f9 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -66,8 +66,8 @@
 
 http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "fcc6dccb39ca88d481224536eb8f9fa754619676c6163f87aa6af94059b02b12",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.0/rules_nodejs-3.2.0.tar.gz"],
+    sha256 = "dd7ea7efda7655c218ca707f55c3e1b9c68055a70c31a98f264b3445bc8f4cb1",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.3/rules_nodejs-3.2.3.tar.gz"],
 )
 
 # Golang support for PolyGerrit local dev server.
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject-body.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject-body.json
new file mode 100644
index 0000000..488de6d
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject-body.json
@@ -0,0 +1,3 @@
+{
+  "force": "${force_project_deletion}"
+}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
index 1752634..eb4df30 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteProject.scala
@@ -20,6 +20,7 @@
 
 class DeleteProject extends ProjectSimulation {
   private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val forceKey = "force_project_deletion"
 
   def this(projectName: String) {
     this()
@@ -28,7 +29,10 @@
 
   val test: ScenarioBuilder = scenario(uniqueName)
       .feed(data)
-      .exec(httpRequest)
+      .exec(session => {
+        session.set(forceKey, getProperty(forceKey, "false"))
+      })
+      .exec(httpRequest.body(ElFileBody(body)).asJson)
 
   setUp(
     test.inject(
diff --git a/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java b/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
index 713fd4d..16dfb9b 100644
--- a/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
+++ b/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
@@ -18,5 +18,4 @@
   public static final String LASTID_COOKIE = "gerrit.last_openid";
 
   public static final String URL_LAUNCHPAD = "https://login.launchpad.net/+openid";
-  public static final String URL_YAHOO = "https://me.yahoo.com";
 }
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 9ea4fe6..7cbfebd 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfoDifference;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
@@ -32,6 +33,7 @@
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -260,6 +262,46 @@
         EnumSet.complementOf(EnumSet.of(ListChangesOption.CHECK, ListChangesOption.SKIP_DIFFSTAT)));
   }
 
+  default ChangeInfoDifference metaDiff(
+      @Nullable String oldMetaRevId, @Nullable String newMetaRevId) throws RestApiException {
+    return metaDiff(
+        oldMetaRevId,
+        newMetaRevId,
+        EnumSet.noneOf(ListChangesOption.class),
+        ImmutableListMultimap.of());
+  }
+
+  default ChangeInfoDifference metaDiff(
+      @Nullable String oldMetaRevId, @Nullable String newMetaRevId, ListChangesOption... options)
+      throws RestApiException {
+    return metaDiff(oldMetaRevId, newMetaRevId, Arrays.asList(options));
+  }
+
+  default ChangeInfoDifference metaDiff(
+      @Nullable String oldMetaRevId,
+      @Nullable String newMetaRevId,
+      Collection<ListChangesOption> options)
+      throws RestApiException {
+    return metaDiff(
+        oldMetaRevId,
+        newMetaRevId,
+        Sets.newEnumSet(options, ListChangesOption.class),
+        ImmutableListMultimap.of());
+  }
+
+  /**
+   * Gets the diff between a change's metadata with the two given refs.
+   *
+   * @param oldMetaRevId the SHA-1 of the 'before' metadata diffed against {@code newMetaRevId}
+   * @param newMetaRevId the SHA-1 of the 'after' metadata diffed against {@code oldMetaRevId}
+   */
+  ChangeInfoDifference metaDiff(
+      @Nullable String oldMetaRevId,
+      @Nullable String newMetaRevId,
+      EnumSet<ListChangesOption> options,
+      ImmutableListMultimap<String, String> pluginOptions)
+      throws RestApiException;
+
   /** {@link #get(ListChangesOption...)} with no options included. */
   default ChangeInfo info() throws RestApiException {
     return get(EnumSet.noneOf(ListChangesOption.class));
@@ -628,6 +670,16 @@
     }
 
     @Override
+    public ChangeInfoDifference metaDiff(
+        @Nullable String oldMetaRevId,
+        @Nullable String newMetaRevId,
+        EnumSet<ListChangesOption> options,
+        ImmutableListMultimap<String, String> pluginOptions)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void setMessage(CommitMessageInput in) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index b5f40ce..b771255 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -91,7 +91,7 @@
    */
   public Boolean containsGitConflicts;
 
-  public int _number;
+  public Integer _number;
 
   public AccountInfo owner;
 
diff --git a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 283cd50..0b6008c 100644
--- a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -59,9 +59,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final ImmutableMap<String, String> ALL_PROVIDERS =
-      ImmutableMap.of(
-          "launchpad", OpenIdUrls.URL_LAUNCHPAD,
-          "yahoo", OpenIdUrls.URL_YAHOO);
+      ImmutableMap.of("launchpad", OpenIdUrls.URL_LAUNCHPAD);
 
   private final ImmutableSet<String> suggestProviders;
   private final Provider<String> urlProvider;
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index fdd9591..0b340b8 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfoDifference;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.CommitMessageInput;
@@ -80,6 +81,7 @@
 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;
@@ -149,6 +151,7 @@
   private final ChangeIncludedIn includedIn;
   private final PostReviewers postReviewers;
   private final Provider<GetChange> getChangeProvider;
+  private final Provider<GetMetaDiff> getMetaDiffProvider;
   private final PostHashtags postHashtags;
   private final GetHashtags getHashtags;
   private final AttentionSet attentionSet;
@@ -204,6 +207,7 @@
       ChangeIncludedIn includedIn,
       PostReviewers postReviewers,
       Provider<GetChange> getChangeProvider,
+      Provider<GetMetaDiff> getMetaDiffProvider,
       PostHashtags postHashtags,
       GetHashtags getHashtags,
       AttentionSet attentionSet,
@@ -257,6 +261,7 @@
     this.includedIn = includedIn;
     this.postReviewers = postReviewers;
     this.getChangeProvider = getChangeProvider;
+    this.getMetaDiffProvider = getMetaDiffProvider;
     this.postHashtags = postHashtags;
     this.getHashtags = getHashtags;
     this.attentionSet = attentionSet;
@@ -517,6 +522,25 @@
   }
 
   @Override
+  public ChangeInfoDifference metaDiff(
+      @Nullable String oldMetaRevId,
+      @Nullable String newMetaRevId,
+      EnumSet<ListChangesOption> options,
+      ImmutableListMultimap<String, String> pluginOptions)
+      throws RestApiException {
+    try (DynamicOptions dynamicOptions = new DynamicOptions(injector, dynamicBeans)) {
+      GetMetaDiff metaDiff = getMetaDiffProvider.get();
+      metaDiff.setOldMetaRevId(oldMetaRevId);
+      metaDiff.setNewMetaRevId(newMetaRevId);
+      options.forEach(metaDiff::addOption);
+      dynamicOptionParser.parseDynamicOptions(metaDiff, pluginOptions, dynamicOptions);
+      return metaDiff.apply(change).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot retrieve metaDiff", e);
+    }
+  }
+
+  @Override
   public ChangeEditApi edit() throws RestApiException {
     return changeEditApi.create(change);
   }
diff --git a/java/com/google/gerrit/server/config/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index a9abd1e..2d0f9a5 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -289,7 +289,7 @@
    */
   public Config getProjectPluginConfigWithInheritance(
       Project.NameKey projectName, String pluginName) throws NoSuchProjectException {
-    return getPluginConfig(projectName, pluginName).getWithInheritance(false);
+    return getPluginConfig(projectName, pluginName).getWithInheritance(/* merge= */ false);
   }
 
   /**
@@ -311,7 +311,7 @@
    */
   public Config getProjectPluginConfigWithInheritance(
       ProjectState projectState, String pluginName) {
-    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance(false);
+    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance(/* merge= */ false);
   }
 
   /**
@@ -336,7 +336,7 @@
    */
   public Config getProjectPluginConfigWithMergedInheritance(
       Project.NameKey projectName, String pluginName) throws NoSuchProjectException {
-    return getPluginConfig(projectName, pluginName).getWithInheritance(true);
+    return getPluginConfig(projectName, pluginName).getWithInheritance(/* merge= */ true);
   }
 
   /**
@@ -359,7 +359,7 @@
    */
   public Config getProjectPluginConfigWithMergedInheritance(
       ProjectState projectState, String pluginName) {
-    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance(true);
+    return projectState.getConfig(pluginName + EXTENSION).getWithInheritance(/* merge= */ true);
   }
 
   private ProjectLevelConfig getPluginConfig(Project.NameKey projectName, String pluginName)
diff --git a/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java b/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java
index ffe9fa8..7f9de0d 100644
--- a/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java
+++ b/java/com/google/gerrit/server/notedb/MissingMetaObjectException.java
@@ -16,6 +16,8 @@
 
 /** Separate exception type to throw if requested meta SHA1 is not available. */
 public class MissingMetaObjectException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
   MissingMetaObjectException(String msg) {
     super(msg);
   }
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 07b01c4..885459a 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -296,15 +296,15 @@
               try {
                 PatchScript patchScript = getPatchScriptWithOldDiffCache(git, aId, bId);
                 if (areEqualPatchscripts(patchScript, expected)) {
-                  metrics.diffs.increment(metrics.MATCH);
+                  metrics.diffs.increment(Metrics.MATCH);
                 } else {
-                  metrics.diffs.increment(metrics.MISMATCH);
+                  metrics.diffs.increment(Metrics.MISMATCH);
                   logger.atWarning().atMostEvery(10, TimeUnit.SECONDS).log(
                       "Mismatching diff for change %s, old commit ID: %s, new commit ID: %s, file name: %s.",
                       changeId.toString(), aId, bId, fileName);
                 }
               } catch (PatchListNotAvailableException | IOException e) {
-                metrics.diffs.increment(metrics.ERROR);
+                metrics.diffs.increment(Metrics.ERROR);
                 logger.atSevere().atMostEvery(10, TimeUnit.SECONDS).log(
                     String.format(
                             "Error computing new diff for change %s, old commit ID: %s, new commit ID: %s.\n",
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index 8163b19..395312f 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -93,7 +93,7 @@
         persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
             .maximumWeight(10 << 20)
             .weigher(FileDiffWeigher.class)
-            .version(3)
+            .version(4)
             .keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
             .valueSerializer(FileDiffOutput.Serializer.INSTANCE)
             .loader(FileDiffLoader.class);
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
index a01d447..e1af81d 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
@@ -209,9 +209,6 @@
     private static final FieldDescriptor NEW_MODE_DESCRIPTOR =
         GitFileDiffProto.getDescriptor().findFieldByNumber(8);
 
-    private static final FieldDescriptor CHANGE_TYPE_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByNumber(9);
-
     private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR =
         GitFileDiffProto.getDescriptor().findFieldByNumber(10);
 
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 5092e12..10aa9cd 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -148,11 +148,11 @@
     // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
     // we have to investigate separately (deferred tags) then perform a reachability check starting
     // from all visible branches (refs/heads/*).
-    Result initialRefFilter = filterRefs(new ArrayList<>(refs), repo, opts);
+    Result initialRefFilter = filterRefs(new ArrayList<>(refs), opts);
     List<Ref> visibleRefs = initialRefFilter.visibleRefs();
     if (!initialRefFilter.deferredTags().isEmpty()) {
       try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
-        Result allVisibleBranches = filterRefs(getTaggableRefs(repo), repo, opts);
+        Result allVisibleBranches = filterRefs(getTaggableRefs(repo), opts);
         checkState(
             allVisibleBranches.deferredTags().isEmpty(),
             "unexpected tags found when filtering refs/heads/* "
@@ -186,8 +186,7 @@
    * separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
    * compute will be returned as part of {@link Result#visibleRefs()}.
    */
-  Result filterRefs(List<Ref> refs, Repository repo, RefFilterOptions opts)
-      throws PermissionBackendException {
+  Result filterRefs(List<Ref> refs, RefFilterOptions opts) throws PermissionBackendException {
     logger.atFinest().log("Filter refs (refs = %s)", refs);
 
     // TODO(hiesel): Remove when optimization is done.
diff --git a/java/com/google/gerrit/server/project/ProjectLevelConfig.java b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
index d82a318..8256198 100644
--- a/java/com/google/gerrit/server/project/ProjectLevelConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectLevelConfig.java
@@ -88,7 +88,7 @@
   }
 
   public Config getWithInheritance() {
-    return getWithInheritance(false);
+    return getWithInheritance(/* merge= */ false);
   }
 
   /**
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 4b813df..cc73e9a 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -36,9 +36,11 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.VoteResource;
@@ -58,6 +60,7 @@
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.HashMap;
@@ -79,6 +82,8 @@
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
   private final MessageIdGenerator messageIdGenerator;
+  private final AddToAttentionSetOp.Factory attentionSetOpfactory;
+  private final Provider<CurrentUser> currentUserProvider;
 
   @Inject
   DeleteVote(
@@ -92,7 +97,9 @@
       NotifyResolver notifyResolver,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache,
-      MessageIdGenerator messageIdGenerator) {
+      MessageIdGenerator messageIdGenerator,
+      AddToAttentionSetOp.Factory attentionSetOpFactory,
+      Provider<CurrentUser> currentUserProvider) {
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -104,6 +111,8 @@
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
     this.messageIdGenerator = messageIdGenerator;
+    this.attentionSetOpfactory = attentionSetOpFactory;
+    this.currentUserProvider = currentUserProvider;
   }
 
   @Override
@@ -140,6 +149,14 @@
               r.getReviewerUser().state(),
               rsrc.getLabel(),
               input));
+      if (!r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
+        bu.addOp(
+            change.getId(),
+            attentionSetOpfactory.create(
+                r.getReviewerUser().getAccountId(),
+                /* reason= */ "Their vote was deleted",
+                /* notify= */ false));
+      }
       bu.execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index 2f1c61e..c51bb91 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -62,6 +62,10 @@
   @Option(name = "--meta", usage = "NoteDb meta SHA1")
   String metaRevId = "";
 
+  public void setMetaRevId(String metaRevId) {
+    this.metaRevId = metaRevId == null ? "" : metaRevId;
+  }
+
   @Option(name = "-O", usage = "Output option flags, in hex")
   void setOptionFlagsHex(String hex) {
     options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
diff --git a/java/com/google/gerrit/server/restapi/change/GetMetaDiff.java b/java/com/google/gerrit/server/restapi/change/GetMetaDiff.java
new file mode 100644
index 0000000..af23ba7
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/GetMetaDiff.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ListOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfoDiffer;
+import com.google.gerrit.extensions.common.ChangeInfoDifference;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+/** Gets the diff for a change at two NoteDb meta SHA-1s. */
+public class GetMetaDiff
+    implements RestReadView<ChangeResource>,
+        DynamicOptions.BeanReceiver,
+        DynamicOptions.BeanProvider {
+
+  private final EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
+  private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
+
+  private final Provider<GetChange> getChangeProvider;
+  private final GitRepositoryManager repoManager;
+
+  @Option(name = "-o", usage = "Output options")
+  public void addOption(ListChangesOption o) {
+    options.add(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    options.addAll(ListOption.fromBits(ListChangesOption.class, Integer.parseInt(hex, 16)));
+  }
+
+  @Option(name = "--old", usage = "old NoteDb meta SHA-1")
+  String oldMetaRevId = "";
+
+  public void setOldMetaRevId(@Nullable String oldMetaRevId) {
+    this.oldMetaRevId = oldMetaRevId == null ? "" : oldMetaRevId;
+  }
+
+  @Option(name = "--meta", usage = "new NoteDb meta SHA-1")
+  String metaRevId = "";
+
+  public void setNewMetaRevId(@Nullable String metaRevId) {
+    this.metaRevId = metaRevId == null ? "" : metaRevId;
+  }
+
+  @Inject
+  GetMetaDiff(Provider<GetChange> getChangeProvider, GitRepositoryManager repoManager) {
+    this.getChangeProvider = getChangeProvider;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
+    dynamicBeans.put(plugin, dynamicBean);
+  }
+
+  @Override
+  public DynamicBean getDynamicBean(String plugin) {
+    return dynamicBeans.get(plugin);
+  }
+
+  @Override
+  public Response<ChangeInfoDifference> apply(ChangeResource resource)
+      throws BadRequestException, PreconditionFailedException, IOException {
+    return Response.ok(
+        ChangeInfoDiffer.getDifference(getOldChangeInfo(resource), getNewChangeInfo(resource)));
+  }
+
+  private ChangeInfo getOldChangeInfo(ChangeResource resource)
+      throws BadRequestException, IOException, PreconditionFailedException {
+    GetChange getChange = createGetChange();
+    getChange.setMetaRevId(getOldMetaRevId(resource));
+    ChangeInfo oldChangeInfo;
+    try {
+      oldChangeInfo = getChange.apply(resource).value();
+    } catch (PreconditionFailedException e) {
+      oldChangeInfo = new ChangeInfo();
+    }
+    return oldChangeInfo;
+  }
+
+  private String getOldMetaRevId(ChangeResource resource)
+      throws IOException, BadRequestException, PreconditionFailedException {
+    if (!oldMetaRevId.isEmpty()) {
+      return oldMetaRevId;
+    }
+    String newMetaRevId = getNewMetaRevId(resource);
+    try (Repository repo = repoManager.openRepository(resource.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      ObjectId resourceId = ObjectId.fromString(newMetaRevId);
+      RevCommit commit = rw.parseCommit(resourceId);
+      return commit.getParentCount() == 0
+          ? resourceId.getName()
+          : commit.getParent(0).getId().getName();
+    } catch (InvalidObjectIdException e) {
+      throw new BadRequestException("invalid meta SHA1: " + newMetaRevId, e);
+    } catch (MissingObjectException e) {
+      throw new PreconditionFailedException(e.getMessage());
+    }
+  }
+
+  private ChangeInfo getNewChangeInfo(ChangeResource resource)
+      throws BadRequestException, PreconditionFailedException, IOException {
+    GetChange getChange = createGetChange();
+    getChange.setMetaRevId(getNewMetaRevId(resource));
+    return getChange.apply(resource).value();
+  }
+
+  private String getNewMetaRevId(ChangeResource resource) throws IOException {
+    if (!metaRevId.isEmpty()) {
+      return metaRevId;
+    }
+    try (Repository repo = repoManager.openRepository(resource.getProject())) {
+      return repo.exactRef(changeMetaRef(resource.getId())).getObjectId().getName();
+    }
+  }
+
+  private GetChange createGetChange() {
+    GetChange getChange = getChangeProvider.get();
+    options.forEach(getChange::addOption);
+    dynamicBeans.forEach(getChange::setDynamicBean);
+    return getChange;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 28f4114..f87c9a1 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -82,6 +82,7 @@
 
     postOnCollection(CHANGE_KIND).to(CreateChange.class);
     get(CHANGE_KIND).to(GetChange.class);
+    get(CHANGE_KIND, "meta_diff").to(GetMetaDiff.class);
     post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class);
     get(CHANGE_KIND, "detail").to(GetDetail.class);
     get(CHANGE_KIND, "topic").to(GetTopic.class);
diff --git a/java/com/google/gerrit/testing/TestLoggingActivator.java b/java/com/google/gerrit/testing/TestLoggingActivator.java
index 6b5d8fd..b3ad862 100644
--- a/java/com/google/gerrit/testing/TestLoggingActivator.java
+++ b/java/com/google/gerrit/testing/TestLoggingActivator.java
@@ -48,11 +48,6 @@
           .put("org.openid4java.server.RealmVerifier", Level.ERROR)
           .put("org.openid4java.message.AuthSuccess", Level.ERROR)
 
-          // Silence non-critical messages from c3p0 (if used).
-          .put("com.mchange.v2.c3p0", Level.WARN)
-          .put("com.mchange.v2.resourcepool", Level.WARN)
-          .put("com.mchange.v2.sql", Level.WARN)
-
           // Silence non-critical messages from apache.http.
           .put("org.apache.http", Level.WARN)
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index d2a48be1..6555b50 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.AttentionSetUpdate.Operation;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
@@ -1758,6 +1759,40 @@
         .isEqualTo(Operation.REMOVE);
   }
 
+  @Test
+  public void deleteSelfVotesDoesNotAddToAttentionSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+    approve(r.getChangeId());
+    gApi.changes()
+        .id(r.getChangeId())
+        .current()
+        .reviewer(admin.id().toString())
+        .deleteVote(LabelId.CODE_REVIEW);
+
+    assertThat(getAttentionSetUpdates(r.getChange().getId())).isEmpty();
+  }
+
+  @Test
+  public void deleteVotesOfOthersAddThemToAttentionSet() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    requestScopeOperations.setApiUser(user.id());
+    recommend(r.getChangeId());
+
+    requestScopeOperations.setApiUser(admin.id());
+    gApi.changes()
+        .id(r.getChangeId())
+        .current()
+        .reviewer(user.id().toString())
+        .deleteVote(LabelId.CODE_REVIEW);
+
+    AttentionSetUpdate attentionSet =
+        Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user));
+    assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
+    assertThat(attentionSet).hasReasonThat().isEqualTo("Their vote was deleted");
+  }
+
   private List<AttentionSetUpdate> getAttentionSetUpdatesForUser(
       PushOneCommit.Result r, TestAccount account) {
     return getAttentionSetUpdates(r.getChange().getId()).stream()
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java
index e025c52..2cd04ed 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMetaIT.java
@@ -104,7 +104,7 @@
   protected static class PluginDefinedSimpleAttributeModule extends AbstractModule {
     static class MyMetaHash extends PluginDefinedInfo {
       String myMetaRef;
-    };
+    }
 
     static PluginDefinedInfo newMyMetaHash(ChangeData cd) {
       MyMetaHash mmh = new MyMetaHash();
@@ -125,6 +125,7 @@
   }
 
   @Test
+  @SuppressWarnings("unchecked")
   public void pluginDefinedAttribute() throws Exception {
     try (AutoCloseable ignored =
         installPlugin("my-plugin", PluginDefinedSimpleAttributeModule.class)) {
@@ -133,7 +134,6 @@
       gApi.changes().id(changeId).setMessage("before\n\n" + "Change-Id: " + result.getChangeId());
       ChangeInfo before = gApi.changes().id(changeId).get();
       gApi.changes().id(changeId).setMessage("after\n\n" + "Change-Id: " + result.getChangeId());
-      ChangeInfo after = gApi.changes().id(changeId).get();
 
       RestResponse resp =
           adminRestSession.get("/changes/" + changeId + "/?meta=" + before.metaRevId);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java b/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java
new file mode 100644
index 0000000..2cb96e8
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/GetMetaDiffIT.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfoDifference;
+import org.junit.Test;
+
+public class GetMetaDiffIT extends AbstractDaemonTest {
+
+  private static final String UNSAVED_REV_ID = "0000000000000000000000000000000000000001";
+  private static final String TOPIC = "topic";
+  private static final String HASHTAG = "hashtag";
+
+  @Test
+  public void metaDiff() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    ChangeApi chApi = gApi.changes().id(ch.getChangeId());
+    chApi.topic(TOPIC);
+    ChangeInfo oldInfo = chApi.get();
+    chApi.topic(TOPIC + "-2");
+    chApi.setHashtags(new HashtagsInput(ImmutableSet.of(HASHTAG)));
+    ChangeInfo newInfo = chApi.get();
+
+    ChangeInfoDifference difference = chApi.metaDiff(oldInfo.metaRevId, newInfo.metaRevId);
+
+    assertThat(difference.added().topic).isEqualTo(newInfo.topic);
+    assertThat(difference.added().hashtags).isNotNull();
+    assertThat(difference.added().hashtags).containsExactly(HASHTAG);
+    assertThat(difference.removed().topic).isEqualTo(oldInfo.topic);
+    assertThat(difference.removed().hashtags).isNull();
+  }
+
+  @Test
+  public void metaDiffReturnsSuccessful() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    ChangeInfo info = gApi.changes().id(ch.getChangeId()).get();
+
+    RestResponse resp =
+        adminRestSession.get("/changes/" + ch.getChangeId() + "/meta_diff/?meta=" + info.metaRevId);
+
+    resp.assertOK();
+  }
+
+  @Test
+  public void metaDiffUnreachableNewSha1() throws Exception {
+    PushOneCommit.Result ch1 = createChange();
+    PushOneCommit.Result ch2 = createChange();
+
+    ChangeInfo info2 = gApi.changes().id(ch2.getChangeId()).get();
+
+    RestResponse resp =
+        adminRestSession.get(
+            "/changes/" + ch1.getChangeId() + "/meta_diff/?meta=" + info2.metaRevId);
+
+    resp.assertStatus(412);
+  }
+
+  @Test
+  public void metaDiffInvalidNewSha1() throws Exception {
+    PushOneCommit.Result ch = createChange();
+
+    RestResponse resp =
+        adminRestSession.get("/changes/" + ch.getChangeId() + "/meta_diff/?meta=invalid");
+
+    resp.assertBadRequest();
+  }
+
+  @Test
+  public void metaDiffInvalidOldSha1() throws Exception {
+    PushOneCommit.Result ch = createChange();
+
+    RestResponse resp =
+        adminRestSession.get("/changes/" + ch.getChangeId() + "/meta_diff/?old=invalid");
+
+    resp.assertBadRequest();
+  }
+
+  @Test
+  public void metaDiffWithNewSha1NotInRepo() throws Exception {
+    PushOneCommit.Result ch = createChange();
+
+    RestResponse resp =
+        adminRestSession.get("/changes/" + ch.getChangeId() + "/meta_diff/?meta=" + UNSAVED_REV_ID);
+
+    resp.assertStatus(412);
+  }
+
+  @Test
+  public void metaDiffUnreachableOldSha1UsesDefault() throws Exception {
+    PushOneCommit.Result ch1 = createChange();
+    PushOneCommit.Result ch2 = createChange();
+    gApi.changes().id(ch1.getChangeId()).topic("intermediate-topic");
+    gApi.changes().id(ch1.getChangeId()).topic(TOPIC);
+    ChangeInfo info1 = gApi.changes().id(ch1.getChangeId()).get();
+    ChangeInfo info2 = gApi.changes().id(ch2.getChangeId()).get();
+
+    ChangeInfoDifference difference =
+        gApi.changes().id(ch1.getChangeId()).metaDiff(info2.metaRevId, info1.metaRevId);
+
+    assertThat(difference.added().topic).isEqualTo(TOPIC);
+    assertThat(difference.removed().topic).isNull();
+  }
+
+  @Test
+  public void metaDiffWithOldSha1NotInRepoUsesDefault() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    gApi.changes().id(ch.getChangeId()).topic("intermediate-topic");
+    gApi.changes().id(ch.getChangeId()).topic(TOPIC);
+    ChangeInfo info = gApi.changes().id(ch.getChangeId()).get();
+
+    ChangeInfoDifference difference =
+        gApi.changes().id(ch.getChangeId()).metaDiff(UNSAVED_REV_ID, info.metaRevId);
+
+    assertThat(difference.added().topic).isEqualTo(TOPIC);
+    assertThat(difference.removed().topic).isNull();
+  }
+
+  @Test
+  public void metaDiffNoOldMetaGivenUsesPatchSetBeforeNew() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    ChangeApi chApi = gApi.changes().id(ch.getChangeId());
+    chApi.topic(TOPIC);
+    ChangeInfo newInfo = chApi.get();
+    chApi.topic(TOPIC + "2");
+
+    ChangeInfoDifference difference = chApi.metaDiff(null, newInfo.metaRevId);
+
+    assertThat(difference.added().topic).isEqualTo(TOPIC);
+    assertThat(difference.removed().topic).isNull();
+  }
+
+  @Test
+  public void metaDiffNoNewMetaGivenUsesCurrentPatchSet() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    ChangeApi chApi = gApi.changes().id(ch.getChangeId());
+    ChangeInfo oldInfo = chApi.get();
+    chApi.topic(TOPIC);
+
+    ChangeInfoDifference difference = chApi.metaDiff(oldInfo.metaRevId, null);
+
+    assertThat(difference.added().topic).isEqualTo(TOPIC);
+    assertThat(difference.removed().topic).isNull();
+  }
+
+  @Test
+  public void metaDiffWithoutOptionDoesNotIncludeExtraInformation() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    ChangeApi chApi = gApi.changes().id(ch.getChangeId());
+    ChangeInfo oldInfo = chApi.get();
+    amendChange(ch.getChangeId());
+    ChangeInfo newInfo = chApi.get();
+
+    ChangeInfoDifference difference = chApi.metaDiff(oldInfo.metaRevId, newInfo.metaRevId);
+
+    assertThat(difference.added().currentRevision).isNull();
+    assertThat(difference.removed().currentRevision).isNull();
+  }
+
+  @Test
+  public void metaDiffWithOptionIncludesExtraInformation() throws Exception {
+    PushOneCommit.Result ch = createChange();
+    ChangeApi chApi = gApi.changes().id(ch.getChangeId());
+    ChangeInfo oldInfo = chApi.get(ListChangesOption.CURRENT_REVISION);
+    amendChange(ch.getChangeId());
+    ChangeInfo newInfo = chApi.get(ListChangesOption.CURRENT_REVISION);
+
+    ChangeInfoDifference difference =
+        chApi.metaDiff(
+            oldInfo.metaRevId,
+            newInfo.metaRevId,
+            ImmutableSet.of(ListChangesOption.CURRENT_REVISION));
+
+    assertThat(newInfo.currentRevision).isNotNull();
+    assertThat(oldInfo.currentRevision).isNotNull();
+    assertThat(difference.added().currentRevision).isEqualTo(newInfo.currentRevision);
+    assertThat(difference.removed().currentRevision).isEqualTo(oldInfo.currentRevision);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index ffdbd8e..83d2256 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -175,7 +175,7 @@
     expectedCfg.setString("s3", null, "k5", "childValue3");
     expectedCfg.setString("s3", "ss", "k6", "childValue4");
 
-    assertThat(state.getConfig(configName).getWithInheritance(true).toText())
+    assertThat(state.getConfig(configName).getWithInheritance(/* merge= */ true).toText())
         .isEqualTo(expectedCfg.toText());
 
     assertThat(state.getConfig(configName).get().toText()).isEqualTo(cfg.toText());
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index a41d63b..09b0438 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -37,6 +37,7 @@
     ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(new ChangeInfo(), new ChangeInfo());
 
     // Spot check a few fields, including collections and maps.
+    assertThat(diff.added()._number).isNull();
     assertThat(diff.added().branch).isNull();
     assertThat(diff.added().project).isNull();
     assertThat(diff.added().currentRevision).isNull();
@@ -44,6 +45,7 @@
     assertThat(diff.added().messages).isNull();
     assertThat(diff.added().reviewers).isNull();
     assertThat(diff.added().hashtags).isNull();
+    assertThat(diff.removed()._number).isNull();
     assertThat(diff.removed().branch).isNull();
     assertThat(diff.removed().project).isNull();
     assertThat(diff.removed().currentRevision).isNull();
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index b97d9f2..cbeb59d 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -705,6 +706,23 @@
   }
 
   @Test
+  public void byProjectWithHidden() throws Exception {
+    TestRepository<Repo> hiddenProject = createProject("hiddenProject");
+    insert(hiddenProject, newChange(hiddenProject));
+    projectOperations
+        .project(Project.nameKey("hiddenProject"))
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    TestRepository<Repo> visibleProject = createProject("visibleProject");
+    Change visibleChange = insert(visibleProject, newChange(visibleProject));
+    assertQuery("project:visibleProject", visibleChange);
+    assertQuery("project:hiddenProject");
+    assertQuery("project:visibleProject OR project:hiddenProject", visibleChange);
+  }
+
+  @Test
   public void byParentOf() throws Exception {
     TestRepository<Repo> repo1 = createProject("repo1");
     RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
diff --git a/package.json b/package.json
index fc4161b..7f4ac44 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
   "version": "3.1.0-SNAPSHOT",
   "description": "Gerrit Code Review",
   "dependencies": {
-    "@bazel/rollup": "^3.2.0",
-    "@bazel/terser": "^3.2.0",
-    "@bazel/typescript": "^3.2.0"
+    "@bazel/rollup": "^3.2.3",
+    "@bazel/terser": "^3.2.3",
+    "@bazel/typescript": "^3.2.3"
   },
   "devDependencies": {
     "@typescript-eslint/eslint-plugin": "^4.11.0",
diff --git a/plugins/BUILD b/plugins/BUILD
index 943471a..353283b 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -58,6 +58,7 @@
     "//java/com/google/gerrit/util/logging",
     "//lib/antlr:java-runtime",
     "//lib/auto:auto-value-annotations",
+    "//lib/auto:auto-value-gson",
     "//lib/commons:compress",
     "//lib/commons:dbcp",
     "//lib/commons:lang",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 3cd520b..30c774f 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 3cd520b1521ff7c558d0cd95274628a3a20de30a
+Subproject commit 30c774f30c1709f71efc250a195dd6fb50c7503b
diff --git a/plugins/delete-project b/plugins/delete-project
index bfe159d..549de03 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit bfe159d3007db0f07e967473b53f679ba8f432df
+Subproject commit 549de033d60b13aaeef45ce5c4bf42be39506268
diff --git a/plugins/replication b/plugins/replication
index 839bbc5..93e61dc 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 839bbc509216fd4386c226697d732f96f2cb794c
+Subproject commit 93e61dc64debe42eab454e6c268f9c4ee22a78bc
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 42876d9..4ae3c68 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -171,7 +171,7 @@
   method: HttpMethod.POST,
 };
 
-function isQuckApproveAction(
+function isQuickApproveAction(
   action: UIActionInfo
 ): action is QuickApproveUIActionInfo {
   return (action as QuickApproveUIActionInfo).key === QUICK_APPROVE_ACTION.key;
@@ -943,9 +943,10 @@
       const status = this._getLabelStatus(labelInfo);
       if (status === LabelStatus.NEED) {
         if (result) {
-          // More than one label is missing, so it's unclear which to quick
-          // approve, return null;
-          return null;
+          // More than one label is missing, so check if Code Review can be
+          // given
+          result = null;
+          break;
         }
         result = label;
       } else if (
@@ -999,7 +1000,7 @@
       throw new Error('_topLevelSecondaryActions must be set');
     }
     this._topLevelSecondaryActions = this._topLevelSecondaryActions.filter(
-      sa => !isQuckApproveAction(sa)
+      sa => !isQuickApproveAction(sa)
     );
     this._hideQuickApproveAction = true;
   }
@@ -1271,7 +1272,7 @@
         this._showActionDialog(this.$.confirmAbandonDialog);
         break;
       case QUICK_APPROVE_ACTION.key: {
-        const action = this._allActionValues.find(isQuckApproveAction);
+        const action = this._allActionValues.find(isQuickApproveAction);
         if (!action) {
           return;
         }
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index 8324fdd..c193e60 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -1629,23 +1629,52 @@
         assert.deepEqual(payload.labels, {foo: 1});
       });
 
-      test('not added when multiple labels are required', () => {
+      test('not added when multiple labels are required without code review',
+          () => {
+            element.change = {
+              current_revision: 'abc1234',
+              labels: {
+                foo: {values: {}},
+                bar: {values: {}},
+              },
+              permitted_labels: {
+                foo: [' 0', '+1'],
+                bar: [' 0', '+1', '+2'],
+              },
+            };
+            flush();
+            const approveButton =
+            element.shadowRoot
+                .querySelector('gr-button[data-action-key=\'review\']');
+            assert.isNull(approveButton);
+          });
+
+      test('code review shown with multiple missing approval', () => {
         element.change = {
           current_revision: 'abc1234',
           labels: {
-            foo: {values: {}},
-            bar: {values: {}},
+            'foo': {values: {}},
+            'bar': {values: {}},
+            'Code-Review': {
+              approved: {},
+              values: {
+                ' 0': '',
+                '+1': '',
+                '+2': '',
+              },
+            },
           },
           permitted_labels: {
-            foo: [' 0', '+1'],
-            bar: [' 0', '+1', '+2'],
+            'foo': [' 0', '+1'],
+            'bar': [' 0', '+1', '+2'],
+            'Code-Review': [' 0', '+1', '+2'],
           },
         };
         flush();
         const approveButton =
             element.shadowRoot
                 .querySelector('gr-button[data-action-key=\'review\']');
-        assert.isNull(approveButton);
+        assert.isOk(approveButton);
       });
 
       test('button label for missing approval', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index aae8bb6..94324f3d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -470,7 +470,6 @@
                     html`<gr-avatar
                       .account="${account}"
                       image-size="32"
-                      aria-label="Account avatar"
                     ></gr-avatar>`
                 )}
                 ${countUnresolvedComments} unresolved</gr-summary-chip
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index ce5b246..eb4053a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -55,7 +55,7 @@
   project?: RepoName;
 
   @property({type: Object})
-  _query?: (_text?: string) => Promise<AutocompleteSuggestion[]>;
+  _query: (input: string) => Promise<AutocompleteSuggestion[]>;
 
   get keyBindings() {
     return {
@@ -67,7 +67,7 @@
 
   constructor() {
     super();
-    this._query = () => this._getProjectBranchesSuggestions();
+    this._query = (text: string) => this._getProjectBranchesSuggestions(text);
   }
 
   _handleConfirmTap(e: Event) {
@@ -93,10 +93,9 @@
   }
 
   _getProjectBranchesSuggestions(
-    input?: string
+    input: string
   ): Promise<AutocompleteSuggestion[]> {
     if (!this.project) return Promise.reject(new Error('Missing project'));
-    if (!input) return Promise.reject(new Error('Missing input'));
     if (input.startsWith('refs/heads/')) {
       input = input.substring('refs/heads/'.length);
     }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
index db00f6b..36a2ad3 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
@@ -64,5 +64,12 @@
       done();
     });
   });
+
+  test('_getProjectBranchesSuggestions input empty string', done => {
+    element._getProjectBranchesSuggestions('').then(branches => {
+      assert.equal(branches.length, 0);
+      done();
+    });
+  });
 });
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index a0bea33..a9f8860 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -197,13 +197,11 @@
   _computeExpandedClass(filesExpanded: FilesExpandedState) {
     const classes = [];
     if (filesExpanded === FilesExpandedState.ALL) {
-      classes.push('expanded');
-    }
-    if (
-      filesExpanded === FilesExpandedState.SOME ||
-      filesExpanded === FilesExpandedState.ALL
-    ) {
       classes.push('openFile');
+      classes.push('allExpanded');
+    } else if (filesExpanded === FilesExpandedState.SOME) {
+      classes.push('openFile');
+      classes.push('someExpanded');
     }
     return classes.join(' ');
   }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index af72b67..e0ee812 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -82,17 +82,18 @@
       justify-content: flex-end;
     }
     #collapseBtn,
-    .expanded #expandBtn,
+    .allExpanded #expandBtn,
     .fileViewActions {
       display: none;
     }
-    .expanded #expandBtn {
-      display: none;
+    .someExpanded #expandBtn {
+      margin-right: 8px;
     }
     gr-linked-chip {
       --linked-chip-text-color: var(--primary-text-color);
     }
-    .expanded #collapseBtn,
+    .someExpanded #collapseBtn,
+    .allExpanded #collapseBtn,
     .openFile .fileViewActions {
       align-items: center;
       display: flex;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index f877527..a9349fb 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -161,7 +161,7 @@
     assert.isTrue(element._expandAllDiffs.called);
   });
 
-  test('collapseAllDiffs called when expand button clicked', () => {
+  test('collapseAllDiffs called when collapse button clicked', () => {
     element.shownFileCount = 1;
     flush();
     sinon.stub(element, '_collapseAllDiffs');
@@ -203,20 +203,29 @@
   });
 
   test('expand/collapse buttons are toggled correctly', () => {
+    // Only the expand button should be visible in the initial state when
+    // NO files are expanded.
     element.shownFileCount = 10;
     flush();
     const expandBtn = element.shadowRoot.querySelector('#expandBtn');
     const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
     assert.notEqual(getComputedStyle(expandBtn).display, 'none');
     assert.equal(getComputedStyle(collapseBtn).display, 'none');
+
+    // Both expand and collapse buttons should be visible when SOME files are
+    // expanded.
     element.filesExpanded = FilesExpandedState.SOME;
     flush();
     assert.notEqual(getComputedStyle(expandBtn).display, 'none');
-    assert.equal(getComputedStyle(collapseBtn).display, 'none');
+    assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
+
+    // Only the collapse button should be visible when ALL files are expanded.
     element.filesExpanded = FilesExpandedState.ALL;
     flush();
     assert.equal(getComputedStyle(expandBtn).display, 'none');
     assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
+
+    // Only the expand button should be visible when NO files are expanded.
     element.filesExpanded = FilesExpandedState.NONE;
     flush();
     assert.notEqual(getComputedStyle(expandBtn).display, 'none');
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
index ec1235e..c75b8c4 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list-experimental/gr-related-changes-list-experimental.ts
@@ -718,16 +718,15 @@
 
     let button: TemplateResult | typeof nothing = nothing;
     if (collapsible) {
-      if (this.showAll) {
-        button = html`<gr-button link="" @click="${this.toggle}"
-          >Show less<iron-icon icon="gr-icons:expand-less"></iron-icon
-        ></gr-button>`;
-      } else {
-        button = html`<gr-button link="" @click="${this.toggle}"
-          >Show all (${this.length})
-          <iron-icon icon="gr-icons:expand-more"></iron-icon
-        ></gr-button>`;
+      let buttonText = 'Show less';
+      let buttonIcon = 'expand-less';
+      if (!this.showAll) {
+        buttonText = `Show all (${this.length})`;
+        buttonIcon = 'expand-more';
       }
+      button = html`<gr-button link="" @click="${this.toggle}"
+        >${buttonText}<iron-icon icon="gr-icons:${buttonIcon}"></iron-icon
+      ></gr-button>`;
     }
 
     return html`<div class="container">${title}${button}</div>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 382e17c..f6bd04e 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -128,7 +128,7 @@
             on-click="_handleAllComments"
             checked="[[_showAllComments(_draftsOnly, unresolvedOnly)]]"
           />
-          <label for="all">
+          <label for="allRadio">
             All ([[_countAllThreads(threads)]])
           </label>
       </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index 4a294ae..d12fa92 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -245,6 +245,7 @@
       this._createTrailingWhitespaceLayer(),
       this._createIntralineLayer(),
       this._createTabIndicatorLayer(),
+      this._createSpecialCharacterIndicatorLayer(),
       this.$.rangeLayer,
       this.$.coverageLayerLeft,
       this.$.coverageLayerRight,
@@ -488,6 +489,31 @@
     };
   }
 
+  _createSpecialCharacterIndicatorLayer(): DiffLayer {
+    return {
+      annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
+        // Find and annotate the locations of soft hyphen.
+        const split = line.text.split('\u00AD'); // \u00AD soft hyphen
+        if (!split || split.length < 2) {
+          return;
+        }
+        for (let i = 0, pos = 0; i < split.length - 1; i++) {
+          // Skip forward by the length of the content
+          pos += split[i].length;
+
+          GrAnnotation.annotateElement(
+            contentEl,
+            pos,
+            1,
+            'style-scope gr-diff special-char-indicator'
+          );
+
+          pos++;
+        }
+      },
+    };
+  }
+
   _createTrailingWhitespaceLayer(): DiffLayer {
     const show = () => {
       return this._showTrailingWhitespace;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 4e34cf6..77fe299 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -36,7 +36,9 @@
 import {classMap} from 'lit-html/directives/class-map';
 import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
 
-import {Dimensions, fitToFrame, FrameConstrainer, Rect} from './util';
+import {Dimensions, fitToFrame, FrameConstrainer, Point, Rect} from './util';
+
+const DRAG_DEAD_ZONE_PIXELS = 5;
 
 /**
  * This components allows the user to rapidly switch between two given images
@@ -95,6 +97,14 @@
     2,
   ];
 
+  @internalProperty() protected grabbing = false;
+
+  private ownsMouseDown = false;
+
+  private centerOnDown: Point = {x: 0, y: 0};
+
+  private pointerOnDown: Point = {x: 0, y: 0};
+
   private readonly frameConstrainer = new FrameConstrainer();
 
   private readonly resizeObserver = new ResizeObserver(
@@ -380,11 +390,17 @@
             base: this.baseSelected,
             revision: !this.baseSelected,
           })}"
-          style="${styleMap(this.zoomedImageStyle)}"
+          style="${styleMap({
+            ...this.zoomedImageStyle,
+            cursor: this.grabbing ? 'grabbing' : 'pointer',
+          })}"
           .scale="${this.scale}"
           .frameRect="${this.magnifierFrame}"
-          @click="${this.toggleImage}"
+          @mousedown="${this.mousedownMagnifier}"
+          @mouseup="${this.mouseupMagnifier}"
           @mousemove="${this.mousemoveMagnifier}"
+          @mouseleave="${this.mouseleaveMagnifier}"
+          @dragstart="${this.dragstartMagnifier}"
         >
           ${sourceImage}
         </gr-zoomed-image>
@@ -453,8 +469,53 @@
     this.followMouse = !this.followMouse;
   }
 
+  mousedownMagnifier(event: MouseEvent) {
+    if (event.buttons === 1) {
+      this.ownsMouseDown = true;
+      this.centerOnDown = this.frameConstrainer.getCenter();
+      this.pointerOnDown = {
+        x: event.clientX,
+        y: event.clientY,
+      };
+    }
+  }
+
+  mouseupMagnifier(event: MouseEvent) {
+    const offsetX = event.clientX - this.pointerOnDown.x;
+    const offsetY = event.clientY - this.pointerOnDown.y;
+    const distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
+    // Consider very short drags as clicks. These tend to happen more often on
+    // external mice.
+    if (this.ownsMouseDown && distance < DRAG_DEAD_ZONE_PIXELS) {
+      this.toggleImage();
+    }
+    this.grabbing = false;
+    this.ownsMouseDown = false;
+  }
+
   mousemoveMagnifier(event: MouseEvent) {
-    if (!this.followMouse) return;
+    if (event.buttons === 1 && this.ownsMouseDown) {
+      this.handleMagnifierDrag(event);
+      return;
+    }
+    if (this.followMouse) {
+      this.handleFollowMouse(event);
+      return;
+    }
+  }
+
+  private handleMagnifierDrag(event: MouseEvent) {
+    this.grabbing = true;
+    const offsetX = event.clientX - this.pointerOnDown.x;
+    const offsetY = event.clientY - this.pointerOnDown.y;
+    this.frameConstrainer.requestCenter({
+      x: this.centerOnDown.x - offsetX / this.scale,
+      y: this.centerOnDown.y - offsetY / this.scale,
+    });
+    this.updateFrames();
+  }
+
+  private handleFollowMouse(event: MouseEvent) {
     const rect = this.imageArea!.getBoundingClientRect();
     const offsetX = event.clientX - rect.left;
     const offsetY = event.clientY - rect.top;
@@ -467,6 +528,15 @@
     this.updateFrames();
   }
 
+  mouseleaveMagnifier() {
+    this.grabbing = false;
+    this.ownsMouseDown = false;
+  }
+
+  dragstartMagnifier(event: DragEvent) {
+    event.preventDefault();
+  }
+
   onOverviewCenterUpdated(event: CustomEvent) {
     this.frameConstrainer.requestCenter({
       x: event.detail.x as number,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
index 621d28c..55f83d6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -184,6 +184,7 @@
   }
 
   clickOverview(event: MouseEvent) {
+    if (event.buttons !== 1) return;
     event.preventDefault();
 
     this.updateOverlaySize();
@@ -197,6 +198,7 @@
   }
 
   grabFrame(event: MouseEvent) {
+    if (event.buttons !== 1) return;
     event.preventDefault();
     // Do not bubble up into clickOverview().
     event.stopPropagation();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index f97fcfb..5a3cdcd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -253,7 +253,7 @@
   @property({type: Boolean})
   showNewlineWarningRight = false;
 
-  @property({type: String})
+  @property({type: String, observer: '_useNewImageDiffUiObserver'})
   useNewImageDiffUi = false;
 
   @property({
@@ -708,6 +708,10 @@
     this._prefsChanged(this.prefs);
   }
 
+  _useNewImageDiffUiObserver() {
+    this._prefsChanged(this.prefs);
+  }
+
   _prefsChanged(prefs?: DiffPreferencesInfo) {
     if (!prefs) return;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index 884d279..3aa4db9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -423,6 +423,15 @@
       content: '\\00BB';
       position: absolute;
     }
+    .special-char-indicator {
+      /* spacing so elements don't collide */
+      padding-right: var(--spacing-m);
+    }
+    .special-char-indicator:before {
+      color: var(--diff-tab-indicator-color);
+      content: '•';
+      position: absolute;
+    }
     /* Is defined after other background-colors, such that this
          rule wins in case of same specificity. */
     .trailing-whitespace,
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 463fab9..2d3289d 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -45,7 +45,7 @@
 installPolymerResin(safeTypesBridge);
 
 @customElement('gr-app')
-export class GrApp extends PolymerElement {
+class GrApp extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/gr-app_test.js b/polygerrit-ui/app/elements/gr-app_test.js
new file mode 100644
index 0000000..8178c89
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_test.js
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * 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.
+ */
+
+import '../test/common-test-setup-karma.js';
+import './gr-app.js';
+import {appContext} from '../services/app-context.js';
+import {GerritNav} from './core/gr-navigation/gr-navigation.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi} from '../test/test-utils.js';
+
+const basicFixture = fixtureFromTemplate(html`<gr-app id="app"></gr-app>`);
+
+suite('gr-app tests', () => {
+  let element;
+  let configStub;
+
+  setup(done => {
+    sinon.stub(appContext.reportingService, 'appStarted');
+    stub('gr-account-dropdown', '_getTopContent');
+    stub('gr-router', 'start');
+    stubRestApi('getAccount').returns(Promise.resolve({}));
+    stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
+    configStub = stubRestApi('getConfig').returns(Promise.resolve({
+      plugin: {},
+      auth: {
+        auth_type: undefined,
+      },
+    }));
+    stubRestApi('getPreferences').returns(Promise.resolve({my: []}));
+    stubRestApi('getVersion').returns(Promise.resolve(42));
+    stubRestApi('probePath').returns(Promise.resolve(42));
+
+    element = basicFixture.instantiate();
+    flush(done);
+  });
+
+  const appElement = () => element.$['app-element'];
+
+  test('reporting', () => {
+    assert.isTrue(appElement().reporting.appStarted.calledOnce);
+  });
+
+  test('reporting called before router start', () => {
+    const element = appElement();
+    const appStartedStub = element.reporting.appStarted;
+    const routerStartStub = element.$.router.start;
+    sinon.assert.callOrder(appStartedStub, routerStartStub);
+  });
+
+  test('passes config to gr-plugin-host', () =>
+    configStub.lastCall.returnValue.then(config => {
+      assert.deepEqual(appElement().$.plugins.config, config);
+    })
+  );
+
+  test('_paramsChanged sets search page', () => {
+    appElement()._paramsChanged({base: {view: GerritNav.View.CHANGE}});
+    assert.notOk(appElement()._lastSearchPage);
+    appElement()._paramsChanged({base: {view: GerritNav.View.SEARCH}});
+    assert.ok(appElement()._lastSearchPage);
+  });
+});
+
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
deleted file mode 100644
index 3583a6a..0000000
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * @license
- * 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.
- */
-
-import '../test/common-test-setup-karma';
-import {GrApp} from './gr-app';
-import {appContext} from '../services/app-context';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-import {queryAndAssert} from '../test/test-utils';
-import {createServerInfo} from '../test/test-data-generators';
-import {GrAppElement} from './gr-app-element';
-import {GrPluginHost} from './plugins/gr-plugin-host/gr-plugin-host';
-import {GerritView} from '../services/router/router-model';
-import {
-  AppElementChangeViewParams,
-  AppElementSearchParam,
-} from './gr-app-types';
-import {GrRouter} from './core/gr-router/gr-router';
-import {ReportingService} from '../services/gr-reporting/gr-reporting';
-
-const basicFixture = fixtureFromTemplate(html`<gr-app id="app"></gr-app>`);
-
-suite('gr-app tests', () => {
-  let element: GrApp;
-  let appStartedStub: sinon.SinonStubbedMember<ReportingService['appStarted']>;
-  let routerStartStub: sinon.SinonStubbedMember<GrRouter['start']>;
-
-  setup(done => {
-    appStartedStub = sinon.stub(appContext.reportingService, 'appStarted');
-    routerStartStub = stub('gr-router', 'start');
-    stub('gr-account-dropdown', '_getTopContent');
-
-    element = basicFixture.instantiate() as GrApp;
-    flush(done);
-  });
-
-  const appElement = () =>
-    queryAndAssert<GrAppElement>(element, '#app-element');
-
-  test('reporting', () => {
-    assert.isTrue(appStartedStub.calledOnce);
-  });
-
-  test('reporting called before router start', () => {
-    sinon.assert.callOrder(appStartedStub, routerStartStub);
-  });
-
-  test('passes config to gr-plugin-host', () => {
-    assert.deepEqual(
-      queryAndAssert<GrPluginHost>(appElement(), 'gr-plugin-host').config,
-      createServerInfo()
-    );
-  });
-
-  test('_paramsChanged sets search page', () => {
-    appElement()._paramsChanged({
-      path: '',
-      value: undefined,
-      base: {view: GerritView.CHANGE} as AppElementChangeViewParams,
-    });
-    assert.notOk(appElement()._lastSearchPage);
-    appElement()._paramsChanged({
-      path: '',
-      value: undefined,
-      base: {view: GerritView.SEARCH} as AppElementSearchParam,
-    });
-    assert.ok(appElement()._lastSearchPage);
-  });
-});
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index 651eac4..ac493a2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -20,7 +20,7 @@
 import {ServerInfo} from '../../../types/common';
 
 @customElement('gr-plugin-host')
-export class GrPluginHost extends PolymerElement {
+class GrPluginHost extends PolymerElement {
   @property({type: Object, observer: '_configChanged'})
   config?: ServerInfo;
 
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index f877771..01cee4c 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -174,7 +174,7 @@
       --header-text-color: var(--primary-text-color);
 
       /* diff colors */
-      --dark-add-highlight-color: #133820;
+      --dark-add-highlight-color: var(--green-tonal); 
       --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
       --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
       --dark-remove-highlight-color: #62110f;
@@ -187,7 +187,7 @@
       --diff-selection-background-color: #3a71d8;
       --diff-tab-indicator-color: var(--deemphasized-text-color);
       --diff-trailing-whitespace-indicator: #ff9ad2;
-      --light-add-highlight-color: #0f401f;
+      --light-add-highlight-color: #182b1f;
       --light-rebased-add-highlight-color: #487165;
       --diff-moved-in-background: #1d4042;
       --diff-moved-out-background: #230e34;
diff --git a/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
index 4923143..efd760f 100644
--- a/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
+++ b/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -75,11 +75,6 @@
           <a href="?id=https://login.launchpad.net/%2Bopenid" id="id_launchpad">Sign in with a Launchpad ID</a>
         </div>
 
-        <div id="provider_yahoo">
-          <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
-          <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
-        </div>
-
         <div style="margin-top: 25px;">
           <h2>What is OpenID?</h2>
           <p>OpenID provides secure single-sign-on, without revealing your passwords to this website.</p>
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 2652612..11717fb 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -172,5 +172,5 @@
 
   // Load gr-app.js after <gr-app ...> tag because gr-router expects that
   // <gr-app ...> already exists in the document when script is executed.
-  <script type="module" src="{$staticResourcePath}/elements/gr-app.js" crossorigin="anonymous"></script>{\n}
+  <script src="{$staticResourcePath}/elements/gr-app.js" crossorigin="anonymous"></script>{\n}
 {/template}
diff --git a/resources/log4j.properties b/resources/log4j.properties
index 39246b3..2898cfc 100644
--- a/resources/log4j.properties
+++ b/resources/log4j.properties
@@ -41,11 +41,5 @@
 log4j.logger.org.openid4java.server.RealmVerifier=ERROR
 log4j.logger.org.openid4java.message.AuthSuccess=ERROR
 
-# Silence non-critical messages from c3p0 (if used).
-#
-log4j.logger.com.mchange.v2.c3p0=WARN
-log4j.logger.com.mchange.v2.resourcepool=WARN
-log4j.logger.com.mchange.v2.sql=WARN
-
 # Silence non-critical messages from apache.http
 log4j.logger.org.apache.http=WARN
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index bbb1432..facb1ce 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -455,22 +455,8 @@
     if not plugin_name:
         plugin_name = name
 
-    html_plugin = app.endswith(".html")
     srcs = srcs if app in srcs else srcs + [app]
-
-    if html_plugin:
-        # Combines all .js and .html files into foo_combined.js and foo_combined.html
-        _bundle_rule(
-            name = name + "_combined",
-            app = app,
-            srcs = srcs,
-            deps = deps,
-            pkg = native.package_name(),
-            **kwargs
-        )
-        js_srcs = [name + "_combined.js"]
-    else:
-        js_srcs = srcs
+    js_srcs = srcs
 
     native.filegroup(
         name = name + "-src-fg",
@@ -483,25 +469,6 @@
         src = name + "-src-fg",
     )
 
-    if html_plugin:
-        native.genrule(
-            name = name + "_rename_html",
-            srcs = [name + "_combined.html"],
-            outs = [plugin_name + ".html"],
-            cmd = "sed 's/<script src=\"" + name + "_combined.js\"/<script src=\"" + plugin_name + ".js\"/g' $(SRCS) > $(OUTS)",
-            output_to_bindir = True,
-        )
-    else:
-        # For polymer 3 migration, we will only have js plugins, in case server side
-        # is still asking for *.html, we still want to create a html placeholder just to load the js
-        # TODO(taoalpha): this should be cleaned up once polymer 3 plugins are the only ones gerrit supports
-        native.genrule(
-            name = name + "_rename_html",
-            outs = [plugin_name + ".html"],
-            cmd = "echo \"<script src='" + plugin_name + ".js'></script>\" > $(OUTS)",
-            output_to_bindir = True,
-        )
-
     native.genrule(
         name = name + "_rename_js",
         srcs = [name + ".min"],
@@ -510,7 +477,7 @@
         output_to_bindir = True,
     )
 
-    static_files = [plugin_name + ".js", plugin_name + ".html"]
+    static_files = [plugin_name + ".js"]
 
     if assets:
         nested, direct = [], []
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 718f8d0..3705407 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index a415f24..ba35ca2 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 5e58fdd..b7954c7 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 3b3a055..118cf39 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.4.0-SNAPSHOT</version>
+  <version>3.5.0-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 9acbd07..bfc5191 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -3,8 +3,8 @@
   "description": "Gerrit Build Tools",
   "browser": false,
   "dependencies": {
-    "@bazel/rollup": "^3.2.0",
-    "@bazel/typescript": "^3.2.0",
+    "@bazel/rollup": "^3.2.3",
+    "@bazel/typescript": "^3.2.3",
     "@types/node": "^10.17.12",
     "@types/parse5": "^4.0.0",
     "@types/parse5-html-rewriting-stream": "^5.1.2",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 45a0c89..767f285 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -492,15 +492,15 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
-"@bazel/rollup@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.2.0.tgz#4241a5767e12e57b01279a539af2537c2d01924a"
-  integrity sha512-Wkw6L+hor/+FzpDswri7IlWAbKyShnUZRx59fG06+qqVhpNaS3V3lnZqVytMlLLT4oSP8YSIzoXC5GkXgLI2/Q==
+"@bazel/rollup@^3.2.3":
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.2.3.tgz#eb6b17f89da5d38b035a6d59a99000f2d5ada667"
+  integrity sha512-7PmJqekk92OaiwwEN3dYsp5qbZX8cqr0WkFUiaM3FBsNYVoN/rdh+VxQJWTuDGLjWiHxLwE95YwVHcvwerSrCg==
 
-"@bazel/typescript@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.2.0.tgz#299bd173fe04f98407ab9be4f654662c1c28470e"
-  integrity sha512-RKdy9ThbcUAqZR3AJK7AR/nxbJqdHi7pPayIGUSMIpxVkeTxVRQpf1aGe2H02HdZ9fR/uk1xXhO/Ff9TLvTgHQ==
+"@bazel/typescript@^3.2.3":
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.2.3.tgz#6e40bdb7c5294e588bac3b7d1269e58b98a1856c"
+  integrity sha512-Q1Yin/AYdh9yrkSJo3H6nVn6mMaohr5syjLd0Df0w7WI4zerdJTxrY5nhoWZwO/S1rPj8/MedDwZudCqPDeDMA==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
diff --git a/version.bzl b/version.bzl
index 066d07e..f2e0d0c 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.4.0-SNAPSHOT"
+GERRIT_VERSION = "3.5.0-SNAPSHOT"
diff --git a/yarn.lock b/yarn.lock
index a424d79..adb731c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -485,20 +485,20 @@
     lodash "^4.17.11"
     to-fast-properties "^2.0.0"
 
-"@bazel/rollup@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.2.0.tgz#4241a5767e12e57b01279a539af2537c2d01924a"
-  integrity sha512-Wkw6L+hor/+FzpDswri7IlWAbKyShnUZRx59fG06+qqVhpNaS3V3lnZqVytMlLLT4oSP8YSIzoXC5GkXgLI2/Q==
+"@bazel/rollup@^3.2.3":
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.2.3.tgz#eb6b17f89da5d38b035a6d59a99000f2d5ada667"
+  integrity sha512-7PmJqekk92OaiwwEN3dYsp5qbZX8cqr0WkFUiaM3FBsNYVoN/rdh+VxQJWTuDGLjWiHxLwE95YwVHcvwerSrCg==
 
-"@bazel/terser@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.2.0.tgz#e53ad32733a0b231323b9eb55ebc2a3c65b10223"
-  integrity sha512-/yq4gST3t1mETkP6NjC05yEyIIL//4mbfLI56hE3CC/mm/xJ6UeooFVpUdlJREQEDRAdNWoiMesQ1ZtgpNPzFg==
+"@bazel/terser@^3.2.3":
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.2.3.tgz#18849cb01f6a9fd840955547b0a9dbf63f02c720"
+  integrity sha512-SzX3ytgt8+i36sODPO9ju0LbjKXB8w9zi00S+XNManQTRilNrr/NODTYbBzKrjuebA/o52NMpvRGigRd1Z2RGg==
 
-"@bazel/typescript@^3.2.0":
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.2.0.tgz#299bd173fe04f98407ab9be4f654662c1c28470e"
-  integrity sha512-RKdy9ThbcUAqZR3AJK7AR/nxbJqdHi7pPayIGUSMIpxVkeTxVRQpf1aGe2H02HdZ9fR/uk1xXhO/Ff9TLvTgHQ==
+"@bazel/typescript@^3.2.3":
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.2.3.tgz#6e40bdb7c5294e588bac3b7d1269e58b98a1856c"
+  integrity sha512-Q1Yin/AYdh9yrkSJo3H6nVn6mMaohr5syjLd0Df0w7WI4zerdJTxrY5nhoWZwO/S1rPj8/MedDwZudCqPDeDMA==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"