Merge "restapi/accounts: clarify permission related behaviors"
diff --git a/BUILD b/BUILD
index c48b3b9..084d383 100644
--- a/BUILD
+++ b/BUILD
@@ -56,19 +56,22 @@
 API_DEPS = [
     "//java/com/google/gerrit/acceptance:framework_deploy.jar",
     "//java/com/google/gerrit/acceptance:libframework-lib-src.jar",
-    "//java/com/google/gerrit/acceptance:framework-javadoc",
     "//java/com/google/gerrit/extensions:extension-api_deploy.jar",
     "//java/com/google/gerrit/extensions:libapi-src.jar",
-    "//java/com/google/gerrit/extensions:extension-api-javadoc",
     "//plugins:plugin-api_deploy.jar",
     "//plugins:plugin-api-sources_deploy.jar",
+]
+
+API_JAVADOC_DEPS = [
+    "//java/com/google/gerrit/acceptance:framework-javadoc",
+    "//java/com/google/gerrit/extensions:extension-api-javadoc",
     "//plugins:plugin-api-javadoc",
 ]
 
 genrule2(
     name = "api",
     testonly = True,
-    srcs = API_DEPS,
+    srcs = API_DEPS + API_JAVADOC_DEPS,
     outs = ["api.zip"],
     cmd = " && ".join([
         "cp $(SRCS) $$TMP",
@@ -76,3 +79,15 @@
         "zip -qr $$ROOT/$@ .",
     ]),
 )
+
+genrule2(
+    name = "api-skip-javadoc",
+    testonly = True,
+    srcs = API_DEPS,
+    outs = ["api-skip-javadoc.zip"],
+    cmd = " && ".join([
+        "cp $(SRCS) $$TMP",
+        "cd $$TMP",
+        "zip -qr $$ROOT/$@ .",
+    ]),
+)
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 313092f..b1f9a31 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3394,6 +3394,49 @@
 config is backwards compatible with what the default was before the config
 was added.
 
+[[event.comment-added.publishPatchSetLevelComment]][event.comment-added.publishPatchSetLevelComment::
++
+Add patch set level comment as event comment. Without this option, patch set
+level comment will not be included in the event comment attribute. Given that
+currently patch set level, file and robot comments are not exposed in the
+`comment-added` event type, those comments will be lost. One particular use
+case is to re-trigger CI build from the change screen by adding a comment with
+specific content, e.g.: `recheck`. Jenkins Gerrit Trigger plugin and Zuul CI
+depend on this feature to trigger change verification.
++
+By default, true.
+
+[[experiments]]
+=== Section experiments
+
+This section covers experimental new features. Gerrit's frontend uses experiments
+to research new behavior. Once the research is done, the experimental feature
+either stays and the experimentation flag gets removed, or the feature as a whole
+gets removed
+
+[[experiments.enabled]]experiments.enabled::
++
+List of experiments that are currently enabled. The release notes contain currently
+available experiments.
++
+We will not remove experiments in stable patch releases. They are likely to be
+removed in the next stable version.
+
+----
+[experiments]
+  enabled = ExperimentKey
+----
+
+[[experiments.disabled]]experiments.disabled::
++
+List of experiments that are currently disabled. The release notes contain currently
+available experiments. This list disables experiments with the given key that are
+either enabled by default or explicitly in the config.
+
+----
+[experiments]
+  disabled = ExperimentKey
+----
 
 [[ldap]]
 === Section ldap
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index c770733..159e2fc 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1007,17 +1007,9 @@
 Runtime exceptions generated by the implementors of ChangePluginDefinedInfoFactory
 are encapsulated in PluginDefinedInfo objects which are part of SSH/REST query output.
 
-==== ChangeAttributeFactory
-
-Alternatively, there is also `ChangeAttributeFactory` which takes in one single
-`ChangeData` at a time. `ChangePluginDefinedInfoFactory` should be preferred
-over this as it handles many changes at once which also decreases the round-trip
-time for queries resulting in performance increase for bulk queries.
-
-Implementors of the `ChangePluginDefinedInfoFactory` and `ChangeAttributeFactory`
-interfaces should check whether they need to contribute to the
-link:#change-etag-computation[change ETag computation] to prevent callers using
-ETags from potentially seeing outdated plugin attributes.
+Implementors of the `ChangePluginDefinedInfoFactory` interface should check whether
+they need to contribute to the link:#change-etag-computation[change ETag computation]
+to prevent callers using ETags from potentially seeing outdated plugin attributes.
 
 [[simple-configuration]]
 == Simple Configuration in `gerrit.config`
@@ -2255,7 +2247,7 @@
 
   @Override
   public WebLinkInfo getPatchSetWebLink(String projectName, String commit,
-   String subject, String branchName) {
+   String commitMessage, String branchName) {
     return new WebLinkInfo(name,
         imageUrl,
         String.format(placeHolderUrlProjectCommit, project, commit),
@@ -2470,6 +2462,32 @@
 Macros that start with `\` such as `\@KEEP@` will render as `@KEEP@`
 even if there is an expansion for `KEEP` in the future.
 
+Documentation should typically contain the following content:
+
+[width="100%",options="header"]
+|===================================================
+|File                                           | Content
+|`README.md`                                    | Home page of the plugin when browsing its source code on Git
+|`LICENSE`                                      | Open-source license
+|`resources/Documentation/about.md`             | Overview of the plugin and its purpose
+|`resources/Documentation/config.md`            | Plugin configuration settings and sample configs
+|`resources/Documentation/build.md`             | How to build the plugin
+|`resources/Documentation/cmd-<command>.md`     | SSH commands
+|`resources/Documentation/rest-api-<api>.md`    | REST API
+|`resources/Documentation/servlet-<servlet>.md` | HTTP Servlets
+|===================================================
+
+The documentation under resources/Documentation may contain macro that
+will be included and expanded by Gerrit once the plugin is loaded.
+
+The files in the root directory are not included in the plugin package
+and must not have any macro for expansion. It may also collect
+additional information that would make the plugin more discoverable, such as
+a more user-friendly description of its use-cases.
+
+The documentation can also include images that can help understanding more
+visually how the plugin can interact with the other Gerrit components.
+
 [[auto-index]]
 === Automatic Index
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index cdf576c..3f0c751 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -15,9 +15,9 @@
 --
 
 The change input link:#change-input[ChangeInput] entity must be provided in the
-request body. Allowed values for `branch` are refs/heads/*,
-refs/meta/dashboards/*, refs/meta/config or HEAD. The request would fail with
-`400 Bad Request` if any other `branch` is specified.
+request body. It is not allowed to create changes on refs/tags/* or Gerrit
+internal refs such as refs/changes/*, refs/meta/external-ids/*, refs/users/*,
+etc.. and the request would fail with `400 Bad Request` in this case.
 
 To create a change the calling user must be allowed to
 link:access-control.html#category_push_review[upload to code review].
@@ -152,6 +152,12 @@
 The `S` or `start` query parameter can be supplied to skip a number
 of changes from the list.
 
+Administrators can use the `skip-visibility` query parameter to skip visibility filtering.
+This can be used to ensure that no changes are missed e.g. when querying for changes which
+need to be reindexed. Without this parameter query results the user has no permission to read
+are filtered out. REST requests with the skip-visibility option are rejected when the current
+user doesn't have the ADMINISTRATE_SERVER capability.
+
 Clients are allowed to specify more than one query by setting the `q`
 parameter multiple times. In this case the result is an array of
 arrays, one per query in the same order the queries were given in.
@@ -2046,7 +2052,7 @@
 comments for each path are sorted by patch set number. Each comment has
 the `patch_set` and `author` fields set.
 
-If the `enable_context` request parameter is set to true, the comment entries
+If the `enable-context` request parameter is set to true, the comment entries
 will contain a list of link:#context-line[ContextLine] containing the lines of
 the source file where the comment was written.
 
@@ -6693,7 +6699,7 @@
 this comment applies.
 |`context_lines` |optional|
 A list of link:#context-line[ContextLine] containing the lines of the source
-file where the comment was written. Available only if the "enable_context"
+file where the comment was written. Available only if the "enable-context"
 parameter (see link:#list-change-comments[List Change Comments]) is set.
 
 |===========================
diff --git a/WORKSPACE b/WORKSPACE
index fd36850..b204e8d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -45,9 +45,11 @@
 load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig")
 
 # Creates a default toolchain config for RBE.
-# Use this as is if you are using the rbe_ubuntu16_04 container,
-# otherwise refer to RBE docs.
-rbe_autoconfig(name = "rbe_default")
+rbe_autoconfig(
+    name = "rbe_jdk11",
+    java_home = "/usr/lib/jvm/11.29.3-ca-jdk11.0.2/reduced",
+    use_checked_in_confs = "Force",
+)
 
 http_archive(
     name = "com_google_protobuf",
@@ -795,26 +797,30 @@
     sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd",
 )
 
-# Note that all of the following org.apache.httpcomponents have newer versions,
-# but 4.4.1 is the only version that is available for all of them.
-HTTPCOMP_VERS = "4.4.1"
+# Base the following org.apache.httpcomponents versions on what
+# elasticsearch-rest-client explicitly depends on, except for
+# commons-codec (non-http) which is not necessary yet. Note that
+# below httpcore version(s) differs from the HTTPCOMP_VERS range,
+# upstream: that specific dependency has no HTTPCOMP_VERS version
+# equivalent currently.
+HTTPCOMP_VERS = "4.5.2"
 
 maven_jar(
     name = "fluent-hc",
     artifact = "org.apache.httpcomponents:fluent-hc:" + HTTPCOMP_VERS,
-    sha1 = "96fb842b68a44cc640c661186828b60590c71261",
+    sha1 = "7bfdfa49de6d720ad3c8cedb6a5238eec564dfed",
 )
 
 maven_jar(
     name = "httpclient",
     artifact = "org.apache.httpcomponents:httpclient:" + HTTPCOMP_VERS,
-    sha1 = "016d0bc512222f1253ee6b64d389c84e22f697f0",
+    sha1 = "733db77aa8d9b2d68015189df76ab06304406e50",
 )
 
 maven_jar(
     name = "httpcore",
-    artifact = "org.apache.httpcomponents:httpcore:" + HTTPCOMP_VERS,
-    sha1 = "f5aa318bda4c6c8d688c9d00b90681dcd82ce636",
+    artifact = "org.apache.httpcomponents:httpcore:4.4.4",
+    sha1 = "b31526a230871fbe285fbcbe2813f9c0839ae9b0",
 )
 
 # Test-only dependencies below.
diff --git a/contrib/reindex/.flake8 b/contrib/reindex/.flake8
new file mode 100644
index 0000000..151557f
--- /dev/null
+++ b/contrib/reindex/.flake8
@@ -0,0 +1,9 @@
+[flake8]
+max-line-length=100
+ignore=
+    # E203 whitespace before ':'
+    E203,
+    # W503: Line break before binary operator
+    W503,
+    # W504: Line break after binary operator
+    W504
diff --git a/contrib/reindex/.gitignore b/contrib/reindex/.gitignore
new file mode 100644
index 0000000..fd8c78f
--- /dev/null
+++ b/contrib/reindex/.gitignore
@@ -0,0 +1 @@
+changes-to-reindex.list
diff --git a/contrib/reindex/Pipfile b/contrib/reindex/Pipfile
new file mode 100644
index 0000000..21ffd90
--- /dev/null
+++ b/contrib/reindex/Pipfile
@@ -0,0 +1,19 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pygerrit2 = "*"
+requests = "*"
+tqdm = "*"
+
+[dev-packages]
+flake8 = "*"
+black = "*"
+
+[requires]
+python_version = "3.9"
+
+[pipenv]
+allow_prereleases = true
diff --git a/contrib/reindex/Pipfile.lock b/contrib/reindex/Pipfile.lock
new file mode 100644
index 0000000..bb7cc2d
--- /dev/null
+++ b/contrib/reindex/Pipfile.lock
@@ -0,0 +1,248 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "37be5a74a22d0e084ebfe168bfdcd7bcaa87ad7b42be66b1d9fbff5e936ebe72"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.9"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "certifi": {
+            "hashes": [
+                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
+                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
+            ],
+            "version": "==2020.12.5"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
+                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==4.0.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.10"
+        },
+        "pbr": {
+            "hashes": [
+                "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
+                "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
+            ],
+            "markers": "python_version >= '2.6'",
+            "version": "==5.5.1"
+        },
+        "pygerrit2": {
+            "hashes": [
+                "sha256:d12cff5cc514dd61281d997ea86771e7f818030c3d2ef230b25bb14dae7d3f86"
+            ],
+            "index": "pypi",
+            "version": "==2.0.14"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
+                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
+            ],
+            "index": "pypi",
+            "version": "==2.25.1"
+        },
+        "tqdm": {
+            "hashes": [
+                "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
+                "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"
+            ],
+            "index": "pypi",
+            "version": "==4.54.1"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
+                "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
+            "version": "==1.26.2"
+        }
+    },
+    "develop": {
+        "appdirs": {
+            "hashes": [
+                "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
+                "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
+            ],
+            "version": "==1.4.4"
+        },
+        "black": {
+            "hashes": [
+                "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
+            ],
+            "index": "pypi",
+            "version": "==20.8b1"
+        },
+        "click": {
+            "hashes": [
+                "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
+                "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==7.1.2"
+        },
+        "flake8": {
+            "hashes": [
+                "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
+                "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
+            ],
+            "index": "pypi",
+            "version": "==3.8.4"
+        },
+        "mccabe": {
+            "hashes": [
+                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+            ],
+            "version": "==0.6.1"
+        },
+        "mypy-extensions": {
+            "hashes": [
+                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
+                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
+            ],
+            "version": "==0.4.3"
+        },
+        "pathspec": {
+            "hashes": [
+                "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
+                "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
+            ],
+            "version": "==0.8.1"
+        },
+        "pycodestyle": {
+            "hashes": [
+                "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
+                "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.6.0"
+        },
+        "pyflakes": {
+            "hashes": [
+                "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
+                "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.2.0"
+        },
+        "regex": {
+            "hashes": [
+                "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538",
+                "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4",
+                "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc",
+                "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa",
+                "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444",
+                "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1",
+                "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af",
+                "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8",
+                "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9",
+                "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88",
+                "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba",
+                "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364",
+                "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e",
+                "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7",
+                "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0",
+                "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31",
+                "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683",
+                "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee",
+                "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b",
+                "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884",
+                "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c",
+                "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e",
+                "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562",
+                "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85",
+                "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c",
+                "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6",
+                "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d",
+                "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b",
+                "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70",
+                "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b",
+                "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b",
+                "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f",
+                "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0",
+                "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5",
+                "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5",
+                "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f",
+                "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e",
+                "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512",
+                "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d",
+                "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917",
+                "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"
+            ],
+            "version": "==2020.11.13"
+        },
+        "toml": {
+            "hashes": [
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+            ],
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==0.10.2"
+        },
+        "typed-ast": {
+            "hashes": [
+                "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
+                "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
+                "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
+                "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
+                "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
+                "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
+                "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
+                "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
+                "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
+                "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
+                "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
+                "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
+                "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
+                "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
+                "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
+                "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
+                "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
+                "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
+                "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
+                "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
+                "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
+                "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
+                "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
+                "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
+                "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
+                "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
+                "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
+                "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
+                "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
+                "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
+            ],
+            "version": "==1.4.1"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
+                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
+                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
+            ],
+            "version": "==3.7.4.3"
+        }
+    }
+}
diff --git a/contrib/reindex/README.md b/contrib/reindex/README.md
new file mode 100644
index 0000000..acb9588
--- /dev/null
+++ b/contrib/reindex/README.md
@@ -0,0 +1,63 @@
+# Incremental reindexing during upgrade of large gerrit site
+
+In order to shorten the downtime needed to reindex changes during a
+Gerrit upgrade the following strategy can be used:
+
+- index preparation
+  - create a full consistent backup
+  - note down the timestamp when the backup was created (backup-time)
+  - create a complete copy of the production system from the backup
+  - upgrade this copy to the new Gerrit version
+  - online reindex this copy
+- upgrade of the production system
+  - make system unavailable so that users can't reach it anymore
+    e.g. by changing port numbers (downtime starts)
+  - take a full backup
+  - run
+
+    ``` bash
+    ./reindex.py -u gerrit-url -s backup-time
+    ```
+
+    to write the list of changes which have been created or modified
+    since the backup for the index preparation was created to a file
+    "changes-to-reindex.list"
+  - upgrade the production system to the new gerrit version skipping
+    reindexing
+  - copy the bulk of the new index from the copy system to the
+    production system
+  - run
+
+    ``` bash
+    ./reindex.py -u gerrit-url
+    ```
+
+    this reindexes all changes which have been created or modified after
+    the backup was taken reading these changes from the file
+    "changes-to-reindex.list"
+  - smoketest the system
+  - make the production system available to the users again
+    (downtime ends)
+
+## Online help
+
+For help on all available options run
+
+``` bash
+./reindex -h
+```
+
+## Python environment
+
+Prerequisites:
+
+- python 3.9
+- pipenv
+
+Install virtual python environment and run the script
+
+``` bash
+pipenv sync
+pipenv shell
+./reindex <options>
+```
diff --git a/contrib/reindex/reindex.py b/contrib/reindex/reindex.py
new file mode 100755
index 0000000..266f5ec
--- /dev/null
+++ b/contrib/reindex/reindex.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+from argparse import ArgumentParser, RawTextHelpFormatter
+from itertools import islice
+import getpass
+import logging
+import os
+
+from pygerrit2 import GerritRestAPI, HTTPBasicAuth, HTTPBasicAuthFromNetrc
+from tqdm import tqdm
+
+EPILOG = """\
+To query the list of changes which have been created or modified since the
+given timestamp and write them to a file "changes-to-reindex.list" run
+$ ./reindex.py -u gerrit-url -s timestamp
+
+To reindex the list of changes in file "changes-to-reindex.list" run
+$ ./reindex.py -u gerrit-url
+"""
+
+
+def _parse_options():
+    parser = ArgumentParser(
+        formatter_class=RawTextHelpFormatter,
+        epilog=EPILOG,
+    )
+    parser.add_argument(
+        "-u",
+        "--url",
+        dest="url",
+        help="gerrit url",
+    )
+    parser.add_argument(
+        "-s",
+        "--since",
+        dest="time",
+        help=(
+            "changes modified after the given 'TIME', inclusive. Must be in the\n"
+            "format '2006-01-02[ 15:04:05[.890][ -0700]]', omitting the time defaults\n"
+            "to 00:00:00 and omitting the timezone defaults to UTC."
+        ),
+    )
+    parser.add_argument(
+        "-f",
+        "--file",
+        default="changes-to-reindex.list",
+        dest="file",
+        help=(
+            "file path to store list of changes if --since is given,\n"
+            "otherwise file path to read list of changes from"
+        ),
+    )
+    parser.add_argument(
+        "-c",
+        "--chunk",
+        default=100,
+        dest="chunksize",
+        help="chunk size defining how many changes are reindexed per request",
+        type=int,
+    )
+    parser.add_argument(
+        "--cert",
+        dest="cert",
+        type=str,
+        help="path to file containing custom ca certificates to trust",
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        dest="verbose",
+        action="store_true",
+        help="verbose debugging output",
+    )
+    parser.add_argument(
+        "-n",
+        "--netrc",
+        default=True,
+        dest="netrc",
+        action="store_true",
+        help=(
+            "read credentials from .netrc, default to environment variables\n"
+            "USERNAME and PASSWORD, otherwise prompt for credentials interactively"
+        ),
+    )
+    return parser.parse_args()
+
+
+def _chunker(iterable, chunksize):
+    it = map(lambda s: s.strip(), iterable)
+    while True:
+        chunk = list(islice(it, chunksize))
+        if not chunk:
+            return
+        yield chunk
+
+
+class Reindexer:
+    """Class for reindexing Gerrit changes"""
+
+    def __init__(self):
+        self.options = _parse_options()
+        self._init_logger()
+        credentials = self._authenticate()
+        if self.options.cert:
+            certs = os.path.expanduser(self.options.cert)
+            self.api = GerritRestAPI(
+                url=self.options.url, auth=credentials, verify=certs
+            )
+        else:
+            self.api = GerritRestAPI(url=self.options.url, auth=credentials)
+
+    def _init_logger(self):
+        self.logger = logging.getLogger("Reindexer")
+        self.logger.setLevel(logging.DEBUG)
+        h = logging.StreamHandler()
+        if self.options.verbose:
+            h.setLevel(logging.DEBUG)
+        else:
+            h.setLevel(logging.INFO)
+        formatter = logging.Formatter("%(message)s")
+        h.setFormatter(formatter)
+        self.logger.addHandler(h)
+
+    def _authenticate(self):
+        username = password = None
+        if self.options.netrc:
+            auth = HTTPBasicAuthFromNetrc(url=self.options.url)
+            username = auth.username
+            password = auth.password
+        if not username:
+            username = os.environ.get("USERNAME")
+        if not password:
+            password = os.environ.get("PASSWORD")
+        while not username:
+            username = input("user: ")
+        while not password:
+            password = getpass.getpass("password: ")
+        auth = HTTPBasicAuth(username, password)
+        return auth
+
+    def _query(self):
+        start = 0
+        more_changes = True
+        while more_changes:
+            query = f"since:{self.options.time}&start={start}&skip-visibility"
+            for change in self.api.get(f"changes/?q={query}"):
+                more_changes = change.get("_more_changes") is not None
+                start += 1
+                yield change.get("_number")
+            break
+
+    def _query_to_file(self):
+        self.logger.debug(
+            f"writing changes since {self.options.time} to file {self.options.file}:"
+        )
+        with open(self.options.file, "w") as output:
+            for id in self._query():
+                self.logger.debug(id)
+                output.write(f"{id}\n")
+
+    def _reindex_chunk(self, chunk):
+        self.logger.debug(f"indexing {chunk}")
+        response = self.api.post(
+            "/config/server/index.changes",
+            chunk,
+        )
+        self.logger.debug(f"response: {response}")
+
+    def _reindex(self):
+        self.logger.debug(f"indexing changes from file {self.options.file}")
+        with open(self.options.file, "r") as f:
+            with tqdm(unit="changes", desc="Indexed") as pbar:
+                for chunk in _chunker(f, self.options.chunksize):
+                    self._reindex_chunk(chunk)
+                    pbar.update(len(chunk))
+
+    def execute(self):
+        if self.options.time:
+            self._query_to_file()
+        else:
+            self._reindex()
+
+
+def main():
+    reindexer = Reindexer()
+    reindexer.execute()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index a91bc49..29dc6a3 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.GetChange;
@@ -40,7 +39,6 @@
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
-import com.google.inject.Module;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -86,21 +84,6 @@
     }
   }
 
-  protected static class NullAttributeModule extends AbstractModule {
-    @Override
-    public void configure() {
-      DynamicSet.bind(binder(), ChangeAttributeFactory.class).toInstance((cd, bp, p) -> null);
-    }
-  }
-
-  protected static class SimpleAttributeModule extends AbstractModule {
-    @Override
-    public void configure() {
-      DynamicSet.bind(binder(), ChangeAttributeFactory.class)
-          .toInstance((cd, bp, p) -> new MyInfo("change " + cd.getId()));
-    }
-  }
-
   protected static class PluginDefinedSimpleAttributeModule extends AbstractModule {
     @Override
     public void configure() {
@@ -170,21 +153,6 @@
     private String opt;
   }
 
-  protected static class OptionAttributeModule extends AbstractModule {
-    @Override
-    public void configure() {
-      DynamicSet.bind(binder(), ChangeAttributeFactory.class)
-          .toInstance(
-              (cd, bp, p) -> {
-                MyOptions opts = (MyOptions) bp.getDynamicBean(p);
-                return opts != null ? new MyInfo("opt " + opts.opt) : null;
-              });
-      bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
-      bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
-      bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class);
-    }
-  }
-
   public static class BulkAttributeFactoryWithOption implements ChangePluginDefinedInfoFactory {
     protected MyOptions opts;
 
@@ -211,33 +179,6 @@
     }
   }
 
-  protected void getChangeWithNullAttribute(PluginInfoGetter getter) throws Exception {
-    Change.Id id = createChange().getChange().getId();
-    assertThat(getter.call(id)).isNull();
-
-    try (AutoCloseable ignored = installPlugin("my-plugin", NullAttributeModule.class)) {
-      assertThat(getter.call(id)).isNull();
-    }
-
-    assertThat(getter.call(id)).isNull();
-  }
-
-  protected void getChangeWithSimpleAttribute(PluginInfoGetter getter) throws Exception {
-    getChangeWithSimpleAttribute(getter, SimpleAttributeModule.class);
-  }
-
-  protected void getChangeWithSimpleAttribute(
-      PluginInfoGetter getter, Class<? extends Module> moduleClass) throws Exception {
-    Change.Id id = createChange().getChange().getId();
-    assertThat(getter.call(id)).isNull();
-
-    try (AutoCloseable ignored = installPlugin("my-plugin", moduleClass)) {
-      assertThat(getter.call(id)).containsExactly(new MyInfo("my-plugin", "change " + id));
-    }
-
-    assertThat(getter.call(id)).isNull();
-  }
-
   protected void getSingleChangeWithPluginDefinedBulkAttribute(BulkPluginInfoGetterWithId getter)
       throws Exception {
     Change.Id id = createChange().getChange().getId();
@@ -298,30 +239,6 @@
     assertThat(pluginInfos.get(changeWithInfo)).isNull();
   }
 
-  protected void getMultipleChangesWithPluginDefinedBulkAndChangeAttributes(
-      BulkPluginInfoGetter getter) throws Exception {
-    Change.Id id1 = createChange().getChange().getId();
-    Change.Id id2 = createChange().getChange().getId();
-
-    Map<Change.Id, List<PluginDefinedInfo>> pluginInfos = getter.call();
-    assertThat(pluginInfos.get(id1)).isNull();
-    assertThat(pluginInfos.get(id2)).isNull();
-
-    try (AutoCloseable ignored =
-            installPlugin("my-plugin-1", PluginDefinedSimpleAttributeModule.class);
-        AutoCloseable ignored1 = installPlugin("my-plugin-2", SimpleAttributeModule.class)) {
-      pluginInfos = getter.call();
-      assertThat(pluginInfos.get(id1)).contains(new MyInfo("my-plugin-1", "change " + id1));
-      assertThat(pluginInfos.get(id1)).contains(new MyInfo("my-plugin-2", "change " + id1));
-      assertThat(pluginInfos.get(id2)).contains(new MyInfo("my-plugin-1", "change " + id2));
-      assertThat(pluginInfos.get(id2)).contains(new MyInfo("my-plugin-2", "change " + id2));
-    }
-
-    pluginInfos = getter.call();
-    assertThat(pluginInfos.get(id1)).isNull();
-    assertThat(pluginInfos.get(id2)).isNull();
-  }
-
   protected void getMultipleChangesWithPluginDefinedBulkAttributeInSingleCall(
       BulkPluginInfoGetter getter) throws Exception {
     Change.Id id1 = createChange().getChange().getId();
@@ -345,22 +262,6 @@
     assertThat(pluginInfos.get(id2)).isNull();
   }
 
-  protected void getChangeWithOption(
-      PluginInfoGetter getterWithoutOptions, PluginInfoGetterWithOptions getterWithOptions)
-      throws Exception {
-    Change.Id id = createChange().getChange().getId();
-    assertThat(getterWithoutOptions.call(id)).isNull();
-
-    try (AutoCloseable ignored = installPlugin("my-plugin", OptionAttributeModule.class)) {
-      assertThat(getterWithoutOptions.call(id))
-          .containsExactly(new MyInfo("my-plugin", "opt null"));
-      assertThat(getterWithOptions.call(id, ImmutableListMultimap.of("my-plugin--opt", "foo")))
-          .containsExactly(new MyInfo("my-plugin", "opt foo"));
-    }
-
-    assertThat(getterWithoutOptions.call(id)).isNull();
-  }
-
   protected void getChangeWithPluginDefinedBulkAttributeOption(
       BulkPluginInfoGetterWithId getterWithoutOptions,
       BulkPluginInfoGetterWithIdAndOptions getterWithOptions)
@@ -455,11 +356,6 @@
   }
 
   @FunctionalInterface
-  protected interface PluginInfoGetter {
-    List<PluginDefinedInfo> call(Change.Id id) throws Exception;
-  }
-
-  @FunctionalInterface
   protected interface BulkPluginInfoGetter {
     Map<Change.Id, List<PluginDefinedInfo>> call() throws Exception;
   }
@@ -474,10 +370,4 @@
     Map<Change.Id, List<PluginDefinedInfo>> call(
         Change.Id id, ImmutableListMultimap<String, String> pluginOptions) throws Exception;
   }
-
-  @FunctionalInterface
-  protected interface PluginInfoGetterWithOptions {
-    List<PluginDefinedInfo> call(Change.Id id, ImmutableListMultimap<String, String> pluginOptions)
-        throws Exception;
-  }
 }
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index afd451a..4215255 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -43,8 +43,12 @@
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.api.TagCommand;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
@@ -277,6 +281,19 @@
     return this;
   }
 
+  public PushOneCommit addSymlink(String path, String target) throws Exception {
+    RevBlob blobId = testRepo.blob(target);
+    commitBuilder.edit(
+        new PathEdit(path) {
+          @Override
+          public void apply(DirCacheEntry ent) {
+            ent.setFileMode(FileMode.SYMLINK);
+            ent.setObjectId(blobId);
+          }
+        });
+    return this;
+  }
+
   public Result to(String ref) throws Exception {
     for (Map.Entry<String, String> e : files.entrySet()) {
       commitBuilder.add(e.getKey(), e.getValue());
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index f18c122..522c60a 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.entities;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.UsedAt;
 
 /** Constants and utilities for Gerrit-specific ref names. */
@@ -105,6 +106,26 @@
   /** A change starred by a user */
   public static final String REFS_STARRED_CHANGES = "refs/starred-changes/";
 
+  /**
+   * List of refs managed by Gerrit. Covers all Gerrit internal refs.
+   *
+   * <p><b>Caution</b> Any ref not in this list will be served if the user was granted a READ
+   * permission on it using Gerrit's permission model.
+   */
+  public static final ImmutableList<String> GERRIT_REFS =
+      ImmutableList.of(
+          REFS_CHANGES,
+          REFS_EXTERNAL_IDS,
+          REFS_CACHE_AUTOMERGE,
+          REFS_DRAFT_COMMENTS,
+          REFS_DELETED_GROUPS,
+          REFS_SEQUENCES,
+          REFS_GROUPS,
+          REFS_GROUPNAMES,
+          REFS_USERS,
+          REFS_STARRED_CHANGES,
+          REFS_REJECT_COMMITS);
+
   public static String fullName(String ref) {
     return (ref.startsWith(REFS) || ref.equals(HEAD)) ? ref : REFS_HEADS + ref;
   }
@@ -306,21 +327,11 @@
    * <p>Any ref for which this method evaluates to true will be served to users who have the {@code
    * ACCESS_DATABASE} capability.
    *
-   * <p><b>Caution</b>Any ref not in this list will be served if the user was granted a READ
+   * <p><b>Caution</b> Any ref not in this list will be served if the user was granted a READ
    * permission on it using Gerrit's permission model.
    */
   public static boolean isGerritRef(String ref) {
-    return ref.startsWith(REFS_CHANGES)
-        || ref.startsWith(REFS_EXTERNAL_IDS)
-        || ref.startsWith(REFS_CACHE_AUTOMERGE)
-        || ref.startsWith(REFS_DRAFT_COMMENTS)
-        || ref.startsWith(REFS_DELETED_GROUPS)
-        || ref.startsWith(REFS_SEQUENCES)
-        || ref.startsWith(REFS_GROUPS)
-        || ref.startsWith(REFS_GROUPNAMES)
-        || ref.startsWith(REFS_USERS)
-        || ref.startsWith(REFS_STARRED_CHANGES)
-        || ref.startsWith(REFS_REJECT_COMMITS);
+    return GERRIT_REFS.stream().anyMatch(internalRef -> ref.startsWith(internalRef));
   }
 
   static Integer parseShardedRefPart(String name) {
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 7ecc0a6..fd445b6 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -21,9 +21,11 @@
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /** Input passed to {@code POST /changes/[id]/revisions/[id]/review}. */
 public class ReviewInput {
@@ -117,6 +119,15 @@
     return this;
   }
 
+  public ReviewInput patchSetLevelComment(String message) {
+    Objects.requireNonNull(message);
+    CommentInput comment = new CommentInput();
+    comment.message = message;
+    // TODO(davido): Because of cyclic dependency, we cannot use here Patch.PATCHSET_LEVEL constant
+    comments = Collections.singletonMap("/PATCHSET_LEVEL", Collections.singletonList(comment));
+    return this;
+  }
+
   public ReviewInput label(String name, short value) {
     if (name == null || name.isEmpty()) {
       throw new IllegalArgumentException();
diff --git a/java/com/google/gerrit/extensions/webui/ParentWebLink.java b/java/com/google/gerrit/extensions/webui/ParentWebLink.java
index 2176e78..9f2bc6e 100644
--- a/java/com/google/gerrit/extensions/webui/ParentWebLink.java
+++ b/java/com/google/gerrit/extensions/webui/ParentWebLink.java
@@ -32,11 +32,11 @@
    *
    * @param projectName name of the project
    * @param commit commit sha1 of the parent revision
-   * @param subject first line of the commit message
+   * @param commitMessage the commit messsage of the change
    * @param branchName target branch of the change
    * @return WebLinkInfo that links to parent commit in external service, null if there should be no
    *     link.
    */
   WebLinkInfo getParentWebLink(
-      String projectName, String commit, String subject, String branchName);
+      String projectName, String commit, String commitMessage, String branchName);
 }
diff --git a/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
index b8ba5c4..0e8e28e 100644
--- a/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
+++ b/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -32,11 +32,11 @@
    *
    * @param projectName name of the project
    * @param commit commit of the patch set
-   * @param subject first line of the commit message
+   * @param commitMessage the commit message of the change
    * @param branchName target branch of the change
    * @return WebLinkInfo that links to patch set in external service, null if there should be no
    *     link.
    */
   WebLinkInfo getPatchSetWebLink(
-      String projectName, String commit, String subject, String branchName);
+      String projectName, String commit, String commitMessage, String branchName);
 }
diff --git a/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index 1eaaba3..b56f973 100644
--- a/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -73,11 +73,10 @@
 
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-
-    final String sid = webSession.get().getSessionId();
-    final CurrentUser currentUser = webSession.get().getUser();
-    final String what = "sign out";
-    final long when = TimeUtil.nowMs();
+    String sid = webSession.get().getSessionId();
+    CurrentUser currentUser = webSession.get().getUser();
+    String what = "sign out";
+    long when = TimeUtil.nowMs();
 
     try {
       doLogout(req, rsp);
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 77d02c1..46dde41 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
@@ -37,15 +38,21 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
+import org.eclipse.jgit.lib.Config;
 
 /** Helper for generating parts of {@code index.html}. */
 @UsedAt(Project.GOOGLE)
 public class IndexHtmlUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  static final ImmutableSet<String> DEFAULT_EXPERIMENTS =
+      ImmutableSet.of(
+          "UiFeature__patchset_comments", "UiFeature__patchset_choice_for_comment_links");
+
   private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
   /**
    * Returns both static and dynamic parameters of {@code index.html}. The result is to be used when
@@ -53,6 +60,7 @@
    */
   public static ImmutableMap<String, Object> templateData(
       GerritApi gerritApi,
+      Config gerritServerConfig,
       String canonicalURL,
       String cdnPath,
       String faviconPath,
@@ -66,7 +74,13 @@
                 canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
         .putAll(dynamicTemplateData(gerritApi, requestedURL));
 
-    Set<String> enabledExperiments = experimentData(urlParameterMap);
+    Set<String> enabledExperiments = new HashSet<>();
+    Arrays.stream(gerritServerConfig.getStringList("experiments", null, "enabled"))
+        .forEach(enabledExperiments::add);
+    DEFAULT_EXPERIMENTS.forEach(enabledExperiments::add);
+    Arrays.stream(gerritServerConfig.getStringList("experiments", null, "disabled"))
+        .forEach(enabledExperiments::remove);
+    experimentData(urlParameterMap).forEach(enabledExperiments::add);
     if (!enabledExperiments.isEmpty()) {
       data.put("enabledExperiments", serializeObject(GSON, enabledExperiments).toString());
     }
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index 97d2270..b2bdf7c 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -34,6 +34,7 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
 
 public class IndexServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
@@ -42,6 +43,7 @@
   @Nullable private final String cdnPath;
   @Nullable private final String faviconPath;
   private final GerritApi gerritApi;
+  private final Config gerritServerConfig;
   private final SoySauce soySauce;
   private final Function<String, SanitizedContent> urlOrdainer;
 
@@ -49,11 +51,13 @@
       @Nullable String canonicalUrl,
       @Nullable String cdnPath,
       @Nullable String faviconPath,
-      GerritApi gerritApi) {
+      GerritApi gerritApi,
+      Config gerritServerConfig) {
     this.canonicalUrl = canonicalUrl;
     this.cdnPath = cdnPath;
     this.faviconPath = faviconPath;
     this.gerritApi = gerritApi;
+    this.gerritServerConfig = gerritServerConfig;
     this.soySauce =
         SoyFileSet.builder()
             .add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
@@ -74,7 +78,14 @@
       // TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
       ImmutableMap<String, Object> templateData =
           IndexHtmlUtil.templateData(
-              gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer, requestUrl);
+              gerritApi,
+              gerritServerConfig,
+              canonicalUrl,
+              cdnPath,
+              faviconPath,
+              parameterMap,
+              urlOrdainer,
+              requestUrl);
       renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
     } catch (URISyntaxException | RestApiException e) {
       throw new IOException(e);
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 4b2c8a9..66e107b 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -225,7 +225,7 @@
       String cdnPath =
           options.useDevCdn() ? options.devCdn() : cfg.getString("gerrit", null, "cdnPath");
       String faviconPath = cfg.getString("gerrit", null, "faviconPath");
-      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi);
+      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi, cfg);
     }
 
     @Provides
diff --git a/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 326cab8..3ab409e 100644
--- a/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -43,6 +43,7 @@
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.HashSet;
@@ -153,6 +154,12 @@
     this.parserFactory = pf;
   }
 
+  /**
+   * Parses query parameters ({@code in}) into annotated option fields of {@code param}.
+   *
+   * @return true if parsing was successful. Requesting help is considered failure and returns
+   *     false.
+   */
   <T> boolean parse(
       T param,
       DynamicOptions pluginOptions,
@@ -160,6 +167,10 @@
       HttpServletRequest req,
       HttpServletResponse res)
       throws IOException {
+    if (param.getClass().getAnnotation(Singleton.class) != null) {
+      // Command-line parsing mutates the object, so we can't have options on @Singleton.
+      return true;
+    }
     CmdLineParser clp = parserFactory.create(param);
     pluginOptions.setBean(param);
     pluginOptions.startLifecycleListeners();
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index e9c0136..894757b 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -39,7 +39,6 @@
 import com.google.gerrit.server.account.externalids.ExternalIdModule;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
@@ -116,8 +115,6 @@
     bind(new TypeLiteral<List<CommentLinkInfo>>() {})
         .toProvider(CommentLinkProvider.class)
         .in(SINGLETON);
-    bind(new TypeLiteral<DynamicSet<ChangeAttributeFactory>>() {})
-        .toInstance(DynamicSet.emptySet());
     bind(new TypeLiteral<DynamicMap<RestView<CommitResource>>>() {})
         .toInstance(DynamicMap.emptyMap());
     bind(String.class)
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index c85c389..afbc74e 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -130,6 +130,14 @@
   }
 
   /**
+   * Returns all email addresses associated with this user. For {@link AnonymousUser} and other
+   * users that don't represent a person user or service account, this set will be empty.
+   */
+  public ImmutableSet<String> getEmailAddresses() {
+    return ImmutableSet.of();
+  }
+
+  /**
    * Returns all {@link ExternalId.Key}s associated with this user. For {@link AnonymousUser} and
    * other users that don't represent a person user or service account, this set will be empty.
    */
diff --git a/java/com/google/gerrit/server/query/change/ExternalUser.java b/java/com/google/gerrit/server/ExternalUser.java
similarity index 79%
rename from java/com/google/gerrit/server/query/change/ExternalUser.java
rename to java/com/google/gerrit/server/ExternalUser.java
index f853a5c..9680f3e 100644
--- a/java/com/google/gerrit/server/query/change/ExternalUser.java
+++ b/java/com/google/gerrit/server/ExternalUser.java
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.query.change;
+package com.google.gerrit.server;
 
 import static com.google.common.flogger.LazyArgs.lazy;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -38,22 +37,36 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    ExternalUser create(Collection<ExternalId.Key> externalIdKeys);
+    ExternalUser create(
+        Collection<String> emailAddresses,
+        Collection<ExternalId.Key> externalIdKeys,
+        PropertyMap propertyMap);
   }
 
   private final GroupBackend groupBackend;
+  private final ImmutableSet<String> emailAddresses;
   private final ImmutableSet<ExternalId.Key> externalIdKeys;
 
   private GroupMembership effectiveGroups;
 
   @Inject
   public ExternalUser(
-      GroupBackend groupBackend, @Assisted Collection<ExternalId.Key> externalIdKeys) {
+      GroupBackend groupBackend,
+      @Assisted Collection<String> emailAddresses,
+      @Assisted Collection<ExternalId.Key> externalIdKeys,
+      @Assisted PropertyMap propertyMap) {
+    super(propertyMap);
     this.groupBackend = groupBackend;
+    this.emailAddresses = ImmutableSet.copyOf(emailAddresses);
     this.externalIdKeys = ImmutableSet.copyOf(externalIdKeys);
   }
 
   @Override
+  public ImmutableSet<String> getEmailAddresses() {
+    return emailAddresses;
+  }
+
+  @Override
   public ImmutableSet<ExternalId.Key> getExternalIdKeys() {
     return externalIdKeys;
   }
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index ca86434..34f0eb5 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -382,6 +382,7 @@
     return false;
   }
 
+  @Override
   public ImmutableSet<String> getEmailAddresses() {
     if (!loadedAllEmails) {
       validEmails.addAll(realm.getEmailAddresses(this));
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 47ba325..e66e7f5 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -86,29 +86,29 @@
   /**
    * @param project Project name.
    * @param commit SHA1 of commit.
-   * @param subject subject of the commit.
+   * @param commitMessage the commit message of the commit.
    * @param branchName branch of the commit.
    * @return Links for patch sets.
    */
   public ImmutableList<WebLinkInfo> getPatchSetLinks(
-      Project.NameKey project, String commit, String subject, String branchName) {
+      Project.NameKey project, String commit, String commitMessage, String branchName) {
     return filterLinks(
         patchSetLinks,
-        webLink -> webLink.getPatchSetWebLink(project.get(), commit, subject, branchName));
+        webLink -> webLink.getPatchSetWebLink(project.get(), commit, commitMessage, branchName));
   }
 
   /**
    * @param project Project name.
    * @param revision SHA1 of the parent revision.
-   * @param subject subject of the parent revision.
+   * @param commitMessage the commit message of the parent revision.
    * @param branchName branch of the revision (and parent revision).
    * @return Links for patch sets.
    */
   public ImmutableList<WebLinkInfo> getParentLinks(
-      Project.NameKey project, String revision, String subject, String branchName) {
+      Project.NameKey project, String revision, String commitMessage, String branchName) {
     return filterLinks(
         parentLinks,
-        webLink -> webLink.getParentWebLink(project.get(), revision, subject, branchName));
+        webLink -> webLink.getParentWebLink(project.get(), revision, commitMessage, branchName));
   }
 
   /**
diff --git a/java/com/google/gerrit/server/change/ChangeAttributeFactory.java b/java/com/google/gerrit/server/change/ChangeAttributeFactory.java
deleted file mode 100644
index 663d7aa..0000000
--- a/java/com/google/gerrit/server/change/ChangeAttributeFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import com.google.gerrit.server.DynamicOptions.BeanProvider;
-import com.google.gerrit.server.query.change.ChangeData;
-
-/**
- * Interface for plugins to provide additional fields in {@link
- * com.google.gerrit.extensions.common.ChangeInfo ChangeInfo}.
- *
- * <p>Register a {@code ChangeAttributeFactory} in a plugin {@code Module} like this:
- *
- * <pre>
- * DynamicSet.bind(binder(), ChangeAttributeFactory.class).to(YourClass.class);
- * </pre>
- *
- * <p>See the <a
- * href="https://gerrit-review.googlesource.com/Documentation/dev-plugins.html#query_attributes">plugin
- * developer documentation for more details and examples.
- */
-@Deprecated
-public interface ChangeAttributeFactory {
-
-  /**
-   * Create a plugin-provided info field.
-   *
-   * <p>Typically, implementations will subclass {@code PluginDefinedInfo} to add additional fields.
-   *
-   * @param cd change.
-   * @param beanProvider provider of {@code DynamicBean}s, which may be used for reading options.
-   * @param plugin plugin name.
-   * @return the plugin's special change info.
-   */
-  PluginDefinedInfo create(ChangeData cd, BeanProvider beanProvider, String plugin);
-}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 6ab0c61..02da518 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -158,17 +158,12 @@
     }
 
     public ChangeJson create(Iterable<ListChangesOption> options) {
-      return factory.create(options, Optional.empty(), Optional.empty());
+      return factory.create(options, Optional.empty());
     }
 
     public ChangeJson create(
-        Iterable<ListChangesOption> options,
-        PluginDefinedAttributesFactory pluginDefinedAttributesFactory,
-        PluginDefinedInfosFactory pluginDefinedInfosFactory) {
-      return factory.create(
-          options,
-          Optional.of(pluginDefinedAttributesFactory),
-          Optional.of(pluginDefinedInfosFactory));
+        Iterable<ListChangesOption> options, PluginDefinedInfosFactory pluginDefinedInfosFactory) {
+      return factory.create(options, Optional.of(pluginDefinedInfosFactory));
     }
 
     public ChangeJson create(ListChangesOption first, ListChangesOption... rest) {
@@ -179,7 +174,6 @@
   public interface AssistedFactory {
     ChangeJson create(
         Iterable<ListChangesOption> options,
-        Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory,
         Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory);
   }
 
@@ -226,7 +220,6 @@
   private final TrackingFooters trackingFooters;
   private final Metrics metrics;
   private final RevisionJson revisionJson;
-  private final Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory;
   private final Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory;
   private final boolean includeMergeable;
   private final boolean lazyLoad;
@@ -251,7 +244,6 @@
       RevisionJson.Factory revisionJsonFactory,
       @GerritServerConfig Config cfg,
       @Assisted Iterable<ListChangesOption> options,
-      @Assisted Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory,
       @Assisted Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory) {
     this.userProvider = user;
     this.changeDataFactory = cdf;
@@ -269,7 +261,6 @@
     this.options = Sets.immutableEnumSet(options);
     this.includeMergeable = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi();
     this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
-    this.pluginDefinedAttributesFactory = pluginDefinedAttributesFactory;
     this.pluginDefinedInfosFactory = pluginDefinedInfosFactory;
 
     logger.atFine().log("options = %s", options);
@@ -611,17 +602,9 @@
     }
 
     setSubmitter(cd, out);
-    if (pluginDefinedAttributesFactory.isPresent()) {
-      out.plugins = pluginDefinedAttributesFactory.get().create(cd);
-    }
 
     if (!pluginInfos.isEmpty()) {
-      if (out.plugins == null) {
-        out.plugins = pluginInfos;
-      } else {
-        out.plugins = new ArrayList<>(out.plugins);
-        out.plugins.addAll(pluginInfos);
-      }
+      out.plugins = pluginInfos;
     }
     out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
     out.submissionId = cd.change().getSubmissionId();
diff --git a/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.java b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.java
index b474dab..db21f11 100644
--- a/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.java
+++ b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactories.java
@@ -14,55 +14,22 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.extensions.registration.Extension;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.Collection;
-import java.util.Objects;
 import java.util.stream.Stream;
 
-/** Static helpers for use by {@link PluginDefinedAttributesFactory} implementations. */
+/** Static helpers for use by {@link PluginDefinedInfosFactory} implementations. */
 public class PluginDefinedAttributesFactories {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  @Nullable
-  public static ImmutableList<PluginDefinedInfo> createAll(
-      ChangeData cd,
-      BeanProvider beanProvider,
-      Stream<Extension<ChangeAttributeFactory>> attrFactories) {
-    ImmutableList<PluginDefinedInfo> result =
-        attrFactories
-            .map(e -> tryCreate(cd, beanProvider, e.getPluginName(), e.get()))
-            .filter(Objects::nonNull)
-            .collect(toImmutableList());
-    return !result.isEmpty() ? result : null;
-  }
-
-  @Nullable
-  private static PluginDefinedInfo tryCreate(
-      ChangeData cd, BeanProvider beanProvider, String plugin, ChangeAttributeFactory attrFactory) {
-    PluginDefinedInfo pdi = null;
-    try {
-      pdi = attrFactory.create(cd, beanProvider, plugin);
-    } catch (RuntimeException ex) {
-      logger.atWarning().atMostEvery(1, MINUTES).withCause(ex).log(
-          "error populating attribute on change %s from plugin %s", cd.getId(), plugin);
-    }
-    if (pdi != null) {
-      pdi.name = plugin;
-    }
-    return pdi;
-  }
-
   public static ImmutableListMultimap<Change.Id, PluginDefinedInfo> createAll(
       Collection<ChangeData> cds,
       BeanProvider beanProvider,
diff --git a/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java b/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
deleted file mode 100644
index 08d6ce7..0000000
--- a/java/com/google/gerrit/server/change/PluginDefinedAttributesFactory.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.common.PluginDefinedInfo;
-import com.google.gerrit.server.query.change.ChangeData;
-import java.util.List;
-
-public interface PluginDefinedAttributesFactory {
-  List<PluginDefinedInfo> create(ChangeData cd);
-}
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 90caea0..c1c0cfc 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -183,7 +183,7 @@
 
     if (addLinks) {
       ImmutableList<WebLinkInfo> links =
-          webLinks.getPatchSetLinks(project, commit.name(), commit.getShortMessage(), branchName);
+          webLinks.getPatchSetLinks(project, commit.name(), commit.getFullMessage(), branchName);
       info.webLinks = links.isEmpty() ? null : links;
     }
 
@@ -194,7 +194,7 @@
       i.subject = parent.getShortMessage();
       if (addLinks) {
         ImmutableList<WebLinkInfo> parentLinks =
-            webLinks.getParentLinks(project, parent.name(), parent.getShortMessage(), branchName);
+            webLinks.getParentLinks(project, parent.name(), parent.getFullMessage(), branchName);
         i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
       }
       info.parents.add(i);
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index d131cf5..9308662 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,6 +81,7 @@
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.ExceptionHookImpl;
+import com.google.gerrit.server.ExternalUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.TraceRequestListener;
@@ -105,7 +106,6 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.change.AbandonOp;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeETagComputation;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.change.ChangeJson;
@@ -179,7 +179,6 @@
 import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
-import com.google.gerrit.server.query.change.ExternalUser;
 import com.google.gerrit.server.quota.QuotaEnforcer;
 import com.google.gerrit.server.restapi.change.OnPostReview;
 import com.google.gerrit.server.restapi.change.SuggestReviewers;
@@ -441,7 +440,6 @@
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
     DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeIsOperandFactory.class);
-    DynamicSet.setOf(binder(), ChangeAttributeFactory.class);
     DynamicSet.setOf(binder(), ChangePluginDefinedInfoFactory.class);
 
     install(new GitwebConfig.LegacyModule(cfg));
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index 3340a52..8214f03 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -328,9 +328,9 @@
 
     @Override
     public WebLinkInfo getPatchSetWebLink(
-        String projectName, String commit, String subject, String branchName) {
+        String projectName, String commit, String commitMessage, String branchName) {
       if (revision != null) {
-        // subject and branchName are not needed, hence not used.
+        // commitMessage and branchName are not needed, hence not used.
         return link(
             revision
                 .replace("project", encode(projectName))
@@ -342,9 +342,9 @@
 
     @Override
     public WebLinkInfo getParentWebLink(
-        String projectName, String commit, String subject, String branchName) {
+        String projectName, String commit, String commitMessage, String branchName) {
       // For Gitweb treat parent revision links the same as patch set links
-      return getPatchSetWebLink(projectName, commit, subject, branchName);
+      return getPatchSetWebLink(projectName, commit, commitMessage, branchName);
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index fb89240..4c90ef9 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2692,12 +2692,10 @@
 
   private void readChangesForReplace() {
     try (TraceTimer traceTimer = newTimer("readChangesForReplace")) {
-      Collection<ChangeNotes> allNotes =
-          notesFactory.createUsingIndexLookup(
-              replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList()));
-      for (ChangeNotes notes : allNotes) {
-        replaceByChange.get(notes.getChangeId()).notes = notes;
-      }
+      replaceByChange.values().stream()
+          .map(r -> r.ontoChange)
+          .map(id -> notesFactory.create(project.getNameKey(), id))
+          .forEach(notes -> replaceByChange.get(notes.getChangeId()).notes = notes);
     }
   }
 
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index dfb42ff..5d50d22 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ExternalUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StartupCheck;
 import com.google.gerrit.server.StartupException;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.change.ExternalUser;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index e8f1fe1..b816264 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -138,9 +138,8 @@
     }
 
     /**
-     * Create change notes based on a {@link com.google.gerrit.entities.Change.Id}. This requires
-     * using the Change index and should only be used when {@link
-     * com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available.
+     * Create change notes based on a {@link Change.Id}. This requires using the Change index and
+     * should only be used when {@link Project.NameKey} and the numeric change ID are not available.
      */
     public ChangeNotes createCheckedUsingIndexLookup(Change.Id changeId) {
       InternalChangeQuery query = queryProvider.get().noFields();
@@ -156,9 +155,9 @@
     }
 
     /**
-     * Create change notes based on a list of {@link com.google.gerrit.entities.Change.Id}s. This
-     * requires using the Change index and should only be used when {@link
-     * com.google.gerrit.entities.Project.NameKey} and the numeric change ID are not available.
+     * Create change notes based on a list of {@link Change.Id}s. This requires using the Change
+     * index and should only be used when {@link Project.NameKey} and the numeric change ID are not
+     * available.
      */
     public List<ChangeNotes> createUsingIndexLookup(Collection<Change.Id> changeIds) {
       List<ChangeNotes> notes = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/patch/DiffNotAvailableException.java b/java/com/google/gerrit/server/patch/DiffNotAvailableException.java
index 34e1577..e75adec 100644
--- a/java/com/google/gerrit/server/patch/DiffNotAvailableException.java
+++ b/java/com/google/gerrit/server/patch/DiffNotAvailableException.java
@@ -25,4 +25,8 @@
   public DiffNotAvailableException(Throwable cause) {
     super(cause);
   }
+
+  public DiffNotAvailableException(String message) {
+    super(message);
+  }
 }
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
index 9198666..1e88f9f 100644
--- a/java/com/google/gerrit/server/patch/DiffUtil.java
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -64,6 +64,16 @@
         || haveCommonParent(commitA, commitB);
   }
 
+  public static int stringSize(String str) {
+    if (str != null) {
+      // each character in the string occupies 2 bytes. Ignoring the fixed overhead for the string
+      // (length, offset and hash code) since they are negligible and do not affect the comparison
+      // of 2 strings.
+      return str.length() * 2;
+    }
+    return 0;
+  }
+
   private static boolean isRootOrMergeCommit(RevCommit commit) {
     return commit.getParentCount() != 1;
   }
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
index 5aa31ec..2ac3f5e 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.patch.diff;
 
+import static com.google.gerrit.server.patch.DiffUtil.stringSize;
+
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
@@ -103,14 +105,4 @@
           .build();
     }
   }
-
-  private static int stringSize(String str) {
-    if (str != null) {
-      // each character in the string occupies 2 bytes. Ignoring the fixed overhead for the string
-      // (length, offset and hash code) since they are negligible and do not
-      // affect the comparison of 2 strings
-      return str.length() * 2;
-    }
-    return 0;
-  }
 }
diff --git a/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java b/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java
new file mode 100644
index 0000000..12decc3
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/AllDiffsEvaluator.java
@@ -0,0 +1,218 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffUtil;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiff;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCache;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheKey;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * A helper class that computes the four {@link GitFileDiff}s for a list of {@link
+ * FileDiffCacheKey}s:
+ *
+ * <ul>
+ *   <li>old commit vs. new commit
+ *   <li>old parent vs. old commit
+ *   <li>new parent vs. new commit
+ *   <li>old parent vs. new parent
+ * </ul>
+ *
+ * The four {@link GitFileDiff} are stored in the entity class {@link AllFileGitDiffs}. We use these
+ * diffs to identify the edits due to rebase using the {@link EditTransformer} class.
+ */
+class AllDiffsEvaluator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final RevWalk rw;
+  private final GitFileDiffCache gitCache;
+
+  interface Factory {
+    AllDiffsEvaluator create(RevWalk rw);
+  }
+
+  @Inject
+  private AllDiffsEvaluator(GitFileDiffCache gitCache, @Assisted RevWalk rw) {
+    this.gitCache = gitCache;
+    this.rw = rw;
+  }
+
+  Map<AugmentedFileDiffCacheKey, AllFileGitDiffs> execute(
+      List<AugmentedFileDiffCacheKey> augmentedKeys) throws DiffNotAvailableException {
+    ImmutableMap.Builder<AugmentedFileDiffCacheKey, AllFileGitDiffs> keyToAllDiffs =
+        ImmutableMap.builderWithExpectedSize(augmentedKeys.size());
+
+    List<AugmentedFileDiffCacheKey> keysWithRebaseEdits =
+        augmentedKeys.stream().filter(k -> !k.ignoreRebase()).collect(Collectors.toList());
+
+    // TODO(ghareeb): as an enhancement, you can batch these calls as follows.
+    // First batch: "old commit vs. new commit" and "new parent vs. new commit"
+    // Second batch: "old parent vs. old commit" and "old parent vs. new parent"
+
+    Map<FileDiffCacheKey, GitDiffEntity> mainDiffs =
+        computeGitFileDiffs(
+            createGitKeys(
+                augmentedKeys,
+                k -> k.key().oldCommit(),
+                k -> k.key().newCommit(),
+                k -> k.key().newFilePath()));
+
+    Map<FileDiffCacheKey, GitDiffEntity> oldVsParentDiffs =
+        computeGitFileDiffs(
+            createGitKeys(
+                keysWithRebaseEdits,
+                k -> k.oldParentId().get(), // oldParent is set for keysWithRebaseEdits
+                k -> k.key().oldCommit(),
+                k -> mainDiffs.get(k.key()).gitDiff().oldPath().orElse(null)));
+
+    Map<FileDiffCacheKey, GitDiffEntity> newVsParentDiffs =
+        computeGitFileDiffs(
+            createGitKeys(
+                keysWithRebaseEdits,
+                k -> k.newParentId().get(), // newParent is set for keysWithRebaseEdits
+                k -> k.key().newCommit(),
+                k -> k.key().newFilePath()));
+
+    Map<FileDiffCacheKey, GitDiffEntity> parentsDiffs =
+        computeGitFileDiffs(
+            createGitKeys(
+                keysWithRebaseEdits,
+                k -> k.oldParentId().get(),
+                k -> k.newParentId().get(),
+                k -> {
+                  GitFileDiff newVsParDiff = newVsParentDiffs.get(k.key()).gitDiff();
+                  // TODO(ghareeb): Follow up on replacing key.newFilePath as a fallback.
+                  // If the file was added between newParent and newCommit, we actually wouldn't
+                  // need to have to determine the oldParent vs. newParent diff as nothing in
+                  // that file could be an edit due to rebase anymore. Only if the returned diff
+                  // is empty, the oldParent vs. newParent diff becomes relevant again (e.g. to
+                  // identify a file deletion which was due to rebase. Check if the structure
+                  // can be improved to make this clearer. Can we maybe even skip the diff in
+                  // the first situation described?
+                  return newVsParDiff.oldPath().orElse(k.key().newFilePath());
+                }));
+
+    for (AugmentedFileDiffCacheKey augmentedKey : augmentedKeys) {
+      FileDiffCacheKey key = augmentedKey.key();
+      AllFileGitDiffs.Builder builder =
+          AllFileGitDiffs.builder().augmentedKey(augmentedKey).mainDiff(mainDiffs.get(key));
+
+      if (augmentedKey.ignoreRebase()) {
+        keyToAllDiffs.put(augmentedKey, builder.build());
+        continue;
+      }
+
+      if (oldVsParentDiffs.containsKey(key) && !oldVsParentDiffs.get(key).gitDiff().isEmpty()) {
+        builder.oldVsParentDiff(Optional.of(oldVsParentDiffs.get(key)));
+      }
+
+      if (newVsParentDiffs.containsKey(key) && !newVsParentDiffs.get(key).gitDiff().isEmpty()) {
+        builder.newVsParentDiff(Optional.of(newVsParentDiffs.get(key)));
+      }
+
+      if (parentsDiffs.containsKey(key) && !parentsDiffs.get(key).gitDiff().isEmpty()) {
+        builder.parentVsParentDiff(Optional.of(parentsDiffs.get(key)));
+      }
+
+      keyToAllDiffs.put(augmentedKey, builder.build());
+    }
+    return keyToAllDiffs.build();
+  }
+
+  /**
+   * Computes the git diff for the git keys of the input map {@code keys} parameter. The computation
+   * uses the underlying {@link GitFileDiffCache}.
+   */
+  private Map<FileDiffCacheKey, GitDiffEntity> computeGitFileDiffs(
+      Map<FileDiffCacheKey, GitFileDiffCacheKey> keys) throws DiffNotAvailableException {
+    ImmutableMap.Builder<FileDiffCacheKey, GitDiffEntity> result =
+        ImmutableMap.builderWithExpectedSize(keys.size());
+    ImmutableMap<GitFileDiffCacheKey, GitFileDiff> gitDiffs = gitCache.getAll(keys.values());
+    for (FileDiffCacheKey key : keys.keySet()) {
+      GitFileDiffCacheKey gitKey = keys.get(key);
+      GitFileDiff gitFileDiff = gitDiffs.get(gitKey);
+      result.put(key, GitDiffEntity.create(gitKey, gitFileDiff));
+    }
+    return result.build();
+  }
+
+  /**
+   * Convert a list of {@link AugmentedFileDiffCacheKey} to their corresponding {@link
+   * GitFileDiffCacheKey} which can be used to call the underlying {@link GitFileDiffCache}.
+   *
+   * @param keys a list of input {@link AugmentedFileDiffCacheKey}s.
+   * @param aCommitFn a function to compute the aCommit that will be used in the git diff.
+   * @param bCommitFn a function to compute the bCommit that will be used in the git diff.
+   * @param newPathFn a function to compute the new path of the git key.
+   * @return a map of the input {@link FileDiffCacheKey} to the {@link GitFileDiffCacheKey}.
+   */
+  private Map<FileDiffCacheKey, GitFileDiffCacheKey> createGitKeys(
+      List<AugmentedFileDiffCacheKey> keys,
+      Function<AugmentedFileDiffCacheKey, ObjectId> aCommitFn,
+      Function<AugmentedFileDiffCacheKey, ObjectId> bCommitFn,
+      Function<AugmentedFileDiffCacheKey, String> newPathFn) {
+    Map<FileDiffCacheKey, GitFileDiffCacheKey> result = new HashMap<>();
+    for (AugmentedFileDiffCacheKey key : keys) {
+      try {
+        String path = newPathFn.apply(key);
+        if (path != null) {
+          result.put(
+              key.key(),
+              createGitKey(key.key(), aCommitFn.apply(key), bCommitFn.apply(key), path, rw));
+        }
+      } catch (IOException e) {
+        // TODO(ghareeb): This implies that the output keys may not have the same size as the input.
+        // Check the caller's code path about the correctness of the computation in this case. If
+        // errors are rare, it may be better to throw an exception and fail the whole computation.
+        logger.atWarning().log("Failed to compute the git key for key %s: %s", key, e.getMessage());
+      }
+    }
+    return result;
+  }
+
+  /** Returns the {@link GitFileDiffCacheKey} for the {@code key} input parameter. */
+  private GitFileDiffCacheKey createGitKey(
+      FileDiffCacheKey key, ObjectId aCommit, ObjectId bCommit, String pathNew, RevWalk rw)
+      throws IOException {
+    ObjectId oldTreeId =
+        aCommit.equals(EMPTY_TREE_ID) ? EMPTY_TREE_ID : DiffUtil.getTreeId(rw, aCommit);
+    ObjectId newTreeId = DiffUtil.getTreeId(rw, bCommit);
+    return GitFileDiffCacheKey.builder()
+        .project(key.project())
+        .oldTree(oldTreeId)
+        .newTree(newTreeId)
+        .newFilePath(pathNew == null ? key.newFilePath() : pathNew)
+        .renameScore(key.renameScore())
+        .diffAlgorithm(key.diffAlgorithm())
+        .whitespace(key.whitespace())
+        .build();
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/AllFileGitDiffs.java b/java/com/google/gerrit/server/patch/filediff/AllFileGitDiffs.java
new file mode 100644
index 0000000..3b1886f
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/AllFileGitDiffs.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+
+/**
+ * An entity containing the four git diffs for a {@link FileDiffCacheKey}:
+ *
+ * <ol>
+ *   <li>The old vs. new commit
+ *   <li>The old commit vs. the old parent
+ *   <li>The new commit vs. the new parent
+ *   <li>The old parent vs. the new parent
+ * </ol>
+ */
+@AutoValue
+abstract class AllFileGitDiffs {
+  abstract AugmentedFileDiffCacheKey augmentedKey();
+
+  abstract GitDiffEntity mainDiff();
+
+  abstract Optional<GitDiffEntity> oldVsParentDiff();
+
+  abstract Optional<GitDiffEntity> newVsParentDiff();
+
+  abstract Optional<GitDiffEntity> parentVsParentDiff();
+
+  static AllFileGitDiffs.Builder builder() {
+    return new AutoValue_AllFileGitDiffs.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder augmentedKey(AugmentedFileDiffCacheKey value);
+
+    public abstract Builder mainDiff(GitDiffEntity value);
+
+    public abstract Builder oldVsParentDiff(Optional<GitDiffEntity> value);
+
+    public abstract Builder newVsParentDiff(Optional<GitDiffEntity> value);
+
+    public abstract Builder parentVsParentDiff(Optional<GitDiffEntity> value);
+
+    public abstract AllFileGitDiffs build();
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/AugmentedFileDiffCacheKey.java b/java/com/google/gerrit/server/patch/filediff/AugmentedFileDiffCacheKey.java
new file mode 100644
index 0000000..8e40452
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/AugmentedFileDiffCacheKey.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * A wrapper entity to the {@link FileDiffCacheKey} that also includes the old parent commit ID, the
+ * new parent commit ID and if we should ignore computing the rebase edits for that key.
+ */
+@AutoValue
+abstract class AugmentedFileDiffCacheKey {
+  abstract FileDiffCacheKey key();
+
+  abstract boolean ignoreRebase();
+
+  abstract Optional<ObjectId> oldParentId();
+
+  abstract Optional<ObjectId> newParentId();
+
+  static Builder builder() {
+    return new AutoValue_AugmentedFileDiffCacheKey.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder oldParentId(Optional<ObjectId> value);
+
+    public abstract Builder newParentId(Optional<ObjectId> value);
+
+    public abstract Builder ignoreRebase(boolean value);
+
+    public abstract Builder key(FileDiffCacheKey value);
+
+    public abstract AugmentedFileDiffCacheKey build();
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/Edit.java b/java/com/google/gerrit/server/patch/filediff/Edit.java
index b1b336e..4a698a4 100644
--- a/java/com/google/gerrit/server/patch/filediff/Edit.java
+++ b/java/com/google/gerrit/server/patch/filediff/Edit.java
@@ -32,6 +32,14 @@
         jgitEdit.getBeginA(), jgitEdit.getEndA(), jgitEdit.getBeginB(), jgitEdit.getEndB());
   }
 
+  public static org.eclipse.jgit.diff.Edit toJGitEdit(Edit e) {
+    return new org.eclipse.jgit.diff.Edit(e.beginA(), e.endA(), e.beginB(), e.endB());
+  }
+
+  public org.eclipse.jgit.diff.Edit asJGitEdit() {
+    return new org.eclipse.jgit.diff.Edit(beginA(), endA(), beginB(), endB());
+  }
+
   /** Start of a region in sequence A. */
   public abstract int beginA();
 
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCache.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCache.java
new file mode 100644
index 0000000..a9bcf03
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCache.java
@@ -0,0 +1,45 @@
+//  Copyright (C) 2020 The Android Open Source Project
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+
+/**
+ * This cache computes the git diff for a single file path and adds some extra logic, e.g. for
+ * identifying edits that are due to rebase.
+ */
+public interface FileDiffCache {
+  /**
+   * Returns the file diff for a single file path identified by its key.
+   *
+   * @param key identifies two git commits, a specific file path and other diff parameters.
+   * @return the file diff for a single file path identified by its key.
+   * @throws DiffNotAvailableException if the commit IDs of the key are invalid for this project or
+   *     if file contents could not be read.
+   */
+  FileDiffOutput get(FileDiffCacheKey key) throws DiffNotAvailableException;
+
+  /**
+   * Returns the file diff for a collection of file paths identified by their keys.
+   *
+   * @param keys identifying different file paths of different projects.
+   * @return a map of the input keys to their corresponding git file diffs.
+   * @throws DiffNotAvailableException if the diff failed to be evaluated for one or more of the
+   *     input keys due to invalid commit IDs or if file contents could not be read.
+   */
+  ImmutableMap<FileDiffCacheKey, FileDiffOutput> getAll(Iterable<FileDiffCacheKey> keys)
+      throws DiffNotAvailableException;
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
new file mode 100644
index 0000000..091b02c
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -0,0 +1,509 @@
+//  Copyright (C) 2020 The Android Open Source Project
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.ComparisonType;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffUtil;
+import com.google.gerrit.server.patch.Text;
+import com.google.gerrit.server.patch.filediff.EditTransformer.ContextAwareEdit;
+import com.google.gerrit.server.patch.gitfilediff.FileHeaderUtil;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiff;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl.DiffAlgorithmFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.diff.EditList;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.patch.FileHeader;
+import org.eclipse.jgit.patch.FileHeader.PatchType;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Cache for the single file diff between two commits for a single file path. This cache adds extra
+ * Gerrit logic such as identifying edits due to rebase.
+ *
+ * <p>If the {@link FileDiffCacheKey#oldCommit()} is equal to {@link
+ * org.eclipse.jgit.lib.Constants#EMPTY_TREE_ID}, the git diff will be evaluated against the empty
+ * tree.
+ */
+public class FileDiffCacheImpl implements FileDiffCache {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private static final String DIFF = "gerrit_file_diff";
+
+  private final LoadingCache<FileDiffCacheKey, FileDiffOutput> cache;
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        bind(FileDiffCache.class).to(FileDiffCacheImpl.class);
+
+        factory(AllDiffsEvaluator.Factory.class);
+
+        persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
+            .maximumWeight(10 << 20)
+            .weigher(FileDiffWeigher.class)
+            .keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
+            .valueSerializer(FileDiffOutput.Serializer.INSTANCE)
+            .loader(FileDiffLoader.class);
+      }
+    };
+  }
+
+  private enum MagicPath {
+    COMMIT,
+    MERGE_LIST
+  }
+
+  @Inject
+  public FileDiffCacheImpl(@Named(DIFF) LoadingCache<FileDiffCacheKey, FileDiffOutput> cache) {
+    this.cache = cache;
+  }
+
+  @Override
+  public FileDiffOutput get(FileDiffCacheKey key) throws DiffNotAvailableException {
+    try {
+      return cache.get(key);
+    } catch (ExecutionException e) {
+      throw new DiffNotAvailableException(e);
+    }
+  }
+
+  @Override
+  public ImmutableMap<FileDiffCacheKey, FileDiffOutput> getAll(Iterable<FileDiffCacheKey> keys)
+      throws DiffNotAvailableException {
+    try {
+      ImmutableMap<FileDiffCacheKey, FileDiffOutput> result = cache.getAll(keys);
+      if (result.size() != Iterables.size(keys)) {
+        throw new DiffNotAvailableException(
+            String.format(
+                "Failed to load the value for all %d keys. Returned "
+                    + "map contains only %d values",
+                Iterables.size(keys), result.size()));
+      }
+      return result;
+    } catch (ExecutionException e) {
+      throw new DiffNotAvailableException(e);
+    }
+  }
+
+  static class FileDiffLoader extends CacheLoader<FileDiffCacheKey, FileDiffOutput> {
+    private final GitRepositoryManager repoManager;
+    private final AllDiffsEvaluator.Factory allDiffsEvaluatorFactory;
+
+    @Inject
+    FileDiffLoader(
+        AllDiffsEvaluator.Factory allDiffsEvaluatorFactory, GitRepositoryManager manager) {
+      this.allDiffsEvaluatorFactory = allDiffsEvaluatorFactory;
+      this.repoManager = manager;
+    }
+
+    @Override
+    public FileDiffOutput load(FileDiffCacheKey key) throws IOException, DiffNotAvailableException {
+      return loadAll(ImmutableList.of(key)).get(key);
+    }
+
+    @Override
+    public Map<FileDiffCacheKey, FileDiffOutput> loadAll(Iterable<? extends FileDiffCacheKey> keys)
+        throws DiffNotAvailableException {
+      ImmutableMap.Builder<FileDiffCacheKey, FileDiffOutput> result = ImmutableMap.builder();
+
+      Map<Project.NameKey, List<FileDiffCacheKey>> keysByProject =
+          Streams.stream(keys).distinct().collect(Collectors.groupingBy(FileDiffCacheKey::project));
+
+      for (Project.NameKey project : keysByProject.keySet()) {
+        List<FileDiffCacheKey> fileKeys = new ArrayList<>();
+
+        try (Repository repo = repoManager.openRepository(project);
+            ObjectReader reader = repo.newObjectReader();
+            RevWalk rw = new RevWalk(reader)) {
+
+          for (FileDiffCacheKey key : keysByProject.get(project)) {
+            if (key.newFilePath().equals(Patch.COMMIT_MSG)) {
+              result.put(key, createMagicPathEntry(key, reader, rw, MagicPath.COMMIT));
+            } else if (key.newFilePath().equals(Patch.MERGE_LIST)) {
+              result.put(key, createMagicPathEntry(key, reader, rw, MagicPath.MERGE_LIST));
+            } else {
+              fileKeys.add(key);
+            }
+          }
+          result.putAll(createFileEntries(reader, fileKeys, rw));
+        } catch (IOException e) {
+          logger.atWarning().log("Failed to open the repository %s: %s", project, e.getMessage());
+        }
+      }
+      return result.build();
+    }
+
+    private ComparisonType getComparisonType(RevWalk rw, ObjectId oldCommitId, ObjectId newCommitId)
+        throws IOException {
+      RevCommit oldCommit = DiffUtil.getRevCommit(rw, oldCommitId);
+      RevCommit newCommit = DiffUtil.getRevCommit(rw, newCommitId);
+      for (int i = 0; i < newCommit.getParentCount(); i++) {
+        if (newCommit.getParent(i).equals(oldCommit)) {
+          return ComparisonType.againstParent(i + 1);
+        }
+      }
+      if (newCommit.getParentCount() > 0) {
+        return ComparisonType.againstAutoMerge();
+      }
+      return ComparisonType.againstOtherPatchSet();
+    }
+
+    /**
+     * Creates a {@link FileDiffOutput} entry for the "Commit message" and "Merge list" file paths.
+     */
+    private FileDiffOutput createMagicPathEntry(
+        FileDiffCacheKey key, ObjectReader reader, RevWalk rw, MagicPath magicPath) {
+      try {
+        RawTextComparator cmp = comparatorFor(key.whitespace());
+        ComparisonType comparisonType = getComparisonType(rw, key.oldCommit(), key.newCommit());
+        RevCommit aCommit =
+            comparisonType.isAgainstParentOrAutoMerge()
+                ? null
+                : DiffUtil.getRevCommit(rw, key.oldCommit());
+        RevCommit bCommit = DiffUtil.getRevCommit(rw, key.newCommit());
+        return magicPath == MagicPath.COMMIT
+            ? createCommitEntry(reader, aCommit, bCommit, cmp, key.diffAlgorithm())
+            : createMergeListEntry(
+                reader, aCommit, bCommit, comparisonType, cmp, key.diffAlgorithm());
+      } catch (IOException e) {
+        logger.atWarning().log("Failed to compute commit entry for key " + key);
+      }
+      return FileDiffOutput.empty(key.newFilePath());
+    }
+
+    private static RawTextComparator comparatorFor(Whitespace ws) {
+      switch (ws) {
+        case IGNORE_ALL:
+          return RawTextComparator.WS_IGNORE_ALL;
+
+        case IGNORE_TRAILING:
+          return RawTextComparator.WS_IGNORE_TRAILING;
+
+        case IGNORE_LEADING_AND_TRAILING:
+          return RawTextComparator.WS_IGNORE_CHANGE;
+
+        case IGNORE_NONE:
+        default:
+          return RawTextComparator.DEFAULT;
+      }
+    }
+
+    private FileDiffOutput createCommitEntry(
+        ObjectReader reader,
+        RevCommit oldCommit,
+        RevCommit newCommit,
+        RawTextComparator rawTextComparator,
+        GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
+        throws IOException {
+      Text aText = newCommit != null ? Text.forCommit(reader, newCommit) : Text.EMPTY;
+      Text bText = Text.forCommit(reader, newCommit);
+      return createMagicFileDiffOutput(
+          rawTextComparator, oldCommit, aText, bText, Patch.COMMIT_MSG, diffAlgorithm);
+    }
+
+    private FileDiffOutput createMergeListEntry(
+        ObjectReader reader,
+        RevCommit oldCommit,
+        RevCommit newCommit,
+        ComparisonType comparisonType,
+        RawTextComparator rawTextComparator,
+        GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm)
+        throws IOException {
+      Text aText =
+          oldCommit != null ? Text.forMergeList(comparisonType, reader, oldCommit) : Text.EMPTY;
+      Text bText = Text.forMergeList(comparisonType, reader, newCommit);
+      return createMagicFileDiffOutput(
+          rawTextComparator, oldCommit, aText, bText, Patch.MERGE_LIST, diffAlgorithm);
+    }
+
+    private static FileDiffOutput createMagicFileDiffOutput(
+        RawTextComparator rawTextComparator,
+        RevCommit aCommit,
+        Text aText,
+        Text bText,
+        String fileName,
+        GitFileDiffCacheImpl.DiffAlgorithm diffAlgorithm) {
+      byte[] rawHdr = getRawHeader(aCommit != null, fileName);
+      byte[] aContent = aText.getContent();
+      byte[] bContent = bText.getContent();
+      long size = bContent.length;
+      long sizeDelta = size - aContent.length;
+      RawText aRawText = new RawText(aContent);
+      RawText bRawText = new RawText(bContent);
+      EditList edits =
+          DiffAlgorithmFactory.create(diffAlgorithm).diff(rawTextComparator, aRawText, bRawText);
+      FileHeader fileHeader = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
+      Patch.ChangeType changeType = FileHeaderUtil.getChangeType(fileHeader);
+      return FileDiffOutput.builder()
+          .oldPath(FileHeaderUtil.getOldPath(fileHeader))
+          .newPath(FileHeaderUtil.getNewPath(fileHeader))
+          .changeType(Optional.of(changeType))
+          .patchType(Optional.of(FileHeaderUtil.getPatchType(fileHeader)))
+          .headerLines(FileHeaderUtil.getHeaderLines(fileHeader))
+          .edits(
+              asTaggedEdits(
+                  edits.stream().map(Edit::fromJGitEdit).collect(Collectors.toList()),
+                  ImmutableList.of()))
+          .size(size)
+          .sizeDelta(sizeDelta)
+          .build();
+    }
+
+    private static byte[] getRawHeader(boolean hasA, String fileName) {
+      StringBuilder hdr = new StringBuilder();
+      hdr.append("diff --git");
+      if (hasA) {
+        hdr.append(" a/").append(fileName);
+      } else {
+        hdr.append(" ").append(FileHeader.DEV_NULL);
+      }
+      hdr.append(" b/").append(fileName);
+      hdr.append("\n");
+
+      if (hasA) {
+        hdr.append("--- a/").append(fileName).append("\n");
+      } else {
+        hdr.append("--- ").append(FileHeader.DEV_NULL).append("\n");
+      }
+      hdr.append("+++ b/").append(fileName).append("\n");
+      return hdr.toString().getBytes(UTF_8);
+    }
+
+    private Map<FileDiffCacheKey, FileDiffOutput> createFileEntries(
+        ObjectReader reader, List<FileDiffCacheKey> keys, RevWalk rw)
+        throws DiffNotAvailableException, IOException {
+      Map<AugmentedFileDiffCacheKey, AllFileGitDiffs> allFileDiffs =
+          allDiffsEvaluatorFactory.create(rw).execute(wrapKeys(keys, rw));
+
+      Map<FileDiffCacheKey, FileDiffOutput> result = new HashMap<>();
+
+      for (AugmentedFileDiffCacheKey augmentedKey : allFileDiffs.keySet()) {
+        AllFileGitDiffs allDiffs = allFileDiffs.get(augmentedKey);
+
+        FileEdits rebaseFileEdits = FileEdits.empty();
+        if (!augmentedKey.ignoreRebase()) {
+          rebaseFileEdits = computeRebaseEdits(allDiffs);
+        }
+        List<Edit> rebaseEdits = rebaseFileEdits.edits();
+
+        RevTree aTree = rw.parseTree(allDiffs.mainDiff().gitKey().oldTree());
+        RevTree bTree = rw.parseTree(allDiffs.mainDiff().gitKey().newTree());
+        GitFileDiff mainGitDiff = allDiffs.mainDiff().gitDiff();
+
+        Long oldSize =
+            mainGitDiff.oldPath().isPresent()
+                ? new FileSizeEvaluator(reader, aTree)
+                    .compute(
+                        mainGitDiff.oldId(),
+                        mainGitDiff.oldMode().get(),
+                        mainGitDiff.oldPath().get())
+                : 0;
+        Long newSize =
+            mainGitDiff.newPath().isPresent()
+                ? new FileSizeEvaluator(reader, bTree)
+                    .compute(
+                        mainGitDiff.newId(),
+                        mainGitDiff.newMode().get(),
+                        mainGitDiff.newPath().get())
+                : 0;
+
+        FileDiffOutput fileDiff =
+            FileDiffOutput.builder()
+                .changeType(mainGitDiff.changeType())
+                .patchType(mainGitDiff.patchType())
+                .oldPath(mainGitDiff.oldPath())
+                .newPath(mainGitDiff.newPath())
+                .headerLines(FileHeaderUtil.getHeaderLines(mainGitDiff.fileHeader()))
+                .edits(asTaggedEdits(mainGitDiff.edits(), rebaseEdits))
+                .size(newSize)
+                .sizeDelta(newSize - oldSize)
+                .build();
+
+        result.put(augmentedKey.key(), fileDiff);
+      }
+
+      return result;
+    }
+
+    /**
+     * Convert the list of input keys {@link FileDiffCacheKey} to a list of {@link
+     * AugmentedFileDiffCacheKey} that also include the old and new parent commit IDs, and a boolean
+     * that indicates whether we should include the rebase edits for each key.
+     *
+     * <p>The output list is expected to have the same size of the input list, i.e. we map all keys.
+     */
+    private List<AugmentedFileDiffCacheKey> wrapKeys(List<FileDiffCacheKey> keys, RevWalk rw) {
+      List<AugmentedFileDiffCacheKey> result = new ArrayList<>();
+      for (FileDiffCacheKey key : keys) {
+        if (key.oldCommit().equals(EMPTY_TREE_ID)) {
+          result.add(AugmentedFileDiffCacheKey.builder().key(key).ignoreRebase(true).build());
+          continue;
+        }
+        try {
+          RevCommit oldRevCommit = DiffUtil.getRevCommit(rw, key.oldCommit());
+          RevCommit newRevCommit = DiffUtil.getRevCommit(rw, key.newCommit());
+          if (!DiffUtil.areRelated(oldRevCommit, newRevCommit)) {
+            result.add(
+                AugmentedFileDiffCacheKey.builder()
+                    .key(key)
+                    .oldParentId(Optional.of(oldRevCommit.getParent(0).getId()))
+                    .newParentId(Optional.of(newRevCommit.getParent(0).getId()))
+                    .ignoreRebase(false)
+                    .build());
+          } else {
+            result.add(AugmentedFileDiffCacheKey.builder().key(key).ignoreRebase(true).build());
+          }
+        } catch (IOException e) {
+          logger.atWarning().log(
+              "Failed to evaluate commits relation for key "
+                  + key
+                  + ". Skipping this key: "
+                  + e.getMessage(),
+              e);
+          result.add(AugmentedFileDiffCacheKey.builder().key(key).ignoreRebase(true).build());
+        }
+      }
+      return result;
+    }
+
+    private static ImmutableList<TaggedEdit> asTaggedEdits(
+        List<Edit> normalEdits, List<Edit> rebaseEdits) {
+      Set<Edit> rebaseEditsSet = new HashSet(rebaseEdits);
+      ImmutableList.Builder<TaggedEdit> result =
+          ImmutableList.builderWithExpectedSize(normalEdits.size());
+      for (Edit e : normalEdits) {
+        result.add(TaggedEdit.create(e, rebaseEditsSet.contains(e)));
+      }
+      return result.build();
+    }
+
+    /**
+     * Computes the subset of edits that are due to rebase between 2 commits.
+     *
+     * <p>The input parameter {@link AllFileGitDiffs#mainDiff} contains all the edits in
+     * consideration. Of those, we identify the edits due to rebase as a function of:
+     *
+     * <ol>
+     *   <li>The edits between the old commit and its parent {@link
+     *       AllFileGitDiffs#oldVsParentDiff}.
+     *   <li>The edits between the new commit and its parent {@link
+     *       AllFileGitDiffs#newVsParentDiff}.
+     *   <li>The edits between the parents of the old commit and new commits {@link
+     *       AllFileGitDiffs#parentVsParentDiff}.
+     * </ol>
+     *
+     * @param diffs an entity containing 4 sets of edits: those between the old and new commit,
+     *     between the old and new commits vs. their parents, and between the old and new parents.
+     * @return the list of edits that are due to rebase.
+     */
+    private FileEdits computeRebaseEdits(AllFileGitDiffs diffs) {
+      if (!diffs.parentVsParentDiff().isPresent()) {
+        return FileEdits.empty();
+      }
+
+      GitFileDiff parentVsParentDiff = diffs.parentVsParentDiff().get().gitDiff();
+
+      EditTransformer editTransformer =
+          new EditTransformer(
+              ImmutableList.of(
+                  FileEdits.create(
+                      parentVsParentDiff.edits().stream()
+                          .map(Edit::toJGitEdit)
+                          .collect(Collectors.toList()),
+                      parentVsParentDiff.oldPath(),
+                      parentVsParentDiff.newPath())));
+
+      if (diffs.oldVsParentDiff().isPresent()) {
+        GitFileDiff oldVsParDiff = diffs.oldVsParentDiff().get().gitDiff();
+        editTransformer.transformReferencesOfSideA(
+            ImmutableList.of(
+                FileEdits.create(
+                    oldVsParDiff.edits().stream()
+                        .map(Edit::toJGitEdit)
+                        .collect(Collectors.toList()),
+                    oldVsParDiff.oldPath(),
+                    oldVsParDiff.newPath())));
+      }
+
+      if (diffs.newVsParentDiff().isPresent()) {
+        GitFileDiff newVsParDiff = diffs.newVsParentDiff().get().gitDiff();
+        editTransformer.transformReferencesOfSideB(
+            ImmutableList.of(
+                FileEdits.create(
+                    newVsParDiff.edits().stream()
+                        .map(Edit::toJGitEdit)
+                        .collect(Collectors.toList()),
+                    newVsParDiff.oldPath(),
+                    newVsParDiff.newPath())));
+      }
+
+      Multimap<String, ContextAwareEdit> editsPerFilePath = editTransformer.getEditsPerFilePath();
+
+      if (editsPerFilePath.isEmpty()) {
+        return FileEdits.empty();
+      }
+
+      // editsPerFilePath is expected to have a single item representing the file
+      String filePath = editsPerFilePath.keys().iterator().next();
+      Collection<ContextAwareEdit> edits = editsPerFilePath.get(filePath);
+      return FileEdits.create(
+          Streams.stream(edits)
+              .map(ContextAwareEdit::toEdit)
+              .filter(Optional::isPresent)
+              .map(Optional::get)
+              .collect(Collectors.toList()),
+          edits.iterator().next().getOldFilePath(),
+          edits.iterator().next().getNewFilePath());
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java
new file mode 100644
index 0000000..a478fcf
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheKey.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import static com.google.gerrit.server.patch.DiffUtil.stringSize;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.server.cache.proto.Cache.FileDiffKeyProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl.DiffAlgorithm;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Cache key for the {@link FileDiffCache}. */
+@AutoValue
+public abstract class FileDiffCacheKey {
+
+  /** A specific git project / repository. */
+  public abstract Project.NameKey project();
+
+  /** The 20 bytes SHA-1 commit ID of the old commit used in the diff. */
+  public abstract ObjectId oldCommit();
+
+  /** The 20 bytes SHA-1 commit ID of the new commit used in the diff. */
+  public abstract ObjectId newCommit();
+
+  /** File path identified by its name. */
+  public abstract String newFilePath();
+
+  /**
+   * Percentage score used to identify a file as a "rename". A special value of -1 means that the
+   * computation will ignore renames and rename detection will be disabled.
+   */
+  public abstract int renameScore();
+
+  /** The diff algorithm that should be used in the computation. */
+  public abstract DiffAlgorithm diffAlgorithm();
+
+  public abstract DiffPreferencesInfo.Whitespace whitespace();
+
+  /** Number of bytes that this entity occupies. */
+  public int weight() {
+    return stringSize(project().get())
+        + 20 * 2 // old and new commits
+        + stringSize(newFilePath())
+        + 4 // renameScore
+        + 4 // diffAlgorithm
+        + 4; // whitespace
+  }
+
+  public static FileDiffCacheKey.Builder builder() {
+    return new AutoValue_FileDiffCacheKey.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract FileDiffCacheKey.Builder project(NameKey value);
+
+    public abstract FileDiffCacheKey.Builder oldCommit(ObjectId value);
+
+    public abstract FileDiffCacheKey.Builder newCommit(ObjectId value);
+
+    public abstract FileDiffCacheKey.Builder newFilePath(String value);
+
+    public abstract FileDiffCacheKey.Builder renameScore(int value);
+
+    public FileDiffCacheKey.Builder disableRenameDetection() {
+      renameScore(-1);
+      return this;
+    }
+
+    public abstract FileDiffCacheKey.Builder diffAlgorithm(DiffAlgorithm value);
+
+    public abstract FileDiffCacheKey.Builder whitespace(Whitespace value);
+
+    public abstract FileDiffCacheKey build();
+  }
+
+  public enum Serializer implements CacheSerializer<FileDiffCacheKey> {
+    INSTANCE;
+
+    @Override
+    public byte[] serialize(FileDiffCacheKey key) {
+      ObjectIdConverter idConverter = ObjectIdConverter.create();
+      return Protos.toByteArray(
+          FileDiffKeyProto.newBuilder()
+              .setProject(key.project().get())
+              .setOldCommit(idConverter.toByteString(key.oldCommit()))
+              .setNewCommit(idConverter.toByteString(key.newCommit()))
+              .setFilePath(key.newFilePath())
+              .setRenameScore(key.renameScore())
+              .setDiffAlgorithm(key.diffAlgorithm().name())
+              .setWhitespace(key.whitespace().name())
+              .build());
+    }
+
+    @Override
+    public FileDiffCacheKey deserialize(byte[] in) {
+      FileDiffKeyProto proto = Protos.parseUnchecked(FileDiffKeyProto.parser(), in);
+      ObjectIdConverter idConverter = ObjectIdConverter.create();
+      return FileDiffCacheKey.builder()
+          .project(Project.nameKey(proto.getProject()))
+          .oldCommit(idConverter.fromByteString(proto.getOldCommit()))
+          .newCommit(idConverter.fromByteString(proto.getNewCommit()))
+          .newFilePath(proto.getFilePath())
+          .renameScore(proto.getRenameScore())
+          .diffAlgorithm(DiffAlgorithm.valueOf(proto.getDiffAlgorithm()))
+          .whitespace(Whitespace.valueOf(proto.getWhitespace()))
+          .build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
new file mode 100644
index 0000000..e89f148
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
@@ -0,0 +1,255 @@
+//  Copyright (C) 2020 The Android Open Source Project
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import static com.google.gerrit.server.patch.DiffUtil.stringSize;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.Patch.PatchType;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.server.cache.proto.Cache.FileDiffOutputProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import java.io.Serializable;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/** File diff for a single file path. Produced as output of the {@link FileDiffCache}. */
+@AutoValue
+public abstract class FileDiffOutput implements Serializable {
+
+  /**
+   * The file path at the old commit. Returns an empty Optional if {@link #changeType()} is equal to
+   * {@link ChangeType#ADDED}.
+   */
+  public abstract Optional<String> oldPath();
+
+  /**
+   * The file path at the new commit. Returns an empty optional if {@link #changeType()} is equal to
+   * {@link ChangeType#DELETED}.
+   */
+  public abstract Optional<String> newPath();
+
+  /** The change type of the underlying file, e.g. added, deleted, renamed, etc... */
+  public abstract Optional<Patch.ChangeType> changeType();
+
+  /** The patch type of the underlying file, e.g. unified, binary , etc... */
+  public abstract Optional<Patch.PatchType> patchType();
+
+  /**
+   * A list of strings representation of the header lines of the {@link
+   * org.eclipse.jgit.patch.FileHeader} that is produced as output of the diff.
+   */
+  public abstract ImmutableList<String> headerLines();
+
+  /** The list of edits resulting from the diff hunks of the file. */
+  public abstract ImmutableList<TaggedEdit> edits();
+
+  /** The file size at the new commit. */
+  public abstract long size();
+
+  /** Difference in file size between the old and new commits. */
+  public abstract long sizeDelta();
+
+  /** A boolean indicating if all underlying edits of the file diff are due to rebase. */
+  public boolean allEditsDueToRebase() {
+    return !edits().isEmpty() && edits().stream().allMatch(TaggedEdit::dueToRebase);
+  }
+
+  /** Returns the number of inserted lines for the file diff. */
+  public int insertions() {
+    int ins = 0;
+    for (TaggedEdit e : edits()) {
+      if (!e.dueToRebase()) {
+        ins += e.edit().endB() - e.edit().beginB();
+      }
+    }
+    return ins;
+  }
+
+  /** Returns the number of deleted lines for the file diff. */
+  public int deletions() {
+    int del = 0;
+    for (TaggedEdit e : edits()) {
+      if (!e.dueToRebase()) {
+        del += e.edit().endA() - e.edit().beginA();
+      }
+    }
+    return del;
+  }
+
+  /** Returns an entity representing an unchanged file between two commits. */
+  static FileDiffOutput empty(String filePath) {
+    return builder()
+        .oldPath(Optional.empty())
+        .newPath(Optional.of(filePath))
+        .headerLines(ImmutableList.of())
+        .edits(ImmutableList.of())
+        .size(0)
+        .sizeDelta(0)
+        .build();
+  }
+
+  /** Returns true if this entity represents an unchanged file between two commits. */
+  public boolean isEmpty() {
+    return headerLines().isEmpty() && edits().isEmpty();
+  }
+
+  public static Builder builder() {
+    return new AutoValue_FileDiffOutput.Builder();
+  }
+
+  public int weight() {
+    int result = 0;
+    if (oldPath().isPresent()) {
+      result += stringSize(oldPath().get());
+    }
+    if (newPath().isPresent()) {
+      result += stringSize(newPath().get());
+    }
+    if (changeType().isPresent()) {
+      result += 4;
+    }
+    if (patchType().isPresent()) {
+      result += 4;
+    }
+    result += 4 + 4; // insertions and deletions
+    result += 4 + 4; // size and size delta
+    result += 20 * edits().size(); // each edit is 4 Integers + boolean = 4 * 4 + 4 = 20
+    for (String s : headerLines()) {
+      s += stringSize(s);
+    }
+    return result;
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder oldPath(Optional<String> value);
+
+    public abstract Builder newPath(Optional<String> value);
+
+    public abstract Builder changeType(Optional<ChangeType> value);
+
+    public abstract Builder patchType(Optional<PatchType> value);
+
+    public abstract Builder headerLines(ImmutableList<String> value);
+
+    public abstract Builder edits(ImmutableList<TaggedEdit> value);
+
+    public abstract Builder size(long value);
+
+    public abstract Builder sizeDelta(long value);
+
+    public abstract FileDiffOutput build();
+  }
+
+  public enum Serializer implements CacheSerializer<FileDiffOutput> {
+    INSTANCE;
+
+    private static final FieldDescriptor OLD_PATH_DESCRIPTOR =
+        FileDiffOutputProto.getDescriptor().findFieldByNumber(1);
+
+    private static final FieldDescriptor NEW_PATH_DESCRIPTOR =
+        FileDiffOutputProto.getDescriptor().findFieldByNumber(2);
+
+    private static final FieldDescriptor CHANGE_TYPE_DESCRIPTOR =
+        FileDiffOutputProto.getDescriptor().findFieldByNumber(3);
+
+    private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR =
+        FileDiffOutputProto.getDescriptor().findFieldByNumber(4);
+
+    @Override
+    public byte[] serialize(FileDiffOutput fileDiff) {
+      FileDiffOutputProto.Builder builder =
+          FileDiffOutputProto.newBuilder()
+              .setSize(fileDiff.size())
+              .setSizeDelta(fileDiff.sizeDelta())
+              .addAllHeaderLines(fileDiff.headerLines())
+              .addAllEdits(
+                  fileDiff.edits().stream()
+                      .map(
+                          e ->
+                              FileDiffOutputProto.TaggedEdit.newBuilder()
+                                  .setEdit(
+                                      FileDiffOutputProto.Edit.newBuilder()
+                                          .setBeginA(e.edit().beginA())
+                                          .setEndA(e.edit().endA())
+                                          .setBeginB(e.edit().beginB())
+                                          .setEndB(e.edit().endB())
+                                          .build())
+                                  .setDueToRebase(e.dueToRebase())
+                                  .build())
+                      .collect(Collectors.toList()));
+
+      if (fileDiff.oldPath().isPresent()) {
+        builder.setOldPath(fileDiff.oldPath().get());
+      }
+
+      if (fileDiff.newPath().isPresent()) {
+        builder.setNewPath(fileDiff.newPath().get());
+      }
+
+      if (fileDiff.changeType().isPresent()) {
+        builder.setChangeType(fileDiff.changeType().get().name());
+      }
+
+      if (fileDiff.patchType().isPresent()) {
+        builder.setPatchType(fileDiff.patchType().get().name());
+      }
+
+      return Protos.toByteArray(builder.build());
+    }
+
+    @Override
+    public FileDiffOutput deserialize(byte[] in) {
+      FileDiffOutputProto proto = Protos.parseUnchecked(FileDiffOutputProto.parser(), in);
+      FileDiffOutput.Builder builder = FileDiffOutput.builder();
+      builder
+          .size(proto.getSize())
+          .sizeDelta(proto.getSizeDelta())
+          .headerLines(proto.getHeaderLinesList().stream().collect(ImmutableList.toImmutableList()))
+          .edits(
+              proto.getEditsList().stream()
+                  .map(
+                      e ->
+                          TaggedEdit.create(
+                              Edit.create(
+                                  e.getEdit().getBeginA(),
+                                  e.getEdit().getEndA(),
+                                  e.getEdit().getBeginB(),
+                                  e.getEdit().getEndB()),
+                              e.getDueToRebase()))
+                  .collect(ImmutableList.toImmutableList()));
+
+      if (proto.hasField(OLD_PATH_DESCRIPTOR)) {
+        builder.oldPath(Optional.of(proto.getOldPath()));
+      }
+      if (proto.hasField(NEW_PATH_DESCRIPTOR)) {
+        builder.newPath(Optional.of(proto.getNewPath()));
+      }
+      if (proto.hasField(CHANGE_TYPE_DESCRIPTOR)) {
+        builder.changeType(Optional.of(Patch.ChangeType.valueOf(proto.getChangeType())));
+      }
+      if (proto.hasField(PATCH_TYPE_DESCRIPTOR)) {
+        builder.patchType(Optional.of(Patch.PatchType.valueOf(proto.getPatchType())));
+      }
+      return builder.build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffWeigher.java b/java/com/google/gerrit/server/patch/filediff/FileDiffWeigher.java
new file mode 100644
index 0000000..8eda234
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffWeigher.java
@@ -0,0 +1,29 @@
+//  Copyright (C) 2020 The Android Open Source Project
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.common.cache.Weigher;
+
+/**
+ * A weigher for the {@link FileDiffCache} key and value. This is used by the cache backend to
+ * assign weights for cache entries and is used for evictions.
+ */
+public class FileDiffWeigher implements Weigher<FileDiffCacheKey, FileDiffOutput> {
+
+  @Override
+  public int weigh(FileDiffCacheKey key, FileDiffOutput fileDiffOutput) {
+    return key.weight() + fileDiffOutput.weight();
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/FileEdits.java b/java/com/google/gerrit/server/patch/filediff/FileEdits.java
index c51cc45..376bbc2 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileEdits.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileEdits.java
@@ -22,7 +22,7 @@
 import java.util.Optional;
 
 /**
- * An entity class containing the list of edits between 2 commits for a file, and the old and new
+ * An entity class containing the list of edits between two commits for a file, and the old and new
  * paths.
  */
 @AutoValue
@@ -41,4 +41,8 @@
   public abstract Optional<String> oldPath();
 
   public abstract Optional<String> newPath();
+
+  public static FileEdits empty() {
+    return new AutoValue_FileEdits(ImmutableList.of(), Optional.empty(), Optional.empty());
+  }
 }
diff --git a/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java b/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java
new file mode 100644
index 0000000..97b55dc
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/FileSizeEvaluator.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.Patch.FileMode;
+import com.google.gerrit.exceptions.StorageException;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Optional;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+/** Helper class for computing the size of a file in a given git tree. */
+class FileSizeEvaluator {
+  private final ObjectReader reader;
+  private final RevTree tree;
+
+  FileSizeEvaluator(ObjectReader reader, RevTree tree) {
+    this.reader = reader;
+    this.tree = tree;
+  }
+
+  /**
+   * Computes the file size identified by the {@code path} parameter at the given git tree
+   * identified by {@code gitTreeId}.
+   */
+  long compute(AbbreviatedObjectId gitTreeId, Patch.FileMode mode, String path) throws IOException {
+    if (!isBlob(mode)) {
+      return 0;
+    }
+    ObjectId fileId =
+        toObjectId(reader, gitTreeId).orElseGet(() -> lookupObjectId(reader, path, tree));
+    if (ObjectId.zeroId().equals(fileId)) {
+      return 0;
+    }
+    return reader.getObjectSize(fileId, OBJ_BLOB);
+  }
+
+  private static ObjectId lookupObjectId(ObjectReader reader, String path, RevTree tree) {
+    // This variant is very expensive.
+    try (TreeWalk treeWalk = TreeWalk.forPath(reader, path, tree)) {
+      return treeWalk != null ? treeWalk.getObjectId(0) : ObjectId.zeroId();
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
+  }
+
+  private static Optional<ObjectId> toObjectId(
+      ObjectReader reader, @Nullable AbbreviatedObjectId abbreviatedId) throws IOException {
+    if (abbreviatedId == null) {
+      // In theory, DiffEntry#getOldId or DiffEntry#getNewId can be null for pure renames or pure
+      // mode changes (e.g. DiffEntry#modify doesn't set the IDs). However, the method we call for
+      // diffs (DiffFormatter#scan) seems to always produce DiffEntries with set IDs, even for pure
+      // renames.
+      return Optional.empty();
+    }
+    if (abbreviatedId.isComplete()) {
+      // With the current JGit version and the method we call for diffs (DiffFormatter#scan),
+      // this
+      // is the only code path taken right now.
+      return Optional.ofNullable(abbreviatedId.toObjectId());
+    }
+    Collection<ObjectId> objectIds = reader.resolve(abbreviatedId);
+    // It seems very unlikely that an ObjectId which was just abbreviated by the diff
+    // computation
+    // now can't be resolved to exactly one ObjectId. The API allows this possibility, though.
+    return objectIds.size() == 1
+        ? Optional.of(Iterables.getOnlyElement(objectIds))
+        : Optional.empty();
+  }
+
+  private static boolean isBlob(Patch.FileMode mode) {
+    return mode.equals(FileMode.REGULAR_FILE) || mode.equals(FileMode.SYMLINK);
+  }
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/GitDiffEntity.java b/java/com/google/gerrit/server/patch/filediff/GitDiffEntity.java
new file mode 100644
index 0000000..2ca8fa6
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/GitDiffEntity.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiff;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheKey;
+
+/** An entity containing a {@link GitFileDiffCacheKey} and its loaded value {@link GitFileDiff}. */
+@AutoValue
+abstract class GitDiffEntity {
+  public static GitDiffEntity create(GitFileDiffCacheKey gitKey, GitFileDiff gitDiff) {
+    return new AutoValue_GitDiffEntity(gitKey, gitDiff);
+  }
+
+  abstract GitFileDiffCacheKey gitKey();
+
+  abstract GitFileDiff gitDiff();
+}
diff --git a/java/com/google/gerrit/server/patch/filediff/TaggedEdit.java b/java/com/google/gerrit/server/patch/filediff/TaggedEdit.java
new file mode 100644
index 0000000..aef2f63
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/filediff/TaggedEdit.java
@@ -0,0 +1,33 @@
+//  Copyright (C) 2020 The Android Open Source Project
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//  http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+package com.google.gerrit.server.patch.filediff;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * An entity class encapsulating a JGit {@link Edit} along with extra attributes (e.g. identifying a
+ * rebase edit).
+ */
+@AutoValue
+public abstract class TaggedEdit {
+
+  public static TaggedEdit create(Edit edit, boolean dueToRebase) {
+    return new AutoValue_TaggedEdit(edit, dueToRebase);
+  }
+
+  abstract Edit edit();
+
+  abstract boolean dueToRebase();
+}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java
index f94f2c9..fb8fce1 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/GitModifiedFilesCacheKey.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.patch.gitdiff;
 
+import static com.google.gerrit.server.patch.DiffUtil.stringSize;
+
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
@@ -124,14 +126,4 @@
           .build();
     }
   }
-
-  private static int stringSize(String str) {
-    if (str != null) {
-      // each character in the string occupies 2 bytes. Ignoring the fixed overhead for the string
-      // (length, offset and hash code) since they are negligible and do not
-      // affect the comparison of 2 strings
-      return str.length() * 2;
-    }
-    return 0;
-  }
 }
diff --git a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
index 800bd41..9512094 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
@@ -79,10 +79,10 @@
     INSTANCE;
 
     private static final FieldDescriptor oldPathDescriptor =
-        ModifiedFileProto.getDescriptor().findFieldByName("old_path");
+        ModifiedFileProto.getDescriptor().findFieldByNumber(2);
 
     private static final FieldDescriptor newPathDescriptor =
-        ModifiedFileProto.getDescriptor().findFieldByName("new_path");
+        ModifiedFileProto.getDescriptor().findFieldByNumber(3);
 
     @Override
     public byte[] serialize(ModifiedFile modifiedFile) {
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/FileHeaderUtil.java b/java/com/google/gerrit/server/patch/gitfilediff/FileHeaderUtil.java
index 8f53751..7454f81 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/FileHeaderUtil.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/FileHeaderUtil.java
@@ -16,11 +16,14 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.PatchType;
 import java.util.Optional;
 import org.eclipse.jgit.patch.CombinedFileHeader;
 import org.eclipse.jgit.patch.FileHeader;
+import org.eclipse.jgit.util.IntList;
+import org.eclipse.jgit.util.RawParseUtils;
 
 /** A utility class for the {@link FileHeader} JGit object */
 public class FileHeaderUtil {
@@ -37,7 +40,7 @@
    */
   private static final int BIN_FILE_MAX_SCAN_LIMIT = 20000;
 
-  /** Converts the {@link FileHeader} parameter ot a String representation. */
+  /** Converts the {@link FileHeader} parameter to a String representation. */
   static String toString(FileHeader header) {
     return new String(FileHeaderUtil.toByteArray(header), UTF_8);
   }
@@ -54,12 +57,37 @@
     return buf;
   }
 
+  /** Splits the {@code FileHeader} string to a list of strings, one string per header line. */
+  public static ImmutableList<String> getHeaderLines(FileHeader fileHeader) {
+    String fileHeaderString = toString(fileHeader);
+    return getHeaderLines(fileHeaderString);
+  }
+
+  public static ImmutableList<String> getHeaderLines(String header) {
+    return getHeaderLines(header.getBytes(UTF_8));
+  }
+
+  static ImmutableList<String> getHeaderLines(byte[] header) {
+    final IntList lineStartOffsets = RawParseUtils.lineMap(header, 0, header.length);
+    final ImmutableList.Builder<String> headerLines =
+        ImmutableList.builderWithExpectedSize(lineStartOffsets.size() - 1);
+    for (int i = 1; i < lineStartOffsets.size() - 1; i++) {
+      final int b = lineStartOffsets.get(i);
+      int e = lineStartOffsets.get(i + 1);
+      if (header[e - 1] == '\n') {
+        e--;
+      }
+      headerLines.add(RawParseUtils.decode(UTF_8, header, b, e));
+    }
+    return headerLines.build();
+  }
+
   /**
    * Returns the old file path associated with the {@link FileHeader}, or empty if the file is
    * {@link com.google.gerrit.entities.Patch.ChangeType#ADDED} or {@link
    * com.google.gerrit.entities.Patch.ChangeType#REWRITE}.
    */
-  static Optional<String> getOldPath(FileHeader header) {
+  public static Optional<String> getOldPath(FileHeader header) {
     Patch.ChangeType changeType = getChangeType(header);
     switch (changeType) {
       case DELETED:
@@ -79,7 +107,7 @@
    * Returns the new file path associated with the {@link FileHeader}, or empty if the file is
    * {@link com.google.gerrit.entities.Patch.ChangeType#DELETED}.
    */
-  static Optional<String> getNewPath(FileHeader header) {
+  public static Optional<String> getNewPath(FileHeader header) {
     Patch.ChangeType changeType = getChangeType(header);
     switch (changeType) {
       case DELETED:
@@ -96,7 +124,7 @@
   }
 
   /** Returns the change type associated with the file header. */
-  static Patch.ChangeType getChangeType(FileHeader header) {
+  public static Patch.ChangeType getChangeType(FileHeader header) {
     // In Gerrit, we define our own entities  of the JGit entities, so that we have full control
     // over their behaviors (e.g. making sure that these entities are immutable so that we can add
     // them as fields of keys / values of persisted caches).
@@ -118,7 +146,7 @@
     }
   }
 
-  static PatchType getPatchType(FileHeader header) {
+  public static PatchType getPatchType(FileHeader header) {
     PatchType patchType;
 
     switch (header.getPatchType()) {
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
index bce3d44..6d63243 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.patch.gitfilediff;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.patch.DiffUtil.stringSize;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
@@ -167,16 +168,6 @@
     return result;
   }
 
-  private static int stringSize(String str) {
-    if (str != null) {
-      // each character in the string occupies two bytes. Ignoring the fixed overhead for the string
-      // (length, offset and hash code) since they are negligible and do not affect the comparison
-      // of two strings
-      return str.length() * 2;
-    }
-    return 0;
-  }
-
   public static Builder builder() {
     return new AutoValue_GitFileDiff.Builder();
   }
@@ -211,22 +202,22 @@
     INSTANCE;
 
     private static final FieldDescriptor OLD_PATH_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByName("old_path");
+        GitFileDiffProto.getDescriptor().findFieldByNumber(3);
 
     private static final FieldDescriptor NEW_PATH_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByName("new_path");
+        GitFileDiffProto.getDescriptor().findFieldByNumber(4);
 
     private static final FieldDescriptor OLD_MODE_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByName("old_mode");
+        GitFileDiffProto.getDescriptor().findFieldByNumber(7);
 
     private static final FieldDescriptor NEW_MODE_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByName("new_mode");
+        GitFileDiffProto.getDescriptor().findFieldByNumber(8);
 
     private static final FieldDescriptor CHANGE_TYPE_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByName("change_type");
+        GitFileDiffProto.getDescriptor().findFieldByNumber(9);
 
     private static final FieldDescriptor PATCH_TYPE_DESCRIPTOR =
-        GitFileDiffProto.getDescriptor().findFieldByName("patch_type");
+        GitFileDiffProto.getDescriptor().findFieldByNumber(10);
 
     @Override
     public byte[] serialize(GitFileDiff gitFileDiff) {
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheKey.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheKey.java
index d570ada..f104388 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheKey.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheKey.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.patch.gitfilediff;
 
+import static com.google.gerrit.server.patch.DiffUtil.stringSize;
+
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
@@ -60,16 +62,6 @@
         + 4; // whitespace
   }
 
-  private static int stringSize(String str) {
-    if (str != null) {
-      // each character in the string occupies 2 bytes. Ignoring the fixed overhead for the string
-      // (length, offset and hash code) since they are negligible and do not
-      // affect the comparison of 2 strings
-      return str.length() * 2;
-    }
-    return 0;
-  }
-
   public static Builder builder() {
     return new AutoValue_GitFileDiffCacheKey.Builder();
   }
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index ad4188f..dd00dca 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -619,6 +619,10 @@
     private boolean can(RefPermission perm) throws PermissionBackendException {
       switch (perm) {
         case READ:
+          /* Internal users such as plugin users should be able to read all refs. */
+          if (getUser().isInternalUser()) {
+            return true;
+          }
           if (refName.startsWith(Constants.R_TAGS)) {
             return isTagVisible();
           }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index e81428c..68a90d2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -660,7 +660,15 @@
     }
 
     if ("submittable".equalsIgnoreCase(value)) {
-      return new SubmittablePredicate(SubmitRecord.Status.OK);
+      // SubmittablePredicate will match if *any* of the submit records are OK,
+      // but we need to check that they're *all* OK, so check that none of the
+      // submit records match any of the negative cases. To avoid checking yet
+      // more negative cases for CLOSED and FORCED, instead make sure at least
+      // one submit record is OK.
+      return Predicate.and(
+          new SubmittablePredicate(SubmitRecord.Status.OK),
+          Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
+          Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
     }
 
     if ("ignored".equalsIgnoreCase(value)) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 370bc75..ed1f2f1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
@@ -33,10 +32,8 @@
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
 import com.google.gerrit.server.account.AccountLimits;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.change.PluginDefinedAttributesFactories;
-import com.google.gerrit.server.change.PluginDefinedAttributesFactory;
 import com.google.gerrit.server.change.PluginDefinedInfosFactory;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
@@ -60,7 +57,6 @@
 public class ChangeQueryProcessor extends QueryProcessor<ChangeData>
     implements DynamicOptions.BeanReceiver, DynamicOptions.BeanProvider, PluginDefinedInfosFactory {
   private final Provider<CurrentUser> userProvider;
-  private final ImmutableListMultimap<String, ChangeAttributeFactory> attributeFactoriesByPlugin;
   private final ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory;
   private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
   private final List<Extension<ChangePluginDefinedInfoFactory>>
@@ -81,7 +77,6 @@
       IndexConfig indexConfig,
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
-      DynamicSet<ChangeAttributeFactory> attributeFactories,
       ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory,
       DynamicSet<ChangePluginDefinedInfoFactory> changePluginDefinedInfoFactories) {
     super(
@@ -95,14 +90,6 @@
     this.userProvider = userProvider;
     this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
 
-    ImmutableListMultimap.Builder<String, ChangeAttributeFactory> factoriesBuilder =
-        ImmutableListMultimap.builder();
-    ImmutableListMultimap.Builder<String, ChangePluginDefinedInfoFactory> infosFactoriesBuilder =
-        ImmutableListMultimap.builder();
-    // Eagerly call Extension#get() rather than storing Extensions, since that method invokes the
-    // Provider on every call, which could be expensive if we invoke it once for every change.
-    attributeFactories.entries().forEach(e -> factoriesBuilder.put(e.getPluginName(), e.get()));
-    attributeFactoriesByPlugin = factoriesBuilder.build();
     changePluginDefinedInfoFactories
         .entries()
         .forEach(e -> changePluginDefinedInfoFactoriesByPlugin.add(e));
@@ -130,18 +117,6 @@
     return dynamicBeans.get(plugin);
   }
 
-  public PluginDefinedAttributesFactory getAttributesFactory() {
-    return this::buildPluginInfo;
-  }
-
-  private ImmutableList<PluginDefinedInfo> buildPluginInfo(ChangeData cd) {
-    return PluginDefinedAttributesFactories.createAll(
-        cd,
-        this,
-        attributeFactoriesByPlugin.entries().stream()
-            .map(e -> new Extension<>(e.getKey(), e::getValue)));
-  }
-
   public PluginDefinedInfosFactory getInfosFactory() {
     return this::createPluginDefinedInfos;
   }
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index b931457..4922b57 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -330,15 +330,9 @@
       eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
     }
 
-    c.plugins = queryProcessor.getAttributesFactory().create(d);
     List<PluginDefinedInfo> pluginInfos = pluginInfosByChange.get(d.getId());
     if (!pluginInfos.isEmpty()) {
-      if (c.plugins == null) {
-        c.plugins = pluginInfos;
-      } else {
-        c.plugins = new ArrayList<>(c.plugins);
-        c.plugins.addAll(pluginInfos);
-      }
+      c.plugins = pluginInfos;
     }
     return c;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index cc8ad47..02f39ab 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -123,7 +123,6 @@
           list = new ArrayList<>();
           out.put(o.path, list);
         }
-        o.path = null;
         list.add(o);
       }
 
@@ -133,10 +132,11 @@
         loader.fill();
       }
 
+      List<T> allComments = out.values().stream().flatMap(Collection::stream).collect(toList());
       if (fillCommentContext) {
-        List<T> allComments = out.values().stream().flatMap(Collection::stream).collect(toList());
         addCommentContext(allComments);
       }
+      allComments.forEach(c -> c.path = null); // we don't need path since it exists in the map keys
       return out;
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index d535d39..c392bd1 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -227,8 +227,7 @@
       throw new BadRequestException(
           "Cannot create a change on ref "
               + input.branch
-              + ". "
-              + "Allowed branches are refs/heads/*, refs/meta/dashboards/* or refs/meta/config");
+              + ". Gerrit internal refs and refs/tags/* are not allowed.");
     }
 
     String subject = Strings.nullToEmpty(input.subject);
@@ -299,11 +298,9 @@
     }
   }
 
+  /** Changes are allowed to be created on any ref that is not Gerrit internal or a tag ref. */
   private boolean isBranchAllowed(String branch) {
-    return branch.startsWith(RefNames.REFS_HEADS)
-        || branch.startsWith(RefNames.REFS_DASHBOARDS)
-        || "HEAD".equals(branch)
-        || RefNames.REFS_CONFIG.equals(branch);
+    return !RefNames.isGerritRef(branch) && !branch.startsWith(RefNames.REFS_TAGS);
   }
 
   private void checkRequiredPermissions(
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index 1ef3c4b..f28d2b3 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.restapi.change;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Streams;
 import com.google.gerrit.entities.Change;
@@ -27,7 +26,6 @@
 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.ChangeAttributeFactory;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.change.ChangeResource;
@@ -46,7 +44,6 @@
         DynamicOptions.BeanReceiver,
         DynamicOptions.BeanProvider {
   private final ChangeJson.Factory json;
-  private final DynamicSet<ChangeAttributeFactory> attrFactories;
   private final DynamicSet<ChangePluginDefinedInfoFactory> pdiFactories;
   private final EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class);
   private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
@@ -62,12 +59,8 @@
   }
 
   @Inject
-  GetChange(
-      ChangeJson.Factory json,
-      DynamicSet<ChangeAttributeFactory> attrFactories,
-      DynamicSet<ChangePluginDefinedInfoFactory> pdiFactories) {
+  GetChange(ChangeJson.Factory json, DynamicSet<ChangePluginDefinedInfoFactory> pdiFactories) {
     this.json = json;
-    this.attrFactories = attrFactories;
     this.pdiFactories = pdiFactories;
   }
 
@@ -91,12 +84,7 @@
   }
 
   private ChangeJson newChangeJson() {
-    return json.create(options, this::buildPluginInfo, this::createPluginDefinedInfos);
-  }
-
-  private ImmutableList<PluginDefinedInfo> buildPluginInfo(ChangeData cd) {
-    return PluginDefinedAttributesFactories.createAll(
-        cd, this, Streams.stream(attrFactories.entries()));
+    return json.create(options, this::createPluginDefinedInfos);
   }
 
   private ImmutableListMultimap<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index a66abaa..bf9990c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -181,6 +181,7 @@
   private final PluginSetContext<OnPostReview> onPostReviews;
   private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
   private final boolean strictLabels;
+  private final boolean publishPatchSetLevelComment;
 
   @Inject
   PostReview(
@@ -228,6 +229,8 @@
     this.onPostReviews = onPostReviews;
     this.replyAttentionSetUpdates = replyAttentionSetUpdates;
     this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
+    this.publishPatchSetLevelComment =
+        gerritConfig.getBoolean("event", "comment-added", "publishPatchSetLevelComment", true);
   }
 
   @Override
@@ -945,14 +948,23 @@
               String.format("Repository %s not found", ctx.getProject().get()), ex);
         }
       }
+      String comment = message.getMessage();
+      if (publishPatchSetLevelComment) {
+        // TODO(davido): Remove this workaround when patch set level comments are exposed in comment
+        // added event. For backwards compatibility, patchset level comment has a higher priority
+        // than change message and should be used as comment in comment added event.
+        if (in.comments != null && in.comments.containsKey(PATCHSET_LEVEL)) {
+          List<CommentInput> patchSetLevelComments = in.comments.get(PATCHSET_LEVEL);
+          if (patchSetLevelComments != null && !patchSetLevelComments.isEmpty()) {
+            CommentInput firstComment = patchSetLevelComments.get(0);
+            if (!Strings.isNullOrEmpty(firstComment.message)) {
+              comment = String.format("Patch Set %s:\n\n%s", psId.get(), firstComment.message);
+            }
+          }
+        }
+      }
       commentAdded.fire(
-          notes.getChange(),
-          ps,
-          user.state(),
-          message.getMessage(),
-          approvals,
-          oldApprovals,
-          ctx.getWhen());
+          notes.getChange(), ps, user.state(), comment, approvals, oldApprovals, ctx.getWhen());
     }
 
     private boolean insertComments(ChangeContext ctx, List<RobotComment> newRobotComments)
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
index ed6c0a5..4acf809 100644
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
@@ -42,7 +42,6 @@
 import com.google.gerrit.server.update.UpdateException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Collection;
@@ -56,7 +55,6 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.kohsuke.args4j.Option;
 
-@Singleton
 public class PreviewSubmit implements RestReadView<RevisionResource> {
   private static final int MAX_DEFAULT_BUNDLE_SIZE = 100 * 1024 * 1024;
 
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 0fec476..cf0d4cf 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -27,8 +27,11 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -49,10 +52,13 @@
   private final ChangeQueryBuilder qb;
   private final Provider<ChangeQueryProcessor> queryProcessorProvider;
   private final HashMap<String, DynamicOptions.DynamicBean> dynamicBeans = new HashMap<>();
+  private final Provider<CurrentUser> userProvider;
+  private final PermissionBackend permissionBackend;
   private EnumSet<ListChangesOption> options;
   private Integer limit;
   private Integer start;
   private Boolean noLimit;
+  private Boolean skipVisibility;
 
   @Option(
       name = "--query",
@@ -94,6 +100,15 @@
     this.noLimit = on;
   }
 
+  @Option(name = "--skip-visibility", usage = "Skip visibility check, only for administrators")
+  public void skipVisibility(boolean on) throws AuthException, PermissionBackendException {
+    if (on) {
+      CurrentUser user = userProvider.get();
+      permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+    }
+    skipVisibility = on;
+  }
+
   @Override
   public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
     dynamicBeans.put(plugin, dynamicBean);
@@ -103,10 +118,14 @@
   QueryChanges(
       ChangeJson.Factory json,
       ChangeQueryBuilder qb,
-      Provider<ChangeQueryProcessor> queryProcessorProvider) {
+      Provider<ChangeQueryProcessor> queryProcessorProvider,
+      Provider<CurrentUser> userProvider,
+      PermissionBackend permissionBackend) {
     this.json = json;
     this.qb = qb;
     this.queryProcessorProvider = queryProcessorProvider;
+    this.userProvider = userProvider;
+    this.permissionBackend = permissionBackend;
 
     options = EnumSet.noneOf(ListChangesOption.class);
   }
@@ -152,6 +171,9 @@
     if (noLimit != null) {
       queryProcessor.setNoLimit(noLimit);
     }
+    if (skipVisibility != null) {
+      queryProcessor.enforceVisibility(!skipVisibility);
+    }
     dynamicBeans.forEach((p, b) -> queryProcessor.setDynamicBean(p, b));
 
     if (queries == null || queries.isEmpty()) {
@@ -165,9 +187,7 @@
     int cnt = queries.size();
     List<QueryResult<ChangeData>> results = queryProcessor.query(qb.parse(queries));
     List<List<ChangeInfo>> res =
-        json.create(
-                options, queryProcessor.getAttributesFactory(), queryProcessor.getInfosFactory())
-            .format(results);
+        json.create(options, queryProcessor.getInfosFactory()).format(results);
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
       if (results.get(n).more() && !info.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
index dbcd8c9..59efd06 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -59,8 +59,8 @@
       throw new AuthException("Authentication required");
     }
 
-    if (!Strings.isNullOrEmpty(input.project)) {
-      throw new BadRequestException("may not specify project");
+    if (!Strings.isNullOrEmpty(input.project) && !rsrc.getName().equals(input.project)) {
+      throw new BadRequestException("project must match URL");
     }
 
     input.project = rsrc.getName();
diff --git a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
index e9a0d7f..34d6696 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateDashboard.java
@@ -26,11 +26,9 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 import java.io.IOException;
 import org.kohsuke.args4j.Option;
 
-@Singleton
 public class CreateDashboard
     implements RestCollectionCreateView<ProjectResource, DashboardResource, SetDashboardInput> {
   private final Provider<SetDefaultDashboard> setDefault;
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 2db625b..409c808 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -481,7 +481,7 @@
                   if (!traceContext.isTracing()) {
                     String traceId = "retry-on-failure-" + new RequestId();
                     traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
-                    logger.atFine().withCause(t).log(
+                    logger.atWarning().withCause(t).log(
                         "AutoRetry: %s failed, retry with tracing enabled (cause = %s)",
                         actionName, cause);
                     opts.onAutoTrace().ifPresent(c -> c.accept(traceId));
@@ -492,7 +492,7 @@
                   // A non-recoverable failure occurred. We retried the operation with tracing
                   // enabled and it failed again. Log the failure so that admin can see if it
                   // differs from the failure that triggered the retry.
-                  logger.atFine().withCause(t).log(
+                  logger.atWarning().withCause(t).log(
                       "AutoRetry: auto-retry of %s has failed (cause = %s)", actionName, cause);
                   metrics.failuresOnAutoRetryCount.increment(actionType, actionName, cause);
                   return false;
@@ -504,7 +504,7 @@
       return executeWithTimeoutCount(actionType, action, opts, retryerBuilder.build(), listener);
     } finally {
       if (listener.getAttemptCount() > 1) {
-        logger.atFine().log("%s was attempted %d times", actionType, listener.getAttemptCount());
+        logger.atWarning().log("%s was attempted %d times", actionType, listener.getAttemptCount());
         metrics.attemptCounts.incrementBy(
             actionType,
             opts.actionName().orElse("N/A"),
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
index fd9af0e..eb5b9b0 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.config.FactoryModule;
@@ -33,7 +35,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
 import org.junit.Test;
 
 public class ChangeSubmitRequirementIT extends AbstractDaemonTest {
@@ -93,20 +94,113 @@
     assertThat(result.get(0).requirements).containsExactly(reqInfo);
   }
 
+  @Test
+  public void submittableQueryRuleNotReady() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    // The custom rule is NOT_READY.
+    rule.block(true);
+    change.index();
+
+    assertThat(queryIsSubmittable()).isEmpty();
+  }
+
+  @Test
+  public void submittableQueryRuleError() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    rule.status(Optional.of(SubmitRecord.Status.RULE_ERROR));
+    change.index();
+
+    assertThat(queryIsSubmittable()).isEmpty();
+  }
+
+  @Test
+  public void submittableQueryDefaultRejected() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // CodeReview:-2 the change, causing the default rule to fail.
+    rejectChange(change);
+
+    rule.status(Optional.of(SubmitRecord.Status.OK));
+    change.index();
+
+    assertThat(queryIsSubmittable()).isEmpty();
+  }
+
+  @Test
+  public void submittableQueryRuleOk() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    rule.status(Optional.of(SubmitRecord.Status.OK));
+    change.index();
+
+    List<ChangeInfo> result = queryIsSubmittable();
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).changeId).isEqualTo(change.info().changeId);
+  }
+
+  @Test
+  public void submittableQueryRuleNoRecord() throws Exception {
+    ChangeApi change = newChangeApi();
+
+    // Satisfy the default rule.
+    approveChange(change);
+
+    // Our custom rule isn't providing any submit records.
+    rule.status(Optional.empty());
+    change.index();
+
+    // is:submittable should return the change, since it was approved and the custom rule is not
+    // blocking it.
+    List<ChangeInfo> result = queryIsSubmittable();
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).changeId).isEqualTo(change.info().changeId);
+  }
+
+  private List<ChangeInfo> queryIsSubmittable() throws Exception {
+    return gApi.changes().query("is:submittable project:" + project.get()).get();
+  }
+
+  private ChangeApi newChangeApi() throws Exception {
+    return gApi.changes().id(createChange().getChangeId());
+  }
+
+  private void approveChange(ChangeApi changeApi) throws Exception {
+    changeApi.current().review(ReviewInput.approve());
+  }
+
+  private void rejectChange(ChangeApi changeApi) throws Exception {
+    changeApi.current().review(ReviewInput.reject());
+  }
+
   @Singleton
   private static class CustomSubmitRule implements SubmitRule {
-    private final AtomicBoolean block = new AtomicBoolean(true);
+    private Optional<SubmitRecord.Status> recordStatus = Optional.empty();
 
     public void block(boolean block) {
-      this.block.set(block);
+      this.status(block ? Optional.of(SubmitRecord.Status.NOT_READY) : Optional.empty());
+    }
+
+    public void status(Optional<SubmitRecord.Status> status) {
+      this.recordStatus = status;
     }
 
     @Override
     public Optional<SubmitRecord> evaluate(ChangeData changeData) {
-      if (block.get()) {
+      if (this.recordStatus.isPresent()) {
         SubmitRecord record = new SubmitRecord();
         record.labels = new ArrayList<>();
-        record.status = SubmitRecord.Status.NOT_READY;
+        record.status = this.recordStatus.get();
         record.requirements = ImmutableList.of(req);
         return Optional.of(record);
       }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
index 31198d5..cebce0b 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
@@ -16,9 +16,6 @@
 
 import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
-import com.google.inject.AbstractModule;
 import java.util.Arrays;
 import org.junit.Test;
 
@@ -27,30 +24,6 @@
   // No tests for /detail via the extension API, since the extension API doesn't have that method.
 
   @Test
-  public void queryChangeWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(
-        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
-  }
-
-  @Test
-  public void getChangeWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(
-        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
-  }
-
-  @Test
-  public void queryChangeWithSimpleAttribute() throws Exception {
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
-  }
-
-  @Test
-  public void getChangeWithSimpleAttribute() throws Exception {
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
-  }
-
-  @Test
   public void querySingleChangeWithBulkAttribute() throws Exception {
     getSingleChangeWithPluginDefinedBulkAttribute(
         id -> pluginInfosFromChangeInfos(gApi.changes().query(id.toString()).get()));
@@ -63,22 +36,6 @@
   }
 
   @Test
-  public void queryChangeWithOption() throws Exception {
-    getChangeWithOption(
-        id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()),
-        (id, opts) ->
-            pluginInfoFromSingletonList(
-                gApi.changes().query(id.toString()).withPluginOptions(opts).get()));
-  }
-
-  @Test
-  public void getChangeWithOption() throws Exception {
-    getChangeWithOption(
-        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get()),
-        (id, opts) -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get(opts)));
-  }
-
-  @Test
   public void queryChangeWithOptionBulkAttribute() throws Exception {
     getChangeWithPluginDefinedBulkAttributeOption(
         id -> pluginInfosFromChangeInfos(gApi.changes().query(id.toString()).get()),
@@ -108,12 +65,6 @@
   }
 
   @Test
-  public void getMultipleChangesWithPluginDefinedAndChangeAttributes() throws Exception {
-    getMultipleChangesWithPluginDefinedBulkAndChangeAttributes(
-        () -> pluginInfosFromChangeInfos(gApi.changes().query("status:open").get()));
-  }
-
-  @Test
   public void getMultipleChangesWithPluginDefinedAttributeInSingleCall() throws Exception {
     getMultipleChangesWithPluginDefinedBulkAttributeInSingleCall(
         () -> pluginInfosFromChangeInfos(gApi.changes().query("status:open").get()));
@@ -124,23 +75,4 @@
     getChangeWithPluginDefinedBulkAttributeWithException(
         id -> pluginInfosFromChangeInfos(Arrays.asList(gApi.changes().id(id.get()).get())));
   }
-
-  static class SimpleAttributeWithExplicitExportModule extends AbstractModule {
-    @Override
-    public void configure() {
-      bind(ChangeAttributeFactory.class)
-          .annotatedWith(Exports.named("simple"))
-          .toInstance((cd, bp, p) -> new MyInfo("change " + cd.getId()));
-    }
-  }
-
-  @Test
-  public void getChangeWithSimpleAttributeWithExplicitExport() throws Exception {
-    // For backwards compatibility with old plugins, allow modules to bind into the
-    // DynamicSet<ChangeAttributeFactory> as if it were a DynamicMap. We only need one variant of
-    // this test to prove that the mapping works.
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()),
-        SimpleAttributeWithExplicitExportModule.class);
-  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
index 33ec556..14704ad 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/QueryChangesIT.java
@@ -25,21 +25,27 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.Arrays;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -50,6 +56,7 @@
   @Inject private AccountOperations accountOperations;
   @Inject private ProjectOperations projectOperations;
   @Inject private Provider<QueryChanges> queryChangesProvider;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
   @SuppressWarnings("unchecked")
@@ -283,9 +290,91 @@
     assertThat(result.get(1).get(0)._number).isEqualTo(numericId2);
   }
 
+  @Test
+  public void skipVisibility_rejectedForNonAdmin() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    final QueryChanges queryChanges = queryChangesProvider.get();
+    String query = "is:open repo:" + project.get();
+    queryChanges.addQuery(query);
+    AuthException thrown =
+        assertThrows(AuthException.class, () -> queryChanges.skipVisibility(true));
+    assertThat(thrown).hasMessageThat().isEqualTo("administrate server not permitted");
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void skipVisibility_noReadPermission() throws Exception {
+    createChange().getChangeId();
+    requestScopeOperations.setApiUser(admin.id());
+    QueryChanges queryChanges = queryChangesProvider.get();
+
+    queryChanges.addQuery("is:open repo:" + project.get());
+    List<List<ChangeInfo>> result =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result).hasSize(1);
+
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      ProjectConfig cfg = u.getConfig();
+      removeAllBranchPermissions(cfg, Permission.READ);
+      u.save();
+    }
+
+    queryChanges = queryChangesProvider.get();
+    queryChanges.addQuery("is:open repo:" + project.get());
+    List<List<ChangeInfo>> result2 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result2).hasSize(0);
+
+    queryChanges = queryChangesProvider.get();
+    queryChanges.addQuery("is:open repo:" + project.get());
+    queryChanges.skipVisibility(true);
+    List<List<ChangeInfo>> result3 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result3).hasSize(1);
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void skipVisibility_privateChange() throws Exception {
+    TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+    PushOneCommit.Result result =
+        pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
+    requestScopeOperations.setApiUser(user.id());
+    gApi.changes().id(result.getChangeId()).setPrivate(true);
+
+    requestScopeOperations.setApiUser(admin.id());
+    QueryChanges queryChanges = queryChangesProvider.get();
+
+    queryChanges.addQuery("is:open repo:" + project.get());
+    List<List<ChangeInfo>> result2 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result2).hasSize(0);
+
+    queryChanges = queryChangesProvider.get();
+    queryChanges.addQuery("is:open repo:" + project.get());
+    queryChanges.skipVisibility(true);
+    List<List<ChangeInfo>> result3 =
+        (List<List<ChangeInfo>>) queryChanges.apply(TopLevelResource.INSTANCE).value();
+    assertThat(result3).hasSize(1);
+  }
+
   private static void assertNoChangeHasMoreChangesSet(List<ChangeInfo> results) {
     for (ChangeInfo info : results) {
       assertThat(info._moreChanges).isNull();
     }
   }
+
+  private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
+    for (AccessSection s : ImmutableList.copyOf(cfg.getAccessSections())) {
+      if (s.getName().startsWith("refs/heads/")
+          || s.getName().startsWith("refs/for/")
+          || s.getName().equals("refs/*")) {
+        cfg.upsertAccessSection(
+            s.getName(),
+            updatedSection -> {
+              Arrays.stream(permissions).forEach(p -> updatedSection.remove(Permission.builder(p)));
+            });
+      }
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 39366bd..ff785a2 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -2745,6 +2745,55 @@
   }
 
   @Test
+  public void symlinkConveredToRegularFileIsIdentifiedAsDeleted() throws Exception {
+    // TODO(ghareeb): See https://bugs.chromium.org/p/gerrit/issues/detail?id=13914.
+    // This test creates a corner scenario of replacing a symlink with a regular file
+    // of the same name. When both patchsets are diffed, the List Files endpoint identifies the
+    // file as a 'REWRITE', however the diff endpoint for the symlink file identifies the file as
+    // deleted. This case is a bit risky since it hides from the user the new content that was added
+    // in the new regular file. Ideally, the diff endpoint should show two entries for the deleted
+    // symlink and the added file, or only one entry "REWRITE" with the content that was added to
+    // the new file.
+    String target = "file.txt";
+    String symlink = "link.lnk";
+
+    // Create a change adding file "FileName" and a symlink "symLink" pointing to the file
+    PushOneCommit push =
+        pushFactory
+            .create(admin.newIdent(), testRepo, "Commit Subject", target, "content")
+            .addSymlink(symlink, target);
+
+    PushOneCommit.Result result = push.to("refs/for/master");
+    String initialRev = gApi.changes().id(result.getChangeId()).get().currentRevision;
+
+    // Delete the symlink with patchset 2
+    gApi.changes().id(result.getChangeId()).edit().deleteFile(symlink);
+    gApi.changes().id(result.getChangeId()).edit().publish();
+
+    // Re-add the symlink as a regular file with patchset 3
+    gApi.changes()
+        .id(result.getChangeId())
+        .edit()
+        .modifyFile(symlink, RawInputUtil.create("Content of the new file named 'symlink'"));
+    gApi.changes().id(result.getChangeId()).edit().publish();
+
+    Map<String, FileInfo> changedFiles =
+        gApi.changes().id(result.getChangeId()).current().files(initialRev);
+
+    assertThat(changedFiles.keySet()).containsExactly("/COMMIT_MSG", symlink);
+    assertThat(changedFiles.get(symlink).status).isEqualTo('W'); // Rewrite
+
+    DiffInfo diffInfo =
+        gApi.changes().id(result.getChangeId()).current().file(symlink).diff(initialRev);
+
+    // TODO(ghareeb): This is not a desired behaviour. The diff endpoint treats the file as
+    // 'DELETED', hence hiding any new content that was added to the new regular file. It is better
+    // to show the file as 'ADDED'. See https://bugs.chromium.org/p/gerrit/issues/detail?id=13914
+    // for more details.
+    assertThat(diffInfo.changeType).isEqualTo(ChangeType.DELETED);
+  }
+
+  @Test
   public void diffOfNonExistentFileIsAnEmptyDiffResult() throws Exception {
     addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 22a7e03..8e31e904 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -1622,7 +1622,7 @@
         new PatchSetWebLink() {
           @Override
           public WebLinkInfo getPatchSetWebLink(
-              String projectName, String commit, String subject, String branchName) {
+              String projectName, String commit, String commitMessage, String branchName) {
             return expectedWebLinkInfo;
           }
         };
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
index b447534..16dc294 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginProvidedRootRestApiBindingsIT.java
@@ -16,6 +16,7 @@
 
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -48,6 +49,7 @@
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Test;
+import org.kohsuke.args4j.Option;
 
 /**
  * Tests for checking plugin-provided REST API bindings directly under {@code /}.
@@ -192,8 +194,15 @@
 
   @Singleton
   static class TestGet implements RestReadView<TestPluginResource> {
+
+    @Option(name = "--crash")
+    String crash;
+
     @Override
     public Response<String> apply(TestPluginResource resource) throws Exception {
+      if (!Strings.nullToEmpty(crash).isEmpty()) {
+        throw new IllegalStateException();
+      }
       return Response.ok("test");
     }
   }
@@ -204,4 +213,13 @@
       RestApiCallHelper.execute(adminRestSession, TEST_CALLS.asList());
     }
   }
+
+  @Test
+  public void testOptionOnSingletonIsIgnored() throws Exception {
+    try (AutoCloseable ignored = installPlugin(PLUGIN_NAME, null, MyPluginHttpModule.class, null)) {
+      RestApiCallHelper.execute(
+          adminRestSession,
+          RestCall.get("/plugins/" + PLUGIN_NAME + "/test-collection/1/detail?crash=xyz"));
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index ce21477..a539bd5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -182,13 +182,7 @@
   }
 
   @Test
-  public void cannotCreateChangeOnNoteDbRefs() throws Exception {
-    String[] disallowedBranches = {
-      "refs/users/82/1000002",
-      "refs/tags/v2.1",
-      "refs/cache-automerge/ec/00000000000000000000000000000000000000"
-    };
-
+  public void cannotCreateChangeOnGerritInternalRefs() throws Exception {
     requestScopeOperations.setApiUser(admin.id());
     projectOperations
         .project(project)
@@ -196,21 +190,41 @@
         .add(allow(CREATE).ref("refs/*").group(REGISTERED_USERS))
         .update();
 
-    for (String branchName : disallowedBranches) {
-      requestScopeOperations.setApiUser(admin.id());
-      BranchNameKey branchNameKey = BranchNameKey.create(project, branchName);
-      createBranch(branchNameKey);
+    String disallowedRef = "refs/changes/00/1000"; // All Gerrit internal refs behave the same way
+    requestScopeOperations.setApiUser(admin.id());
+    BranchNameKey branchNameKey = BranchNameKey.create(project, disallowedRef);
+    createBranch(branchNameKey);
 
-      requestScopeOperations.setApiUser(user.id());
-      ChangeInput ci = newChangeInput(ChangeStatus.NEW);
-      ci.subject = "Subject";
-      ci.branch = branchName;
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "Subject";
+    ci.branch = disallowedRef;
 
-      Throwable thrown = assertThrows(RestApiException.class, () -> gApi.changes().create(ci));
-      assertThat(thrown)
-          .hasMessageThat()
-          .contains("Cannot create a change on ref " + ci.branch + ". ");
-    }
+    Throwable thrown = assertThrows(RestApiException.class, () -> gApi.changes().create(ci));
+    assertThat(thrown).hasMessageThat().contains("Cannot create a change on ref " + ci.branch);
+  }
+
+  @Test
+  public void cannotCreateChangeOnTagRefs() throws Exception {
+    requestScopeOperations.setApiUser(admin.id());
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(CREATE).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    String branchName = "refs/tags/v1.0";
+    requestScopeOperations.setApiUser(admin.id());
+    BranchNameKey branchNameKey = BranchNameKey.create(project, branchName);
+    createBranch(branchNameKey);
+
+    requestScopeOperations.setApiUser(user.id());
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "Subject";
+    ci.branch = branchName;
+
+    Throwable thrown = assertThrows(RestApiException.class, () -> gApi.changes().create(ci));
+    assertThat(thrown).hasMessageThat().contains("Cannot create a change on ref " + ci.branch);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
index 17bf37e..a4ec40e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.acceptance.AbstractPluginFieldsTest;
@@ -35,41 +33,6 @@
   private static final Gson GSON = OutputFormat.JSON.newGson();
 
   @Test
-  public void queryChangeWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(
-        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
-  }
-
-  @Test
-  public void getChangeWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
-  }
-
-  @Test
-  public void getChangeDetailWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(
-        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
-  }
-
-  @Test
-  public void queryChangeWithSimpleAttribute() throws Exception {
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
-  }
-
-  @Test
-  public void getChangeWithSimpleAttribute() throws Exception {
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
-  }
-
-  @Test
-  public void getChangeDetailWithSimpleAttribute() throws Exception {
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
-  }
-
-  @Test
   public void querySingleChangeWithBulkAttribute() throws Exception {
     getSingleChangeWithPluginDefinedBulkAttribute(
         id -> pluginInfosFromChangeInfos(adminRestSession.get(changeQueryUrl(id))));
@@ -88,27 +51,6 @@
   }
 
   @Test
-  public void queryChangeWithOption() throws Exception {
-    getChangeWithOption(
-        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))),
-        (id, opts) -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id, opts))));
-  }
-
-  @Test
-  public void getChangeWithOption() throws Exception {
-    getChangeWithOption(
-        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))),
-        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id, opts))));
-  }
-
-  @Test
-  public void getChangeDetailWithOption() throws Exception {
-    getChangeWithOption(
-        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))),
-        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id, opts))));
-  }
-
-  @Test
   public void pluginDefinedQueryChangeWithOption() throws Exception {
     getChangeWithPluginDefinedBulkAttributeOption(
         id -> pluginInfosFromChangeInfos(adminRestSession.get(changeQueryUrl(id))),
@@ -142,12 +84,6 @@
   }
 
   @Test
-  public void getMultipleChangesWithPluginDefinedAndChangeAttributes() throws Exception {
-    getMultipleChangesWithPluginDefinedBulkAndChangeAttributes(
-        () -> pluginInfosFromChangeInfos(adminRestSession.get("/changes/?q=status:open")));
-  }
-
-  @Test
   public void getMultipleChangesWithPluginDefinedAttributeInSingleCall() throws Exception {
     getMultipleChangesWithPluginDefinedBulkAttributeInSingleCall(
         () -> pluginInfosFromChangeInfos(adminRestSession.get("/changes/?q=status:open")));
@@ -204,24 +140,6 @@
   }
 
   @Nullable
-  private static List<PluginDefinedInfo> pluginInfoFromSingletonList(RestResponse res)
-      throws Exception {
-    res.assertOK();
-    List<Map<String, Object>> changeInfos =
-        GSON.fromJson(res.getReader(), new TypeToken<List<Map<String, Object>>>() {}.getType());
-    assertThat(changeInfos).hasSize(1);
-    return decodeRawPluginsList(GSON, changeInfos.get(0).get("plugins"));
-  }
-
-  @Nullable
-  private List<PluginDefinedInfo> pluginInfoFromChangeInfo(RestResponse res) throws Exception {
-    res.assertOK();
-    Map<String, Object> changeInfo =
-        GSON.fromJson(res.getReader(), new TypeToken<Map<String, Object>>() {}.getType());
-    return decodeRawPluginsList(GSON, changeInfo.get("plugins"));
-  }
-
-  @Nullable
   private Map<Change.Id, List<PluginDefinedInfo>> pluginInfoMapFromChangeInfo(RestResponse res)
       throws Exception {
     res.assertOK();
diff --git a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index 002b860..fb3259f 100644
--- a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -201,6 +202,25 @@
       assertThat(attr.value).isEqualTo(-1);
       assertThat(listener.getLastCommentAddedEvent().getComment())
           .isEqualTo(String.format("Patch Set 1:\n\n%s", label.getName()));
+
+      // review with patch set level comment
+      reviewInput = new ReviewInput().patchSetLevelComment("a patch set level comment");
+      revision(r).review(reviewInput);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1:\n\n%s", "a patch set level comment"));
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "event.comment-added.publishPatchSetLevelComment", value = "false")
+  public void publishPatchSetLevelComment() throws Exception {
+    PushOneCommit.Result r = createChange();
+    TestListener listener = new TestListener();
+    try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+      ReviewInput reviewInput = new ReviewInput().patchSetLevelComment("a patch set level comment");
+      revision(r).review(reviewInput);
+      assertThat(listener.getLastCommentAddedEvent().getComment())
+          .isEqualTo(String.format("Patch Set 1:\n\n%s", "(1 comment)"));
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
index fc7ab2f..9e4907c 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ExternalUser;
+import com.google.gerrit.server.PropertyMap;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -47,7 +49,6 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.change.ExternalUser;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
 import java.util.Collection;
@@ -293,6 +294,8 @@
 
   ExternalUser createUserInGroup(String userId, String groupId) {
     return externalUserFactory.create(
-        ImmutableSet.of(ExternalId.Key.parse("company-auth:" + groupId + "-" + userId)));
+        ImmutableSet.of(),
+        ImmutableSet.of(ExternalId.Key.parse("company-auth:" + groupId + "-" + userId)),
+        PropertyMap.EMPTY);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
index 38293f9..009e05d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.ssh;
 
-import static com.google.common.truth.Truth.assertThat;
 import static java.util.stream.Collectors.joining;
 
 import com.google.common.collect.ImmutableListMultimap;
@@ -41,31 +40,12 @@
   private static final Gson GSON = OutputStreamQuery.GSON;
 
   @Test
-  public void queryChangeWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(
-        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
-  }
-
-  @Test
-  public void queryChangeWithSimpleAttribute() throws Exception {
-    getChangeWithSimpleAttribute(
-        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
-  }
-
-  @Test
   public void querySingleChangeWithBulkAttribute() throws Exception {
     getSingleChangeWithPluginDefinedBulkAttribute(
         id -> pluginInfosFromList(adminSshSession.exec(changeQueryCmd(id))));
   }
 
   @Test
-  public void queryChangeWithOption() throws Exception {
-    getChangeWithOption(
-        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))),
-        (id, opts) -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id, opts))));
-  }
-
-  @Test
   public void queryPluginDefinedAttributeChangeWithOption() throws Exception {
     getChangeWithPluginDefinedBulkAttributeOption(
         id -> pluginInfosFromList(adminSshSession.exec(changeQueryCmd(id))),
@@ -85,12 +65,6 @@
   }
 
   @Test
-  public void getMultipleChangesWithPluginDefinedAndChangeAttributes() throws Exception {
-    getMultipleChangesWithPluginDefinedBulkAndChangeAttributes(
-        () -> pluginInfosFromList(adminSshSession.exec("gerrit query --format json status:open")));
-  }
-
-  @Test
   public void getMultipleChangesWithPluginDefinedAttributeInSingleCall() throws Exception {
     getMultipleChangesWithPluginDefinedBulkAttributeInSingleCall(
         () -> pluginInfosFromList(adminSshSession.exec("gerrit query --format json status:open")));
@@ -116,15 +90,6 @@
   }
 
   @Nullable
-  private static List<PluginDefinedInfo> pluginInfoFromSingletonList(String sshOutput)
-      throws Exception {
-    List<Map<String, Object>> changeAttrs = getChangeAttrs(sshOutput);
-
-    assertThat(changeAttrs).hasSize(1);
-    return decodeRawPluginsList(GSON, changeAttrs.get(0).get("plugins"));
-  }
-
-  @Nullable
   private static Map<Change.Id, List<PluginDefinedInfo>> pluginInfosFromList(String sshOutput)
       throws Exception {
     List<Map<String, Object>> changeAttrs = getChangeAttrs(sshOutput);
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index 77ab58b..ba9475f 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.joining;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -53,8 +54,15 @@
     String testCanonicalUrl = "foo-url";
     String testCdnPath = "bar-cdn";
     String testFaviconURL = "zaz-url";
+
+    String disabledDefault = IndexHtmlUtil.DEFAULT_EXPERIMENTS.asList().get(0);
+    org.eclipse.jgit.lib.Config serverConfig = new org.eclipse.jgit.lib.Config();
+    serverConfig.setStringList(
+        "experiments", null, "enabled", ImmutableList.of("NewFeature", "DisabledFeature"));
+    serverConfig.setStringList(
+        "experiments", null, "disabled", ImmutableList.of("DisabledFeature", disabledDefault));
     IndexServlet servlet =
-        new IndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL, gerritApi);
+        new IndexServlet(testCanonicalUrl, testCdnPath, testFaviconURL, gerritApi, serverConfig);
 
     FakeHttpServletResponse response = new FakeHttpServletResponse();
 
@@ -76,6 +84,15 @@
                 + "'\\x7b\\x22\\/config\\/server\\/version\\x22: \\x22123\\x22, "
                 + "\\x22\\/config\\/server\\/info\\x22: \\x7b\\x22default_theme\\x22:"
                 + "\\x22my-default-theme\\x22\\x7d, \\x22\\/config\\/server\\/top-menus\\x22: "
-                + "\\x5b\\x5d\\x7d');</script>");
+                + "\\x5b\\x5d\\x7d');");
+    String enabledDefaults =
+        IndexHtmlUtil.DEFAULT_EXPERIMENTS.stream()
+            .filter(e -> !e.equals(disabledDefault))
+            .collect(joining("\\x22,"));
+    assertThat(output)
+        .contains(
+            "window.ENABLED_EXPERIMENTS = JSON.parse('\\x5b\\x22NewFeature\\x22,\\x22"
+                + enabledDefaults
+                + "\\x22\\x5d');</script>");
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffCacheKeySerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffCacheKeySerializerTest.java
new file mode 100644
index 0000000..ecab07d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffCacheKeySerializerTest.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.server.patch.filediff.FileDiffCacheKey;
+import com.google.gerrit.server.patch.gitfilediff.GitFileDiffCacheImpl.DiffAlgorithm;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+/** Tests the protobuf serializer for the {@link FileDiffCacheKey}. */
+public class FileDiffCacheKeySerializerTest {
+  private static final ObjectId COMMIT_ID_1 =
+      ObjectId.fromString("123e9fa8a286255ac7d5ba11b598892735758391");
+  private static final ObjectId COMMIT_ID_2 =
+      ObjectId.fromString("d07a03a9818c120301cb5b4a969b035479400b5f");
+
+  @Test
+  public void roundTrip() {
+    FileDiffCacheKey key =
+        FileDiffCacheKey.builder()
+            .project(Project.nameKey("project/x"))
+            .oldCommit(COMMIT_ID_1)
+            .newCommit(COMMIT_ID_2)
+            .newFilePath("some_file.txt")
+            .renameScore(65)
+            .diffAlgorithm(DiffAlgorithm.HISTOGRAM)
+            .whitespace(Whitespace.IGNORE_ALL)
+            .build();
+
+    byte[] serialized = FileDiffCacheKey.Serializer.INSTANCE.serialize(key);
+
+    assertThat(FileDiffCacheKey.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(key);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
new file mode 100644
index 0000000..4307954
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/FileDiffOutputSerializerTest.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.entities.Patch.PatchType;
+import com.google.gerrit.server.patch.filediff.Edit;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.filediff.TaggedEdit;
+import java.util.Optional;
+import org.junit.Test;
+
+public class FileDiffOutputSerializerTest {
+  @Test
+  public void roundTrip() {
+    ImmutableList<TaggedEdit> edits =
+        ImmutableList.of(
+            TaggedEdit.create(Edit.create(1, 5, 3, 4), true),
+            TaggedEdit.create(Edit.create(21, 30, 150, 158), false));
+
+    FileDiffOutput fileDiff =
+        FileDiffOutput.builder()
+            .oldPath(Optional.of("old_file_path.txt"))
+            .newPath(Optional.empty())
+            .changeType(Optional.of(ChangeType.DELETED))
+            .patchType(Optional.of(PatchType.UNIFIED))
+            .size(23)
+            .sizeDelta(10)
+            .headerLines(ImmutableList.of("header line 1", "header line 2"))
+            .edits(edits)
+            .build();
+
+    byte[] serialized = FileDiffOutput.Serializer.INSTANCE.serialize(fileDiff);
+    assertThat(FileDiffOutput.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(fileDiff);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 65196bf..87db21f 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -313,6 +314,11 @@
   }
 
   @Test
+  public void userRefIsVisibleForInternalUser() throws Exception {
+    internalUser(localKey).controlForRef("refs/users/default").asForRef().check(RefPermission.READ);
+  }
+
+  @Test
   public void branchDelegation1() throws Exception {
     projectOperations
         .project(localKey)
@@ -1220,6 +1226,10 @@
     return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
   }
 
+  private ProjectControl internalUser(Project.NameKey localKey) throws Exception {
+    return projectControlFactory.create(new InternalUser(), getProjectState(localKey));
+  }
+
   private ProjectControl user(Project.NameKey localKey, AccountGroup.UUID... memberOf)
       throws Exception {
     return user(localKey, null, memberOf);
diff --git a/package.json b/package.json
index 35544c2..91b8042 100644
--- a/package.json
+++ b/package.json
@@ -8,18 +8,20 @@
     "@bazel/typescript": "^3.0.0-rc.1"
   },
   "devDependencies": {
-    "eslint": "^6.6.0",
-    "eslint-config-google": "^0.13.0",
-    "eslint-plugin-html": "^6.0.0",
-    "eslint-plugin-import": "^2.20.1",
-    "eslint-plugin-jsdoc": "^19.2.0",
-    "eslint-plugin-prettier": "^3.1.3",
-    "gts": "^2.0.2",
+    "@typescript-eslint/eslint-plugin": "^4.11.0",
+    "eslint": "^7.16.0",
+    "eslint-config-google": "^0.14.0",
+    "eslint-plugin-html": "^6.1.1",
+    "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-jsdoc": "^30.7.9",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-prettier": "^3.3.0",
+    "gts": "^3.0.3",
     "polymer-cli": "^1.9.11",
     "prettier": "2.0.5",
     "rollup": "^2.3.4",
     "terser": "^4.8.0",
-    "typescript": "3.9.5"
+    "typescript": "4.0.5"
   },
   "scripts": {
     "clean": "git clean -fdx && bazel clean --expunge",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 7357ab4..740c35a 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 7357ab473599d16ae33cc982bbd65472f08c2dd6
+Subproject commit 740c35ae36f44748b3c91e60ee7dcb2fb6e99549
diff --git a/plugins/gitiles b/plugins/gitiles
index 0cd08bb..b196dd5 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 0cd08bbfac1a938079be8aaf6a6eef0e82ff9a41
+Subproject commit b196dd5b6fcfd50518a6625a64cb93424c084620
diff --git a/plugins/replication b/plugins/replication
index aa2792a..84b9304 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit aa2792a5f8a89c21e776cea29082a95d459e0697
+Subproject commit 84b93043383fe4296944598739db0a2dfb9b2ee6
diff --git a/polygerrit-ui/app/.eslintignore b/polygerrit-ui/app/.eslintignore
index 16ea228..087a049 100644
--- a/polygerrit-ui/app/.eslintignore
+++ b/polygerrit-ui/app/.eslintignore
@@ -1,3 +1,4 @@
 **/node_modules
 **/rollup.config.js
 node_modules_licenses
+!.eslintrc-bazel.js
diff --git a/polygerrit-ui/app/.eslintrc-bazel.js b/polygerrit-ui/app/.eslintrc-bazel.js
index 977eb45..9a51242 100644
--- a/polygerrit-ui/app/.eslintrc-bazel.js
+++ b/polygerrit-ui/app/.eslintrc-bazel.js
@@ -20,7 +20,7 @@
 // for node_modules.
 
 function getBazelSettings() {
-  const runFilesDir = process.env["RUNFILES_DIR"];
+  const runFilesDir = process.env['RUNFILES_DIR'];
   if (!runFilesDir) {
     // eslint is executed with 'bazel run ...' to fix the source code. It runs
     // against real source code, no special paths for node_modules is set.
@@ -28,18 +28,18 @@
   }
   // eslint is executed with 'bazel test...'. Set path to required node_modules
   return {
-    "import/resolver": {
-      "node": {
-        "paths": [
+    'import/resolver': {
+      node: {
+        paths: [
           `${runFilesDir}/ui_npm/node_modules`,
-          `${runFilesDir}/ui_dev_npm/node_modules`
-        ]
-      }
-    }
+          `${runFilesDir}/ui_dev_npm/node_modules`,
+        ],
+      },
+    },
   };
 }
 
 module.exports = {
-  "extends": "./.eslintrc.js",
-  "settings": getBazelSettings(),
+  extends: './.eslintrc.js',
+  settings: getBazelSettings(),
 };
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 28bea0b..ba2c40d 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -20,219 +20,224 @@
 const path = require('path');
 
 module.exports = {
-  "extends": ["eslint:recommended", "google"],
-  "parserOptions": {
-    "ecmaVersion": 9,
-    "sourceType": "module"
+  extends: ['eslint:recommended', 'google'],
+  parserOptions: {
+    ecmaVersion: 9,
+    sourceType: 'module',
   },
-  "env": {
-    "browser": true,
-    "es6": true
+  env: {
+    browser: true,
+    es6: true,
   },
-  "rules": {
+  rules: {
     // https://eslint.org/docs/rules/no-confusing-arrow
-    "no-confusing-arrow": "error",
+    'no-confusing-arrow': 'error',
     // https://eslint.org/docs/rules/newline-per-chained-call
-    "newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
+    'newline-per-chained-call': ['error', {ignoreChainWithDepth: 2}],
     // https://eslint.org/docs/rules/arrow-body-style
-    "arrow-body-style": ["error", "as-needed",
-      {"requireReturnForObjectLiteral": true}],
+    'arrow-body-style': ['error', 'as-needed',
+      {requireReturnForObjectLiteral: true}],
     // https://eslint.org/docs/rules/arrow-parens
-    "arrow-parens": ["error", "as-needed"],
+    'arrow-parens': ['error', 'as-needed'],
     // https://eslint.org/docs/rules/block-spacing
-    "block-spacing": ["error", "always"],
+    'block-spacing': ['error', 'always'],
     // https://eslint.org/docs/rules/brace-style
-    "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+    'brace-style': ['error', '1tbs', {allowSingleLine: true}],
     // https://eslint.org/docs/rules/camelcase
-    "camelcase": "off",
+    'camelcase': 'off',
     // https://eslint.org/docs/rules/comma-dangle
-    "comma-dangle": ["error", {
-      "arrays": "always-multiline",
-      "objects": "always-multiline",
-      "imports": "always-multiline",
-      "exports": "always-multiline",
-      "functions": "never"
+    'comma-dangle': ['error', {
+      arrays: 'always-multiline',
+      objects: 'always-multiline',
+      imports: 'always-multiline',
+      exports: 'always-multiline',
+      functions: 'never',
     }],
     // https://eslint.org/docs/rules/eol-last
-    "eol-last": "off",
+    'eol-last': 'off',
     // https://eslint.org/docs/rules/indent
-    "indent": ["error", 2, {
-      "MemberExpression": 2,
-      "FunctionDeclaration": {"body": 1, "parameters": 2},
-      "FunctionExpression": {"body": 1, "parameters": 2},
-      "CallExpression": {"arguments": 2},
-      "ArrayExpression": 1,
-      "ObjectExpression": 1,
-      "SwitchCase": 1
+    'indent': ['error', 2, {
+      MemberExpression: 2,
+      FunctionDeclaration: {body: 1, parameters: 2},
+      FunctionExpression: {body: 1, parameters: 2},
+      CallExpression: {arguments: 2},
+      ArrayExpression: 1,
+      ObjectExpression: 1,
+      SwitchCase: 1,
     }],
     // https://eslint.org/docs/rules/keyword-spacing
-    "keyword-spacing": ["error", {"after": true, "before": true}],
+    'keyword-spacing': ['error', {after: true, before: true}],
     // https://eslint.org/docs/rules/lines-between-class-members
-    "lines-between-class-members": ["error", "always"],
+    'lines-between-class-members': ['error', 'always'],
     // https://eslint.org/docs/rules/max-len
-    "max-len": [
-      "error",
+    'max-len': [
+      'error',
       80,
       2,
       {
-        "ignoreComments": true,
-        "ignorePattern": "^import .*;$"
-      }
+        ignoreComments: true,
+        ignorePattern: '^import .*;$',
+      },
     ],
     // https://eslint.org/docs/rules/new-cap
-    "new-cap": ["error", {
-      "capIsNewExceptions": ["Polymer", "GestureEventListeners"],
-      "capIsNewExceptionPattern": "^.*Mixin$"
+    'new-cap': ['error', {
+      capIsNewExceptions: ['Polymer', 'GestureEventListeners'],
+      capIsNewExceptionPattern: '^.*Mixin$',
     }],
     // https://eslint.org/docs/rules/no-console
-    "no-console": ["error", { allow: ["warn", "error", "info", "assert", "group", "groupEnd"] }],
+    'no-console': [
+      'error',
+      {allow: ['warn', 'error', 'info', 'assert', 'group', 'groupEnd']},
+    ],
     // https://eslint.org/docs/rules/no-multiple-empty-lines
-    "no-multiple-empty-lines": ["error", {"max": 1}],
+    'no-multiple-empty-lines': ['error', {max: 1}],
     // https://eslint.org/docs/rules/no-prototype-builtins
-    "no-prototype-builtins": "off",
+    'no-prototype-builtins': 'off',
     // https://eslint.org/docs/rules/no-redeclare
-    "no-redeclare": "off",
+    'no-redeclare': 'off',
     // https://eslint.org/docs/rules/no-trailing-spaces
-    "no-trailing-spaces": "error",
+    'no-trailing-spaces': 'error',
     // https://eslint.org/docs/rules/no-irregular-whitespace
-    "no-irregular-whitespace": "error",
+    'no-irregular-whitespace': 'error',
     // https://eslint.org/docs/rules/array-callback-return
-    "array-callback-return": ['error', { allowImplicit: true }],
+    'array-callback-return': ['error', {allowImplicit: true}],
     // https://eslint.org/docs/rules/no-restricted-syntax
-    "no-restricted-syntax": [
-      "error",
+    'no-restricted-syntax': [
+      'error',
       {
-        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
-        "message": "Remove test.only."
+        selector: 'ExpressionStatement > CallExpression > ' +
+            'MemberExpression[object.name=\'test\'][property.name=\'only\']',
+        message: 'Remove test.only.',
       },
       {
-        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
-        "message": "Remove suite.only."
-      }
+        selector: 'ExpressionStatement > CallExpression > ' +
+            'MemberExpression[object.name=\'suite\'][property.name=\'only\']',
+        message: 'Remove suite.only.',
+      },
     ],
     // no-undef disables global variable.
     // "globals" declares allowed global variables.
     // https://eslint.org/docs/rules/no-undef
-    "no-undef": ["error"],
+    'no-undef': ['error'],
     // https://eslint.org/docs/rules/no-useless-escape
-    "no-useless-escape": "off",
+    'no-useless-escape': 'off',
     // https://eslint.org/docs/rules/no-var
-    "no-var": "error",
+    'no-var': 'error',
     // https://eslint.org/docs/rules/operator-linebreak
-    "operator-linebreak": "off",
+    'operator-linebreak': 'off',
     // https://eslint.org/docs/rules/object-shorthand
-    "object-shorthand": ["error", "always"],
+    'object-shorthand': ['error', 'always'],
     // https://eslint.org/docs/rules/padding-line-between-statements
-    "padding-line-between-statements": [
-      "error",
+    'padding-line-between-statements': [
+      'error',
       {
-        "blankLine": "always",
-        "prev": "class",
-        "next": "*"
+        blankLine: 'always',
+        prev: 'class',
+        next: '*',
       },
       {
-        "blankLine": "always",
-        "prev": "*",
-        "next": "class"
-      }
+        blankLine: 'always',
+        prev: '*',
+        next: 'class',
+      },
     ],
     // https://eslint.org/docs/rules/prefer-arrow-callback
-    "prefer-arrow-callback": "error",
+    'prefer-arrow-callback': 'error',
     // https://eslint.org/docs/rules/prefer-const
-    "prefer-const": "error",
+    'prefer-const': 'error',
     // https://eslint.org/docs/rules/prefer-promise-reject-errors
-    "prefer-promise-reject-errors": "error",
+    'prefer-promise-reject-errors': 'error',
     // https://eslint.org/docs/rules/prefer-spread
-    "prefer-spread": "error",
+    'prefer-spread': 'error',
     // https://eslint.org/docs/rules/prefer-object-spread
-    "prefer-object-spread": "error",
+    'prefer-object-spread': 'error',
     // https://eslint.org/docs/rules/quote-props
-    "quote-props": ["error", "consistent-as-needed"],
+    'quote-props': ['error', 'consistent-as-needed'],
     // https://eslint.org/docs/rules/semi
-    "semi": ["error", "always"],
+    'semi': ['error', 'always'],
     // https://eslint.org/docs/rules/template-curly-spacing
-    "template-curly-spacing": "error",
+    'template-curly-spacing': 'error',
 
     // https://eslint.org/docs/rules/require-jsdoc
-    "require-jsdoc": 0,
+    'require-jsdoc': 0,
     // https://eslint.org/docs/rules/valid-jsdoc
-    "valid-jsdoc": 0,
+    'valid-jsdoc': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-alignment
-    "jsdoc/check-alignment": 2,
+    'jsdoc/check-alignment': 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-examples
-    "jsdoc/check-examples": 0,
+    'jsdoc/check-examples': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-indentation
-    "jsdoc/check-indentation": 0,
+    'jsdoc/check-indentation': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-param-names
-    "jsdoc/check-param-names": 0,
+    'jsdoc/check-param-names': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-syntax
-    "jsdoc/check-syntax": 0,
+    'jsdoc/check-syntax': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-tag-names
-    "jsdoc/check-tag-names": 0,
+    'jsdoc/check-tag-names': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-types
-    "jsdoc/check-types": 0,
+    'jsdoc/check-types': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-implements-on-classes
-    "jsdoc/implements-on-classes": 2,
+    'jsdoc/implements-on-classes': 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-match-description
-    "jsdoc/match-description": 0,
+    'jsdoc/match-description': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-newline-after-description
-    "jsdoc/newline-after-description": 2,
+    'jsdoc/newline-after-description': 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-types
-    "jsdoc/no-types": 0,
+    'jsdoc/no-types': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-undefined-types
-    "jsdoc/no-undefined-types": 0,
+    'jsdoc/no-undefined-types': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description
-    "jsdoc/require-description": 0,
+    'jsdoc/require-description': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description-complete-sentence
-    "jsdoc/require-description-complete-sentence": 0,
+    'jsdoc/require-description-complete-sentence': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-example
-    "jsdoc/require-example": 0,
+    'jsdoc/require-example': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-hyphen-before-param-description
-    "jsdoc/require-hyphen-before-param-description": 0,
+    'jsdoc/require-hyphen-before-param-description': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-jsdoc
-    "jsdoc/require-jsdoc": 0,
+    'jsdoc/require-jsdoc': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param
-    "jsdoc/require-param": 0,
+    'jsdoc/require-param': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-description
-    "jsdoc/require-param-description": 0,
+    'jsdoc/require-param-description': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-name
-    "jsdoc/require-param-name": 2,
+    'jsdoc/require-param-name': 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns
-    "jsdoc/require-returns": 0,
+    'jsdoc/require-returns': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-check
-    "jsdoc/require-returns-check": 0,
+    'jsdoc/require-returns-check': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description
-    "jsdoc/require-returns-description": 0,
+    'jsdoc/require-returns-description': 0,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types
-    "jsdoc/valid-types": 2,
+    'jsdoc/valid-types': 2,
     // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-file-overview
-    "jsdoc/require-file-overview": ["error", {
-      "tags": {
-        "license": {
-          "mustExist": true,
-          "preventDuplicates": true
-        }
-      }
+    'jsdoc/require-file-overview': ['error', {
+      tags: {
+        license: {
+          mustExist: true,
+          preventDuplicates: true,
+        },
+      },
     }],
     // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
-    "import/no-self-import": 2,
+    'import/no-self-import': 2,
     // The no-cycle rule is slow, because it doesn't cache dependencies.
     // Disable it.
     // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-cycle.md
-    "import/no-cycle": 0,
+    'import/no-cycle': 0,
     // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-useless-path-segments.md
-    "import/no-useless-path-segments": 2,
+    'import/no-useless-path-segments': 2,
     // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unused-modules.md
-    "import/no-unused-modules": 2,
+    'import/no-unused-modules': 2,
     // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
-    "import/no-default-export": 2,
+    'import/no-default-export': 2,
     // Prevents certain identifiers being used.
     // Prefer flush() over flushAsynchronousOperations().
-    "id-blacklist": ["error", "flushAsynchronousOperations"],
+    'id-blacklist': ['error', 'flushAsynchronousOperations'],
   },
 
   // List of allowed globals in all files
-  "globals": {
+  globals: {
     // Polygerrit global variables.
     // You must not add anything new in this list!
     // Instead export variables from modules
@@ -240,156 +245,166 @@
     // Global variables from 3rd party libraries.
     // You should not add anything in this list, always try to import
     // If import is not possible - you can extend this list
-    "ShadyCSS": "readonly",
-    "linkify": "readonly",
-    "security": "readonly",
+    ShadyCSS: 'readonly',
+    linkify: 'readonly',
+    security: 'readonly',
   },
-  "overrides": [
+  overrides: [
     {
-      // .js-only rules
-      "files": ["**/*.js"],
-      "rules": {
-        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
-        "jsdoc/require-param-type": 2,
-        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
-        "jsdoc/require-returns-type": 2,
-        // The rule is required for .js files only, because typescript compiler
-        // always checks import.
-        "import/no-unresolved": 2,
-        "import/named": 2,
+      files: ['.eslintrc.js', '.eslintrc-bazel.js'],
+      env: {
+        browser: false,
+        es6: true,
+        node: true,
       },
-      "globals": {
-        "goog": "readonly",
-      }
     },
     {
-      "files": ["**/*.ts"],
-      "extends": [require.resolve("gts/.eslintrc.json")],
-      "rules": {
-        "no-restricted-imports": ["error", {
-          name: "@polymer/decorators/lib/decorators",
-          message: "Use @polymer/decorators instead",
+      // .js-only rules
+      files: ['**/*.js'],
+      rules: {
+        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
+        'jsdoc/require-param-type': 2,
+        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
+        'jsdoc/require-returns-type': 2,
+        // The rule is required for .js files only, because typescript compiler
+        // always checks import.
+        'import/no-unresolved': 2,
+        'import/named': 2,
+      },
+      globals: {
+        goog: 'readonly',
+      },
+    },
+    {
+      files: ['**/*.ts'],
+      extends: [require.resolve('gts/.eslintrc.json')],
+      rules: {
+        'no-restricted-imports': ['error', {
+          name: '@polymer/decorators/lib/decorators',
+          message: 'Use @polymer/decorators instead',
         }],
-        "@typescript-eslint/no-explicit-any": "error",
+        '@typescript-eslint/no-explicit-any': 'error',
         // The following rules is required to match internal google rules
-        "@typescript-eslint/restrict-plus-operands": "error",
-        "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
+        '@typescript-eslint/restrict-plus-operands': 'error',
+        '@typescript-eslint/no-unused-vars': [
+          'error',
+          {argsIgnorePattern: '^_'},
+        ],
         // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
-        "node/no-unsupported-features/node-builtins": "off",
+        'node/no-unsupported-features/node-builtins': 'off',
         // Disable no-invalid-this for ts files, because it incorrectly reports
         // errors in some cases (see https://github.com/typescript-eslint/typescript-eslint/issues/491)
         // At the same time, we are using typescript in a strict mode and
         // it catches almost all errors related to invalid usage of this.
-        "no-invalid-this": "off",
+        'no-invalid-this': 'off',
 
-        "node/no-extraneous-import": "off",
+        'node/no-extraneous-import': 'off',
 
         // Typescript already checks for undef
-        "no-undef": "off",
+        'no-undef': 'off',
 
-        "jsdoc/no-types": 2,
+        'jsdoc/no-types': 2,
       },
-      "parserOptions": {
-        "project": path.resolve(__dirname, "./tsconfig_eslint.json"),
-      }
-    },
-    {
-      "files": ["*_test.ts"],
-      "rules": {
-        "@typescript-eslint/no-explicit-any": "off"
+      parserOptions: {
+        project: path.resolve(__dirname, './tsconfig_eslint.json'),
       },
     },
     {
-      "files": ["*.html", "test.js", "test-infra.js"],
-      "rules": {
-        "jsdoc/require-file-overview": "off"
+      files: ['*_test.ts'],
+      rules: {
+        '@typescript-eslint/no-explicit-any': 'off',
       },
     },
     {
-      "files": [
-        "*.html",
-        "*_test.js",
-        "a11y-test-utils.js",
+      files: ['*.html', 'test.js', 'test-infra.js'],
+      rules: {
+        'jsdoc/require-file-overview': 'off',
+      },
+    },
+    {
+      files: [
+        '*.html',
+        '*_test.js',
+        'a11y-test-utils.js',
       ],
       // Additional global variables allowed in tests
-      "globals": {
+      globals: {
         // Global variables from 3rd party test libraries/frameworks.
         // You can extend this list if you want to use other global
         // variables from these libraries and import is not possible
-        "MockInteractions": "readonly",
-        "_": "readonly",
-        "axs": "readonly",
-        "a11ySuite": "readonly",
-        "assert": "readonly",
-        "expect": "readonly",
-        "fixture": "readonly",
-        "flush": "readonly",
-        "flushAsynchronousOperations": "readonly",
-        "setup": "readonly",
-        "sinon": "readonly",
-        "stub": "readonly",
-        "suite": "readonly",
-        "suiteSetup": "readonly",
-        "suiteTeardown": "readonly",
-        "teardown": "readonly",
-        "test": "readonly",
-        "fixtureFromElement": "readonly",
-        "fixtureFromTemplate": "readonly",
-      }
+        MockInteractions: 'readonly',
+        _: 'readonly',
+        axs: 'readonly',
+        a11ySuite: 'readonly',
+        assert: 'readonly',
+        expect: 'readonly',
+        fixture: 'readonly',
+        flush: 'readonly',
+        setup: 'readonly',
+        sinon: 'readonly',
+        stub: 'readonly',
+        suite: 'readonly',
+        suiteSetup: 'readonly',
+        suiteTeardown: 'readonly',
+        teardown: 'readonly',
+        test: 'readonly',
+        fixtureFromElement: 'readonly',
+        fixtureFromTemplate: 'readonly',
+      },
     },
     {
-      "files": "import-href.js",
-      "globals": {
-        "HTMLImports": "readonly",
-      }
+      files: 'import-href.js',
+      globals: {
+        HTMLImports: 'readonly',
+      },
     },
     {
-      "files": ["samples/**/*.js"],
-      "globals": {
+      files: ['samples/**/*.js'],
+      globals: {
         // Settings for samples. You can add globals here if you want to use it
-        "Gerrit": "readonly",
-        "Polymer": "readonly",
-      }
+        Gerrit: 'readonly',
+        Polymer: 'readonly',
+      },
     },
     {
-      "files": ["test/functional/**/*.js"],
+      files: ['test/functional/**/*.js'],
       // Settings for functional tests. These scripts are node scripts.
       // Turn off "no-undef" to allow any global variable
-      "env": {
-        "browser": false,
-        "node": true,
-        "es6": false
+      env: {
+        browser: false,
+        node: true,
+        es6: false,
       },
-      "rules": {
-        "no-undef": "off",
-      }
+      rules: {
+        'no-undef': 'off',
+      },
     },
     {
-      "files": ["*_html.js", "gr-icons.js", "*-theme.js", "*-styles.js"],
-      "rules": {
-        "max-len": "off"
-      }
+      files: ['*_html.js', 'gr-icons.js', '*-theme.js', '*-styles.js'],
+      rules: {
+        'max-len': 'off',
+      },
     },
     {
-      "files": ["*_html.js"],
-      "rules": {
-        "prettier/prettier": ["error", {
-          "bracketSpacing": false,
-          "singleQuote": true,
-        }]
-      }
-    }
+      files: ['*_html.js'],
+      rules: {
+        'prettier/prettier': ['error', {
+          bracketSpacing: false,
+          singleQuote: true,
+        }],
+      },
+    },
   ],
-  "plugins": [
-    "html",
-    "jsdoc",
-    "import",
-    "prettier"
+  plugins: [
+    'html',
+    'jsdoc',
+    'import',
+    'prettier',
   ],
-  "settings": {
-    "html/report-bad-indent": "error",
-    "import/resolver": {
-      "node": {},
+  settings: {
+    'html/report-bad-indent': 'error',
+    'import/resolver': {
+      node: {},
       [path.resolve(__dirname, './.eslint-ts-resolver.js')]: {},
     },
   },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 9afc5f4..c45e801 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -227,7 +227,9 @@
         preferences && preferences.legacycid_in_change_table
       );
       if (preferences.change_table && preferences.change_table.length > 0) {
-        const prefColumns = this.getVisibleColumns(preferences.change_table);
+        const prefColumns = this.renameProjectToRepoColumn(
+          preferences.change_table
+        );
         this.visibleChangeTableColumns = this.getEnabledColumns(
           prefColumns,
           config,
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 febf18a..fdf2811 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
@@ -38,11 +38,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {appContext} from '../../../services/app-context';
-import {
-  fetchChangeUpdates,
-  patchNumEquals,
-  CURRENT,
-} from '../../../utils/patch-set-util';
+import {fetchChangeUpdates, CURRENT} from '../../../utils/patch-set-util';
 import {
   changeIsOpen,
   isOwner,
@@ -1153,7 +1149,7 @@
 
   _getRevision(change: ChangeViewChangeInfo, patchNum?: PatchSetNum) {
     for (const rev of Object.values(change.revisions)) {
-      if (patchNumEquals(rev._number, patchNum)) {
+      if (rev._number === patchNum) {
         return rev;
       }
     }
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 da214d9..f7bca82 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
@@ -258,7 +258,7 @@
           rev2: revObj,
         },
       };
-      assert.deepEqual(element._getRevision(change, '2'), revObj);
+      assert.deepEqual(element._getRevision(change, 2), revObj);
     });
 
     test('_actionComparator sort order', () => {
@@ -742,11 +742,11 @@
         const changes = [
           {
             change_id: '12345678901234', topic: 'T', subject: 'random',
-            project: 'A',
+            project: 'A', status: 'MERGED',
           },
           {
             change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
-            project: 'B',
+            project: 'B', status: 'NEW',
           },
         ];
         setup(done => {
@@ -769,8 +769,8 @@
           flush(() => {
             const changesTable = dialog.shadowRoot.querySelector('table');
             const headers = Array.from(changesTable.querySelectorAll('th'));
-            const expectedHeadings = ['Change', 'Subject', 'Project',
-              'Status', ''];
+            const expectedHeadings = ['', 'Change', 'Status', 'Subject',
+              'Project', 'Progress', ''];
             const headings = headers.map(header => header.innerText);
             assert.equal(headings.length, expectedHeadings.length);
             for (let i = 0; i < headings.length; i++) {
@@ -779,7 +779,7 @@
             const changeRows = changesTable.querySelectorAll('tbody > tr');
             const change = Array.from(changeRows[0].querySelectorAll('td'))
                 .map(e => e.innerText);
-            const expectedChange = ['1234567890', 'random', 'A',
+            const expectedChange = ['', '1234567890', 'MERGED', 'random', 'A',
               'NOT STARTED', ''];
             for (let i = 0; i < change.length; i++) {
               assert.equal(change[i].trim(), expectedChange[i]);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 3ee3470..ab7d09b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -360,13 +360,6 @@
         );
       });
 
-      test('_getNonOwnerRole null for uploader with no current rev', () => {
-        delete change!.current_revision;
-        assert.isNotOk(
-          element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER)
-        );
-      });
-
       test('_computeShowRoleClass show uploader', () => {
         assert.equal(
           element._computeShowRoleClass(change, element._CHANGE_ROLE.UPLOADER),
@@ -407,26 +400,12 @@
         );
       });
 
-      test('_getNonOwnerRole null for committer with no current rev', () => {
-        delete change!.current_revision;
-        assert.isNotOk(
-          element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER)
-        );
-      });
-
       test('_getNonOwnerRole null for committer with no commit', () => {
         delete change!.revisions.rev1.commit;
         assert.isNotOk(
           element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER)
         );
       });
-
-      test('_getNonOwnerRole null for committer with no committer', () => {
-        delete change!.revisions.rev1.commit!.committer;
-        assert.isNotOk(
-          element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER)
-        );
-      });
     });
 
     suite('role=author', () => {
@@ -445,26 +424,12 @@
         );
       });
 
-      test('_getNonOwnerRole null for author with no current rev', () => {
-        delete change!.current_revision;
-        assert.isNotOk(
-          element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR)
-        );
-      });
-
       test('_getNonOwnerRole null for author with no commit', () => {
         delete change!.revisions.rev1.commit;
         assert.isNotOk(
           element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR)
         );
       });
-
-      test('_getNonOwnerRole null for author with no author', () => {
-        delete change!.revisions.rev1.commit!.author;
-        assert.isNotOk(
-          element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR)
-        );
-      });
     });
   });
 
@@ -609,7 +574,6 @@
     element.revision = undefined;
     assert.equal(element._currentParents[0].commit, '111');
     element.change = createParsedChange();
-    delete element.change.current_revision;
     assert.deepEqual(element._currentParents, []);
   });
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 950fbf3..126d017 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -69,7 +69,6 @@
   fetchChangeUpdates,
   hasEditBasedOnCurrentPatchSet,
   hasEditPatchsetLoaded,
-  patchNumEquals,
   PatchSet,
 } from '../../../utils/patch-set-util';
 import {changeStatuses, changeStatusString} from '../../../utils/change-util';
@@ -145,6 +144,7 @@
   EditableContentSaveEvent,
   OpenFixPreviewEvent,
   SwitchTabEvent,
+  ThreadListModifiedEvent,
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrMessagesList} from '../gr-messages-list/gr-messages-list';
@@ -600,6 +600,11 @@
       this._handleReloadCommentThreads()
     );
 
+    this.addEventListener(
+      'thread-list-modified',
+      (e: ThreadListModifiedEvent) => this._handleReloadDiffComments(e)
+    );
+
     this.addEventListener('open-reply-dialog', () => this._openReplyDialog());
   }
 
@@ -1650,7 +1655,7 @@
     if (!this._change) throw new Error('missing required change property');
     if (!this._patchRange)
       throw new Error('missing required _patchRange property');
-    if (patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)) {
+    if (this._patchRange.basePatchNum === ParentPatchSetNum) {
       fireAlert(this, 'Base is already selected.');
       return;
     }
@@ -1664,7 +1669,7 @@
     if (!this._change) throw new Error('missing required change property');
     if (!this._patchRange)
       throw new Error('missing required _patchRange property');
-    if (patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)) {
+    if (this._patchRange.basePatchNum === ParentPatchSetNum) {
       fireAlert(this, 'Left is already base.');
       return;
     }
@@ -1679,7 +1684,7 @@
     if (!this._patchRange)
       throw new Error('missing required _patchRange property');
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
-    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    if (this._patchRange.patchNum === latestPatchNum) {
       fireAlert(this, 'Latest is already selected.');
       return;
     }
@@ -1698,7 +1703,7 @@
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
     if (!this._patchRange)
       throw new Error('missing required _patchRange property');
-    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    if (this._patchRange.patchNum === latestPatchNum) {
       fireAlert(this, 'Right is already latest.');
       return;
     }
@@ -1718,8 +1723,8 @@
       throw new Error('missing required _patchRange property');
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
     if (
-      patchNumEquals(this._patchRange.patchNum, latestPatchNum) &&
-      patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)
+      this._patchRange.patchNum === latestPatchNum &&
+      this._patchRange.basePatchNum === ParentPatchSetNum
     ) {
       fireAlert(this, 'Already diffing base against latest.');
       return;
@@ -1961,7 +1966,7 @@
         if (
           !this._patchRange ||
           !this._patchRange.patchNum ||
-          patchNumEquals(this._patchRange.patchNum, currentRevision._number)
+          this._patchRange.patchNum === currentRevision._number
         ) {
           // CommitInfo.commit is optional, and may need patching.
           if (currentRevision.commit && !currentRevision.commit.commit) {
@@ -2561,7 +2566,7 @@
     }
 
     const patchRange = patchRangeRecord.base || {};
-    return patchNumEquals(patchRange.patchNum, EditPatchSetNum);
+    return patchRange.patchNum === EditPatchSetNum;
   }
 
   _handleFileActionTap(e: CustomEvent<{path: string; action: string}>) {
@@ -2645,10 +2650,7 @@
       throw new Error('missing required _patchRange property');
     let patchNum;
     if (
-      !patchNumEquals(
-        this._patchRange.patchNum,
-        computeLatestPatchNum(this._allPatchSets)
-      )
+      !(this._patchRange.patchNum === computeLatestPatchNum(this._allPatchSets))
     ) {
       patchNum = this._patchRange.patchNum;
     }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index cb8ed6a..4fcc9fe 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -646,7 +646,6 @@
           change-num="[[_changeNum]]"
           logged-in="[[_loggedIn]]"
           only-show-robot-comments-with-human-reply=""
-          on-thread-list-modified="_handleReloadDiffComments"
           unresolved-only
         ></gr-thread-list>
       </template>
@@ -674,7 +673,6 @@
           logged-in="[[_loggedIn]]"
           hide-toggle-buttons
           empty-thread-msg="[[_messages.NO_ROBOT_COMMENTS_THREADS_MSG]]"
-          on-thread-list-modified="_handleReloadDiffComments"
         >
         </gr-thread-list>
         <template is="dom-if" if="[[_showRobotCommentsButton]]">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index 2d90c0b..c4526bb 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -31,6 +31,7 @@
   RepoName,
   BranchName,
   CommitId,
+  ChangeInfoId,
 } from '../../../types/common';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {customElement, property, observe} from '@polymer/decorators';
@@ -40,6 +41,7 @@
 } from '../../shared/gr-autocomplete/gr-autocomplete';
 import {HttpMethod, ChangeStatus} from '../../../constants/constants';
 import {hasOwnProperty} from '../../../utils/common-util';
+import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 
 const SUGGESTIONS_LIMIT = 15;
 const CHANGE_SUBJECT_LIMIT = 50;
@@ -48,11 +50,18 @@
   TOPIC,
 }
 
+// These values are directly displayed in the dialog to show progress of change
+enum ProgressStatus {
+  RUNNING = 'RUNNING',
+  FAILED = 'FAILED',
+  NOT_STARTED = 'NOT STARTED',
+  SUCCESSFUL = 'SUCCESSFUL',
+}
+
 type Statuses = {[changeId: string]: Status};
 
-// TODO(TS): maybe convert status to an enum
 interface Status {
-  status: string;
+  status: ProgressStatus;
   msg?: string;
 }
 
@@ -139,6 +148,8 @@
   @property({type: Object})
   reporting: ReportingService;
 
+  private selectedChangeIds = new Set<ChangeInfoId>();
+
   private restApiService = appContext.restApiService;
 
   constructor() {
@@ -154,6 +165,7 @@
     const projects: {[projectName: string]: boolean} = {};
     this._duplicateProjectChanges = false;
     changes.forEach(change => {
+      this.selectedChangeIds.add(change.id);
       if (projects[change.project]) {
         this._duplicateProjectChanges = true;
       }
@@ -171,6 +183,19 @@
     );
   }
 
+  _isChangeSelected(changeId: ChangeInfoId) {
+    return this.selectedChangeIds.has(changeId);
+  }
+
+  _toggleChangeSelected(e: Event) {
+    const changeId = ((dom(e) as EventApi).localTarget as HTMLElement).dataset[
+      'item'
+    ]! as ChangeInfoId;
+    if (this.selectedChangeIds.has(changeId))
+      this.selectedChangeIds.delete(changeId);
+    else this.selectedChangeIds.add(changeId);
+  }
+
   _computeTopicErrorMessage(duplicateProjectChanges: boolean) {
     if (duplicateProjectChanges) {
       return 'Two changes cannot be of the same project';
@@ -183,18 +208,19 @@
   }
 
   _computeStatus(change: ChangeInfo, statuses: Statuses) {
-    if (!change || !statuses || !statuses[change.id]) return 'NOT STARTED';
+    if (!change || !statuses || !statuses[change.id])
+      return ProgressStatus.NOT_STARTED;
     return statuses[change.id].status;
   }
 
   _computeStatusClass(change: ChangeInfo, statuses: Statuses) {
     if (!change || !statuses || !statuses[change.id]) return '';
-    return statuses[change.id].status === 'FAILED' ? 'error' : '';
+    return statuses[change.id].status === ProgressStatus.FAILED ? 'error' : '';
   }
 
   _computeError(change: ChangeInfo, statuses: Statuses) {
     if (!change || !statuses || !statuses[change.id]) return '';
-    if (statuses[change.id].status === 'FAILED') {
+    if (statuses[change.id].status === ProgressStatus.FAILED) {
       return statuses[change.id].msg;
     }
     return '';
@@ -212,7 +238,7 @@
 
   _computeCancelLabel(statuses: Statuses) {
     const isRunningChange = Object.values(statuses).some(
-      v => v.status === 'RUNNING'
+      v => v.status === ProgressStatus.RUNNING
     );
     return isRunningChange ? 'Close' : 'Cancel';
   }
@@ -220,14 +246,16 @@
   _computeDisableCherryPick(
     cherryPickType: CherryPickType,
     duplicateProjectChanges: boolean,
-    statuses: Statuses
+    statuses: Statuses,
+    branch?: BranchName
   ) {
+    if (!branch) return true;
     const duplicateProject =
       cherryPickType === CherryPickType.TOPIC && duplicateProjectChanges;
     if (duplicateProject) return true;
     if (!statuses) return false;
     const isRunningChange = Object.values(statuses).some(
-      v => v.status === 'RUNNING'
+      v => v.status === ProgressStatus.RUNNING
     );
     return isRunningChange;
   }
@@ -280,14 +308,22 @@
   _handleCherryPickFailed(change: ChangeInfo, response?: Response | null) {
     if (!response) return;
     response.text().then((errText: string) => {
-      this.updateStatus(change, {status: 'FAILED', msg: errText});
+      this.updateStatus(change, {status: ProgressStatus.FAILED, msg: errText});
     });
   }
 
   _handleCherryPickTopic() {
-    const topic = this._generateRandomCherryPickTopic(this.changes[0]);
-    this.changes.forEach(change => {
-      this.updateStatus(change, {status: 'RUNNING'});
+    const changes = this.changes.filter(change =>
+      this.selectedChangeIds.has(change.id)
+    );
+    if (!changes.length) {
+      const errorSpan = this.shadowRoot?.querySelector('.error-message');
+      errorSpan!.innerHTML = 'No change selected';
+      return;
+    }
+    const topic = this._generateRandomCherryPickTopic(changes[0]);
+    changes.forEach(change => {
+      this.updateStatus(change, {status: ProgressStatus.RUNNING});
       const payload = {
         destination: this.branch,
         base: null,
@@ -310,9 +346,9 @@
           handleError
         )
         .then(() => {
-          this.updateStatus(change, {status: 'SUCCESSFUL'});
+          this.updateStatus(change, {status: ProgressStatus.SUCCESSFUL});
           const failedOrPending = Object.values(this._statuses).find(
-            v => v.status !== 'SUCCESSFUL'
+            v => v.status !== ProgressStatus.SUCCESSFUL
           );
           if (!failedOrPending) {
             /* This needs some more work, as the new topic may not always be
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
index f784425..4de395c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
@@ -85,7 +85,7 @@
   <gr-dialog
     confirm-label="Cherry Pick"
     cancel-label="[[_computeCancelLabel(_statuses)]]"
-    disabled$="[[_computeDisableCherryPick(_cherryPickType, _duplicateProjectChanges, _statuses)]]"
+    disabled$="[[_computeDisableCherryPick(_cherryPickType, _duplicateProjectChanges, _statuses, branch)]]"
     on-confirm="_handleConfirmTap"
     on-cancel="_handleCancelTap"
   >
@@ -179,10 +179,12 @@
         <table>
           <thead>
             <tr>
+              <th></th>
               <th>Change</th>
+              <th>Status</th>
               <th>Subject</th>
               <th>Project</th>
-              <th>Status</th>
+              <th>Progress</th>
               <!-- Error Message -->
               <th></th>
             </tr>
@@ -190,7 +192,16 @@
           <tbody>
             <template is="dom-repeat" items="[[changes]]">
               <tr>
+                <td>
+                  <input
+                    type="checkbox"
+                    data-item$="[[item.id]]"
+                    on-change="_toggleChangeSelected"
+                    checked="[[_isChangeSelected(item.id)]]"
+                  />
+                </td>
                 <td><span> [[_getChangeId(item)]] </span></td>
+                <td><span> [[item.status]] </span></td>
                 <td>
                   <span> [[_getTrimmedChangeSubject(item.subject)]] </span>
                 </td>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index cc4bb8f..5564fcf 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -88,6 +88,7 @@
   suite('cherry pick topic', () => {
     const changes = [
       {
+        id: '1234',
         change_id: '12345678901234', topic: 'T', subject: 'random',
         project: 'A',
         _number: 1,
@@ -97,6 +98,7 @@
         current_revision: 'a',
       },
       {
+        id: '5678',
         change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
         project: 'B',
         _number: 2,
@@ -109,6 +111,7 @@
     setup(() => {
       element.updateChanges(changes);
       element._cherryPickType = CHERRY_PICK_TYPES.TOPIC;
+      flush();
     });
 
     test('cherry pick topic submit', done => {
@@ -129,6 +132,38 @@
       });
     });
 
+    test('deselecting a change removes it from being cherry picked', () => {
+      element.branch = 'master';
+      const executeChangeActionStub = sinon.stub(element.restApiService,
+          'executeChangeAction').returns(Promise.resolve([]));
+      const checkboxes = element.shadowRoot.querySelectorAll(
+          'input[type="checkbox"]');
+      assert.equal(checkboxes.length, 2);
+      assert.isTrue(checkboxes[0].checked);
+      MockInteractions.tap(checkboxes[0]);
+      MockInteractions.tap(element.shadowRoot.
+          querySelector('gr-dialog').$.confirm);
+      flush();
+      assert.equal(executeChangeActionStub.callCount, 1);
+    });
+
+    test('deselecting all change shows error message', () => {
+      element.branch = 'master';
+      const executeChangeActionStub = sinon.stub(element.restApiService,
+          'executeChangeAction').returns(Promise.resolve([]));
+      const checkboxes = element.shadowRoot.querySelectorAll(
+          'input[type="checkbox"]');
+      assert.equal(checkboxes.length, 2);
+      MockInteractions.tap(checkboxes[0]);
+      MockInteractions.tap(checkboxes[1]);
+      MockInteractions.tap(element.shadowRoot.
+          querySelector('gr-dialog').$.confirm);
+      flush();
+      assert.equal(executeChangeActionStub.callCount, 0);
+      assert.equal(element.shadowRoot.querySelector('.error-message').innerText
+          , 'No change selected');
+    });
+
     test('_computeStatusClass', () => {
       assert.equal(element._computeStatusClass({id: 1}, {1: {status: 'RUNNING'},
       }), '');
@@ -139,6 +174,9 @@
     test('submit button is blocked while cherry picks is running', done => {
       const confirmButton = element.shadowRoot.querySelector('gr-dialog').$
           .confirm;
+      assert.isTrue(confirmButton.hasAttribute('disabled'));
+      element.branch = 'b';
+      flush();
       assert.isFalse(confirmButton.hasAttribute('disabled'));
       element.updateStatus(changes[0], {status: 'RUNNING'});
       flush(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 27d9756..5949513 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -20,7 +20,6 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-download-dialog_html';
-import {patchNumEquals} from '../../../utils/patch-set-util';
 import {changeBaseURL} from '../../../utils/change-util';
 import {customElement, property, computed, observe} from '@polymer/decorators';
 import {ChangeInfo, ServerInfo, PatchSetNum} from '../../../types/common';
@@ -72,7 +71,7 @@
     }
 
     for (const rev of Object.values(this.change.revisions || {})) {
-      if (patchNumEquals(rev._number, this.patchNum)) {
+      if (rev._number === this.patchNum) {
         const fetch = rev.fetch;
         if (fetch) {
           return Object.keys(fetch).sort();
@@ -113,7 +112,7 @@
     if (!change || !selectedScheme) return [];
     for (const rev of Object.values(change.revisions || {})) {
       if (
-        patchNumEquals(rev._number, patchNum) &&
+        rev._number === patchNum &&
         rev &&
         rev.fetch &&
         hasOwnProperty(rev.fetch, selectedScheme)
@@ -171,7 +170,7 @@
 
     let shortRev = '';
     for (const rev in change.revisions) {
-      if (patchNumEquals(change.revisions[rev]._number, patchNum)) {
+      if (change.revisions[rev]._number === patchNum) {
         shortRev = rev.substr(0, 7);
         break;
       }
@@ -185,7 +184,7 @@
       return false;
     }
     for (const rev of Object.values(change.revisions || {})) {
-      if (patchNumEquals(rev._number, patchNum)) {
+      if (rev._number === patchNum) {
         const parentLength =
           rev.commit && rev.commit.parents ? rev.commit.parents.length : 0;
         return parentLength === 0 || parentLength > 1;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index e436325..52ec8af 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -121,7 +121,7 @@
   });
 
   test('anchors use download attribute', () => {
-    const anchors = Array.from(element.root?.querySelectorAll('a')!);
+    const anchors = Array.from(element.root!.querySelectorAll('a'));
     assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
   });
 
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 3c99648..146b3e2 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
@@ -35,7 +35,6 @@
 import {
   computeLatestPatchNum,
   getRevisionByPatchNum,
-  patchNumEquals,
   PatchSet,
 } from '../../../utils/patch-set-util';
 import {property, computed, observe, customElement} from '@polymer/decorators';
@@ -321,8 +320,7 @@
   _handlePatchChange(e: CustomEvent) {
     const {basePatchNum, patchNum} = e.detail;
     if (
-      (patchNumEquals(basePatchNum, this.basePatchNum) &&
-        patchNumEquals(patchNum, this.patchNum)) ||
+      (basePatchNum === this.basePatchNum && patchNum === this.patchNum) ||
       !this.change
     ) {
       return;
@@ -354,7 +352,7 @@
 
   _computePatchInfoClass(patchNum?: PatchSetNum, allPatchSets?: PatchSet[]) {
     const latestNum = computeLatestPatchNum(allPatchSets);
-    if (patchNumEquals(patchNum, latestNum)) {
+    if (patchNum === latestNum) {
       return '';
     }
     return 'patchInfoOldPatchSet';
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 cee0262..b8670c0 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
@@ -249,11 +249,11 @@
 
   test('class is applied to file list on old patch set', () => {
     const allPatchSets = [{num: 4}, {num: 2}, {num: 1}];
-    assert.equal(element._computePatchInfoClass('1', allPatchSets),
+    assert.equal(element._computePatchInfoClass(1, allPatchSets),
         'patchInfoOldPatchSet');
-    assert.equal(element._computePatchInfoClass('2', allPatchSets),
+    assert.equal(element._computePatchInfoClass(2, allPatchSets),
         'patchInfoOldPatchSet');
-    assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
+    assert.equal(element._computePatchInfoClass(4, allPatchSets), '');
   });
 
   suite('editMode behavior', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index a25ff20..bc61dad 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -45,8 +45,12 @@
 import {hasOwnProperty} from '../../../utils/common-util';
 import {appContext} from '../../../services/app-context';
 import {pluralize} from '../../../utils/string-util';
-import {fireEvent} from '../../../utils/event-util';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  computeAllPatchSets,
+  computeLatestPatchNum,
+  computePredecessor,
+} from '../../../utils/patch-set-util';
 
 const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?(?:P|p)atch (?:S|s)et \d+:\s*(.*)/;
 const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.]?$/;
@@ -226,15 +230,6 @@
     return pluralize(threadsLength, 'comment');
   }
 
-  _onThreadListModified() {
-    // TODO(taoalpha): this won't propagate the changes to the files
-    // should consider replacing this with either top level events
-    // or gerrit level events
-
-    // emit the event so change-view can also get updated with latest changes
-    fireEvent(this, 'comment-refresh');
-  }
-
   _computeMessageContentExpanded(content?: string, tag?: ReviewInputTag) {
     return this._computeMessageContent(true, content, tag);
   }
@@ -280,13 +275,20 @@
   _handleViewPatchsetDiff(e: Event) {
     if (!this.message || !this.change) return;
     const match = this.message.message.match(/Uploaded patch set (\d+)./);
-    if (!match || match.length < 1) return;
-    const patchNum = Number(match[1]);
-    if (isNaN(patchNum)) throw new Error('invalid patchnum in message');
+    let patchNum: PatchSetNum;
+    // Message is of the form "Commit Message was updated" or "Patchset X
+    // was rebased"
+    if (!match || match.length < 1) {
+      patchNum = computeLatestPatchNum(computeAllPatchSets(this.change))!;
+    } else {
+      if (isNaN(Number(match[1])))
+        throw new Error('invalid patchnum in message');
+      patchNum = Number(match[1]) as PatchSetNum;
+    }
     GerritNav.navigateToChange(
       this.change,
-      patchNum as PatchSetNum,
-      (patchNum === 1 ? 'PARENT' : patchNum - 1) as PatchSetNum
+      patchNum,
+      computePredecessor(patchNum)
     );
     // stop propagation to stop message expansion
     e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index 815d8bb..57beacf 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -256,7 +256,6 @@
               change-num="[[changeNum]]"
               logged-in="[[_loggedIn]]"
               hide-toggle-buttons
-              on-thread-list-modified="_onThreadListModified"
             >
             </gr-thread-list>
           </template>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
index 94507e6..facde01 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-message.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {createChange, createRevisions} from '../../../test/test-data-generators.js';
 
 const basicFixture = fixtureFromElement('gr-message');
 
@@ -232,7 +233,7 @@
     suite('uploaded patchset X message navigates to X - 1 vs  X', () => {
       let navStub;
       setup(() => {
-        element.change = {changeNum: 12345};
+        element.change = {...createChange(), revisions: createRevisions(4)};
         navStub = sinon.stub(GerritNav, 'navigateToChange');
       });
 
@@ -241,7 +242,7 @@
           message: 'Uploaded patch set 1.',
         };
         element._handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(navStub.calledWithExactly({changeNum: 12345}, 1,
+        assert.isTrue(navStub.calledWithExactly(element.change, 1,
             'PARENT'));
       });
 
@@ -250,21 +251,21 @@
           message: 'Uploaded patch set 2.',
         };
         element._handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(navStub.calledWithExactly({changeNum: 12345}, 2, 1));
+        assert.isTrue(navStub.calledWithExactly(element.change, 2, 1));
 
         element.message = {
           message: 'Uploaded patch set 200.',
         };
         element._handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(navStub.calledWithExactly({changeNum: 12345}, 200, 199));
+        assert.isTrue(navStub.calledWithExactly(element.change, 200, 199));
       });
 
-      test('invalid patchset does not cause navigation', () => {
+      test('Commit message updated', () => {
         element.message = {
-          message: 'Uploaded patch set XYZ.',
+          message: 'Commit message updated.',
         };
         element._handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isFalse(navStub.called);
+        assert.isTrue(navStub.calledWithExactly(element.change, 4, 3));
       });
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index fc6b5ba..e6c9f42 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -25,7 +25,7 @@
 import {htmlTemplate} from './gr-related-changes-list_html';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {ChangeStatus} from '../../../constants/constants';
-import {patchNumEquals} from '../../../utils/patch-set-util';
+
 import {changeIsOpen} from '../../../utils/change-util';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {customElement, observe, property} from '@polymer/decorators';
@@ -410,7 +410,7 @@
       return [];
     }
     for (const rev in change.revisions) {
-      if (patchNumEquals(change.revisions[rev]._number, patchNum)) {
+      if (change.revisions[rev]._number === patchNum) {
         changeRevision = rev;
       }
     }
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index f4d698ea..b92f179 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -42,7 +42,6 @@
   ReviewerState,
   SpecialFilePath,
 } from '../../../constants/constants';
-import {KnownExperimentId} from '../../../services/flags/flags';
 import {fetchChangeUpdates} from '../../../utils/patch-set-util';
 import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {accountKey, removeServiceUsers} from '../../../utils/account-util';
@@ -380,8 +379,6 @@
     };
   }
 
-  _isPatchsetCommentsExperimentEnabled = false;
-
   constructor() {
     super();
     this.filterReviewerSuggestion = this._filterReviewerSuggestionGenerator(
@@ -421,9 +418,6 @@
   /** @override */
   ready() {
     super.ready();
-    this._isPatchsetCommentsExperimentEnabled = this.flagsService.isEnabled(
-      KnownExperimentId.PATCHSET_COMMENTS
-    );
     this.$.jsAPI.addElement(TargetElement.REPLY_DIALOG, this);
   }
 
@@ -675,17 +669,13 @@
     }
 
     if (this.draft) {
-      if (this._isPatchsetCommentsExperimentEnabled) {
-        const comment: CommentInput = {
-          message: this.draft,
-          unresolved: !this._isResolvedPatchsetLevelComment,
-        };
-        reviewInput.comments = {
-          [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [comment],
-        };
-      } else {
-        reviewInput.message = this.draft;
-      }
+      const comment: CommentInput = {
+        message: this.draft,
+        unresolved: !this._isResolvedPatchsetLevelComment,
+      };
+      reviewInput.comments = {
+        [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [comment],
+      };
     }
 
     const accountAdditions = new Map<AccountId | EmailAddress, boolean>();
@@ -1463,15 +1453,6 @@
     return provider;
   }
 
-  _onThreadListModified() {
-    // TODO(taoalpha): this won't propogate the changes to the files
-    // should consider replacing this with either top level events
-    // or gerrit level events
-
-    // emit the event so change-view can also get updated with latest changes
-    fireEvent(this, 'comment-refresh');
-  }
-
   reportAttentionSetChanges(
     modified: boolean,
     addedSet?: AttentionSetInput[],
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 0948c1e..15e461a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -303,16 +303,14 @@
       </gr-endpoint-decorator>
     </section>
     <section class="previewContainer">
-      <template is="dom-if" if="[[_isPatchsetCommentsExperimentEnabled]]">
-        <label>
-          <input
-            id="resolvedPatchsetLevelCommentCheckbox"
-            type="checkbox"
-            checked="{{_isResolvedPatchsetLevelComment::change}}"
-          />
-          Resolved
-        </label>
-      </template>
+      <label>
+        <input
+          id="resolvedPatchsetLevelCommentCheckbox"
+          type="checkbox"
+          checked="{{_isResolvedPatchsetLevelComment::change}}"
+        />
+        Resolved
+      </label>
       <label class="preview-formatting">
         <input type="checkbox" checked="{{_previewFormatting::change}}" />
         Preview formatting
@@ -357,7 +355,6 @@
         change-num="[[change._number]]"
         logged-in="true"
         hide-toggle-buttons=""
-        on-thread-list-modified="_onThreadListModified"
       >
       </gr-thread-list>
       <span
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index ff30452..4976503 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -33,6 +33,7 @@
 import {ChangeInfo} from '../../../types/common';
 import {CommentThread, isDraft, UIRobot} from '../../../utils/comment-util';
 import {pluralize} from '../../../utils/string-util';
+import {fireThreadListModifiedEvent} from '../../../utils/event-util';
 
 interface CommentThreadWithInfo {
   thread: CommentThread;
@@ -421,11 +422,7 @@
   }
 
   _handleCommentsChanged(e: CustomEvent) {
-    this.dispatchEvent(
-      new CustomEvent('thread-list-modified', {
-        detail: {rootId: e.detail.rootId, path: e.detail.path},
-      })
-    );
+    fireThreadListModifiedEvent(this, e.detail.rootId, e.detail.path);
   }
 
   _isOnParent(side?: CommentSide) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 07045cd..2f3d03c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -46,10 +46,7 @@
   WeblinkType,
 } from '../gr-navigation/gr-navigation';
 import {appContext} from '../../../services/app-context';
-import {
-  convertToPatchSetNum,
-  patchNumEquals,
-} from '../../../utils/patch-set-util';
+import {convertToPatchSetNum} from '../../../utils/patch-set-util';
 import {customElement, property} from '@polymer/decorators';
 import {assertNever} from '../../../utils/common-util';
 import {
@@ -693,10 +690,7 @@
 
     // Diffing a patch against itself is invalid, so if the base and revision
     // patches are equal clear the base.
-    if (
-      params.patchNum &&
-      patchNumEquals(params.basePatchNum, params.patchNum)
-    ) {
+    if (params.patchNum && params.basePatchNum === params.patchNum) {
       needsRedirect = true;
       params.basePatchNum = null;
     } else if (!hasPatchNum) {
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index 9988116..5210616 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -18,7 +18,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-comment-api_html';
-import {patchNumEquals, CURRENT} from '../../../utils/patch-set-util';
+import {CURRENT} from '../../../utils/patch-set-util';
 import {customElement, property} from '@polymer/decorators';
 import {
   CommentBasics,
@@ -30,6 +30,7 @@
   NumericChangeId,
   PathToCommentsInfoMap,
   FileInfo,
+  ParentPatchSetNum,
 } from '../../../types/common';
 import {hasOwnProperty} from '../../../utils/common-util';
 import {
@@ -235,9 +236,7 @@
       allComments = allComments.concat(drafts);
     }
     if (patchNum) {
-      allComments = allComments.filter(c =>
-        patchNumEquals(c.patch_set, patchNum)
-      );
+      allComments = allComments.filter(c => c.patch_set === patchNum);
     }
     return allComments.map(c => {
       return {...c};
@@ -284,7 +283,7 @@
   getAllDraftsForPath(path: string, patchNum?: PatchSetNum): Comment[] {
     let comments = this._drafts[path] || [];
     if (patchNum) {
-      comments = comments.filter(c => patchNumEquals(c.patch_set, patchNum));
+      comments = comments.filter(c => c.patch_set === patchNum);
     }
     return comments.map(c => {
       return {...c, __draft: true};
@@ -369,6 +368,10 @@
    * does not return the range of the ported thread and it becomes a file level
    * thread.
    *
+   * If a comment was created with Side=PARENT, then we only show this ported
+   * comment if Base is part of the patch range, always on the left side of
+   * the diff.
+   *
    * @return only the ported threads for the specified file and patch range
    */
   _getPortedCommentThreads(
@@ -407,15 +410,22 @@
         return false;
       }
 
-      // TODO(dhruvsri): Add handling for thread.commentSide = PARENT
-      if (thread.commentSide === CommentSide.PARENT) return false;
+      thread.diffSide = Side.RIGHT;
+      if (thread.commentSide === CommentSide.PARENT) {
+        // TODO(dhruvsri): Add handling for merge parents
+        if (
+          patchRange.basePatchNum !== ParentPatchSetNum ||
+          !!thread.mergeParentNum
+        )
+          return false;
+        thread.diffSide = Side.LEFT;
+      }
 
       if (!isUnresolved(thread) && !isDraftThread(thread)) return false;
 
       thread.range = portedComment.range;
       thread.line = portedComment.line;
       thread.ported = true;
-      thread.diffSide = Side.RIGHT;
       return true;
     });
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
index dd36613..e107c16 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
@@ -20,6 +20,7 @@
 import {ChangeComments} from './gr-comment-api.js';
 import {isInRevisionOfPatchRange, isInBaseOfPatchRange, isDraftThread, isUnresolved, createCommentThreads} from '../../../utils/comment-util.js';
 import {createDraft, createComment, createChangeComments, createCommentThread} from '../../../test/test-data-generators.js';
+import {CommentSide, Side} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromElement('gr-comment-api');
 
@@ -153,7 +154,7 @@
       const comment1 = {
         ...createComment(),
         unresolved: true,
-        id: 'db977012_e1f13818',
+        id: '1',
         line: 136,
         patch_set: 2,
         range: {
@@ -167,10 +168,22 @@
       const comment2 = {
         ...createComment(),
         patch_set: 2,
-        id: 'ecf0b9fa_fe1a5f62',
+        id: '2',
         line: 5,
       };
 
+      const comment3 = {
+        ...createComment(),
+        side: CommentSide.PARENT,
+        line: 10,
+        unresolved: true,
+      };
+
+      const comment4 = {
+        ...comment3,
+        parent: -2,
+      };
+
       const draft1 = {
         ...createDraft(),
         id: 'db977012_e1f13828',
@@ -277,6 +290,74 @@
         ).length, 0);
       });
 
+      test('comments with side=PARENT are ported over', () => {
+        changeComments = new ChangeComments(
+            {/* comments */
+              // comment left on Base
+              'karma.conf.js': [comment3],
+            },
+            {}/* robot comments */,
+            {/* drafts */
+              'karma.conf.js': [draft2],
+            },
+            {/* ported comments */
+              'karma.conf.js': [{
+                ...comment3,
+                line: 31,
+                patch_set: 4,
+              }],
+            },
+            {}/* ported drafts */
+        );
+
+        const portedThreads = changeComments._getPortedCommentThreads(
+            {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 'PARENT'});
+        assert.equal(portedThreads.length, 1);
+        assert.equal(portedThreads[0].line, 31);
+        assert.equal(portedThreads[0].diffSide, Side.LEFT);
+
+        assert.equal(changeComments._getPortedCommentThreads(
+            {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: -2}
+        ).length, 0);
+
+        assert.equal(changeComments._getPortedCommentThreads(
+            {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 2}
+        ).length, 0);
+      });
+
+      test('comments left on merge parent is not ported over', () => {
+        changeComments = new ChangeComments(
+            {/* comments */
+              // comment left on Base
+              'karma.conf.js': [comment4],
+            },
+            {}/* robot comments */,
+            {/* drafts */
+              'karma.conf.js': [draft2],
+            },
+            {/* ported comments */
+              'karma.conf.js': [{
+                ...comment4,
+                line: 31,
+                patch_set: 4,
+              }],
+            },
+            {}/* ported drafts */
+        );
+
+        const portedThreads = changeComments._getPortedCommentThreads(
+            {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 'PARENT'});
+        assert.equal(portedThreads.length, 0);
+
+        assert.equal(changeComments._getPortedCommentThreads(
+            {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: -2}
+        ).length, 0);
+
+        assert.equal(changeComments._getPortedCommentThreads(
+            {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 2}
+        ).length, 0);
+      });
+
       test('ported comments contribute to comment count', () => {
         assert.equal(changeComments.computeCommentsString(
             {basePatchNum: 'PARENT', patchNum: 2}, 'karma.conf.js',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 601ea80..9574c1d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -716,10 +716,19 @@
       }
 
       if (lineNumberEl) {
-        for (const layer of this.layers) {
-          if (typeof layer.annotate === 'function') {
-            layer.annotate(contentText, lineNumberEl, line);
+        const ANNOTATE_MAX_LINE_LENGTH = 1000;
+        // For performance reason, we skip annotating long lines.
+        if (line.text.length < ANNOTATE_MAX_LINE_LENGTH) {
+          for (const layer of this.layers) {
+            if (typeof layer.annotate === 'function') {
+              layer.annotate(contentText, lineNumberEl, line);
+            }
           }
+        } else {
+          const msg =
+            `A line is longer than ${ANNOTATE_MAX_LINE_LENGTH}.` +
+            ' Line annotation was skipped.';
+          console.warn(msg);
         }
       } else {
         console.error('The lineNumberEl is null, skipping layer annotations.');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index ccfab4f..da50135 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -588,7 +588,7 @@
           this._getIgnoreWhitespace(),
           reject
         )
-        .then(resolve);
+        .then(diff => resolve(diff!)); // reject is called in case of error, so we can't get undefined here
     });
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 2c050f9..d09c811 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -1040,6 +1040,7 @@
         commentSide: CommentSide.REVISION,
         path: '/p',
         rootId: 'betsys_confession',
+        mergeParentNum: undefined,
         comments: [{
           id: 'betsys_confession',
           path: '/p',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 828c53d..84b227b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -44,7 +44,6 @@
 import {
   computeAllPatchSets,
   computeLatestPatchNum,
-  patchNumEquals,
   PatchSet,
 } from '../../../utils/patch-set-util';
 import {
@@ -880,12 +879,10 @@
 
   _displayDiffAgainstLatestToast(latestPatchNum?: PatchSetNum) {
     if (!this._patchRange) return;
-    const leftPatchset = patchNumEquals(
-      this._patchRange.basePatchNum,
-      ParentPatchSetNum
-    )
-      ? 'Base'
-      : `Patchset ${this._patchRange.basePatchNum}`;
+    const leftPatchset =
+      this._patchRange.basePatchNum === ParentPatchSetNum
+        ? 'Base'
+        : `Patchset ${this._patchRange.basePatchNum}`;
     fireAlert(
       this,
       `${leftPatchset} vs
@@ -896,12 +893,12 @@
 
   _displayToasts() {
     if (!this._patchRange) return;
-    if (!patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)) {
+    if (this._patchRange.basePatchNum !== ParentPatchSetNum) {
       this._displayDiffBaseAgainstLeftToast();
       return;
     }
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
-    if (!patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    if (this._patchRange.patchNum !== latestPatchNum) {
       this._displayDiffAgainstLatestToast(latestPatchNum);
       return;
     }
@@ -916,17 +913,17 @@
       if (!hasOwnProperty(this._change.revisions, commitSha)) continue;
       const revision = this._change.revisions[commitSha];
       const patchNum = revision._number;
-      if (patchNumEquals(patchNum, this._patchRange.patchNum)) {
+      if (patchNum === this._patchRange.patchNum) {
         commit = commitSha as CommitId;
         const commitObj = revision.commit;
         const parents = commitObj?.parents || [];
         if (
-          patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum) &&
+          this._patchRange.basePatchNum === ParentPatchSetNum &&
           parents.length
         ) {
           baseCommit = parents[parents.length - 1].commit;
         }
-      } else if (patchNumEquals(patchNum, this._patchRange.basePatchNum)) {
+      } else if (patchNum === this._patchRange.basePatchNum) {
         baseCommit = commitSha as CommitId;
       }
     }
@@ -1250,7 +1247,7 @@
     if (!patchRange) return {patchNum, basePatchNum};
     if (
       patchRange.basePatchNum !== ParentPatchSetNum ||
-      !patchNumEquals(patchRange.patchNum, latestPatchNum as PatchSetNum)
+      patchRange.patchNum !== latestPatchNum
     ) {
       patchNum = patchRange.patchNum;
       basePatchNum = patchRange.basePatchNum;
@@ -1352,8 +1349,8 @@
 
     const {basePatchNum, patchNum} = e.detail;
     if (
-      patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
-      patchNumEquals(patchNum, this._patchRange.patchNum)
+      basePatchNum === this._patchRange.basePatchNum &&
+      patchNum === this._patchRange.patchNum
     ) {
       return;
     }
@@ -1582,7 +1579,7 @@
     patchRangeRecord: PolymerDeepPropertyChange<PatchRange, PatchRange>
   ) {
     const patchRange = patchRangeRecord.base || {};
-    return patchNumEquals(patchRange.patchNum, EditPatchSetNum);
+    return patchRange.patchNum === EditPatchSetNum;
   }
 
   _computeBlameToggleLabel(loaded?: boolean, loading?: boolean) {
@@ -1641,7 +1638,7 @@
     if (!this._path) return;
     if (!this._patchRange) return;
 
-    if (patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)) {
+    if (this._patchRange.basePatchNum === ParentPatchSetNum) {
       fireAlert(this, 'Base is already selected.');
       return;
     }
@@ -1658,7 +1655,7 @@
     if (!this._path) return;
     if (!this._patchRange) return;
 
-    if (patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)) {
+    if (this._patchRange.basePatchNum === ParentPatchSetNum) {
       fireAlert(this, 'Left is already base.');
       return;
     }
@@ -1680,7 +1677,7 @@
     if (!this._patchRange) return;
 
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
-    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    if (this._patchRange.patchNum === latestPatchNum) {
       fireAlert(this, 'Latest is already selected.');
       return;
     }
@@ -1700,7 +1697,7 @@
     if (!this._patchRange) return;
 
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
-    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    if (this._patchRange.patchNum === latestPatchNum) {
       fireAlert(this, 'Right is already latest.');
       return;
     }
@@ -1720,8 +1717,8 @@
 
     const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
     if (
-      patchNumEquals(this._patchRange.patchNum, latestPatchNum) &&
-      patchNumEquals(this._patchRange.basePatchNum, ParentPatchSetNum)
+      this._patchRange.patchNum === latestPatchNum &&
+      this._patchRange.basePatchNum === ParentPatchSetNum
     ) {
       fireAlert(this, 'Already diffing base against latest.');
       return;
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 04714bb..39d645e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -38,7 +38,7 @@
   rangesEqual,
 } from './gr-diff-utils';
 import {getHiddenScroll} from '../../../scripts/hiddenscroll';
-import {isMergeParent, patchNumEquals} from '../../../utils/patch-set-util';
+import {isMergeParent} from '../../../utils/patch-set-util';
 import {customElement, observe, property} from '@polymer/decorators';
 import {
   BlameInfo,
@@ -605,10 +605,10 @@
       ? this.patchRange.basePatchNum
       : this.patchRange.patchNum;
 
-    const isEdit = patchNumEquals(patchNum, EditPatchSetNum);
+    const isEdit = patchNum === EditPatchSetNum;
     const isEditBase =
-      patchNumEquals(patchNum, ParentPatchSetNum) &&
-      patchNumEquals(this.patchRange.patchNum, EditPatchSetNum);
+      patchNum === ParentPatchSetNum &&
+      this.patchRange.patchNum === EditPatchSetNum;
 
     if (isEdit) {
       fireAlert(this, 'You cannot comment on an edit.');
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index ec39da2..671bd41 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -30,7 +30,6 @@
   getParentIndex,
   getRevisionByPatchNum,
   isMergeParent,
-  patchNumEquals,
   sortRevisions,
   PatchSet,
 } from '../../../utils/patch-set-util';
@@ -346,7 +345,7 @@
     patchNum: PatchSetNum,
     sortedRevisions: RevisionInfo[]
   ): boolean {
-    if (patchNumEquals(basePatchNum, ParentPatchSetNum)) {
+    if (basePatchNum === ParentPatchSetNum) {
       return false;
     }
 
@@ -441,7 +440,7 @@
       });
       detail.patchNum = patchSetValue;
     } else {
-      if (patchNumEquals(detail.basePatchNum, patchSetValue)) return;
+      if (detail.basePatchNum === patchSetValue) return;
       this.reporting.reportInteraction('left-patchset-changed', {
         previous: detail.basePatchNum,
         current: e.detail.value,
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
index f5e5e04..197a0c4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
@@ -304,7 +304,7 @@
     this._processPromise = util.makeCancelable(
       this._loadHLJS().then(
         () =>
-          new Promise(resolve => {
+          new Promise<void>(resolve => {
             const nextStep = () => {
               this._processHandle = null;
               this._processNextLine(state, rangesCache);
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index b380ce4..df927d6 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -24,7 +24,9 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-table-editor_html';
 import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, property, observe} from '@polymer/decorators';
+import {ServerInfo} from '../../../types/common';
+import {appContext} from '../../../services/app-context';
 
 @customElement('gr-change-table-editor')
 class GrChangeTableEditor extends ChangeTableMixin(
@@ -40,6 +42,27 @@
   @property({type: Boolean, notify: true})
   showNumber?: boolean;
 
+  @property({type: Object})
+  serverConfig?: ServerInfo;
+
+  @property({type: Array})
+  defaultColumns?: string[];
+
+  flagsService = appContext.flagsService;
+
+  @observe('serverConfig')
+  _configChanged(config: ServerInfo) {
+    this.defaultColumns = this.getEnabledColumns(
+      this.columnNames,
+      config,
+      this.flagsService.enabledExperiments
+    );
+    if (!this.displayedColumns) return;
+    this.displayedColumns = this.displayedColumns.filter(column =>
+      this.isColumnEnabled(column, config, this.flagsService.enabledExperiments)
+    );
+  }
+
   /**
    * Get the list of enabled column names from whichever checkboxes are
    * checked (excluding the number checkbox).
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
index 1233cf1..4c87d83 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
@@ -61,7 +61,7 @@
             />
           </td>
         </tr>
-        <template is="dom-repeat" items="[[columnNames]]">
+        <template is="dom-repeat" items="[[defaultColumns]]">
           <tr>
             <td>[[item]]</td>
             <td
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js
index 42085ff..db5c035 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.js
@@ -41,6 +41,9 @@
 
     element.set('displayedColumns', columns);
     element.showNumber = false;
+    element.serverConfig = {
+      change: {},
+    };
     flush();
   });
 
@@ -51,13 +54,25 @@
 
     // The `+ 1` is for the number column, which isn't included in the change
     // table behavior's list.
-    assert.equal(rows.length, element.columnNames.length + 1);
-    for (let i = 0; i < columns.length; i++) {
+    assert.equal(rows.length, element.defaultColumns.length + 1);
+    for (let i = 0; i < element.defaultColumns.length; i++) {
       tds = rows[i + 1].querySelectorAll('td');
-      assert.equal(tds[0].textContent, columns[i]);
+      assert.equal(tds[0].textContent, element.defaultColumns[i]);
     }
   });
 
+  test('disabled experiments are hidden', () => {
+    assert.isFalse(element.displayedColumns.includes('Assignee'));
+    element.set('displayedColumns', columns);
+    element.serverConfig = {
+      change: {
+        enable_assignee: true,
+      },
+    };
+    flush();
+    assert.isTrue(element.displayedColumns.includes('Assignee'));
+  });
+
   test('hide item', () => {
     const checkbox = element.shadowRoot
         .querySelector('table tr:nth-child(2) input');
@@ -80,6 +95,10 @@
       'Branch',
       'Updated',
     ]);
+    // trigger computation of enabled displayed columns
+    element.serverConfig = {
+      change: {},
+    };
     flush();
     const checkbox = element.shadowRoot
         .querySelector('table tr:nth-child(2) input');
@@ -97,12 +116,15 @@
   });
 
   test('_getDisplayedColumns', () => {
-    assert.deepEqual(element._getDisplayedColumns(), columns);
+    const enabledColumns = columns.filter(column => element.isColumnEnabled(
+        column, element.serverConfig, []
+    ));
+    assert.deepEqual(element._getDisplayedColumns(), enabledColumns);
     MockInteractions.tap(
         element.shadowRoot
-            .querySelector('.checkboxContainer input[name=Assignee]'));
+            .querySelector('.checkboxContainer input[name=Subject]'));
     assert.deepEqual(element._getDisplayedColumns(),
-        columns.filter(c => c !== 'Assignee'));
+        enabledColumns.filter(c => c !== 'Subject'));
   });
 
   test('_handleCheckboxContainerClick relays taps to checkboxes', () => {
@@ -140,12 +162,12 @@
 
   test('_handleTargetClick', () => {
     sinon.spy(element, '_handleTargetClick');
-    assert.include(element.displayedColumns, 'Assignee');
+    assert.include(element.displayedColumns, 'Subject');
     MockInteractions
         .tap(element.shadowRoot
-            .querySelector('.checkboxContainer input[name=Assignee]'));
+            .querySelector('.checkboxContainer input[name=Subject]'));
     assert.isTrue(element._handleTargetClick.calledOnce);
-    assert.notInclude(element.displayedColumns, 'Assignee');
+    assert.notInclude(element.displayedColumns, 'Subject');
   });
 });
 
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index e5e8537..809139d 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -147,9 +147,6 @@
   @property({type: Boolean})
   _accountInfoChanged?: boolean;
 
-  @property({type: Array})
-  _changeTableColumnsNotDisplayed?: string[];
-
   @property({type: Object})
   _localPrefs: PreferencesInput = {};
 
@@ -241,8 +238,11 @@
         this.prefs = prefs;
         this._showNumber = !!prefs.legacycid_in_change_table;
         this._copyPrefs(CopyPrefsDirection.PrefsToLocalPrefs);
-        this._cloneMenu(prefs.my);
-        this._cloneChangeTableColumns(prefs.change_table);
+        this._localMenu = this._cloneMenu(prefs.my);
+        this._localChangeTableColumns =
+          prefs.change_table.length === 0
+            ? this.columnNames
+            : this.renameProjectToRepoColumn(prefs.change_table);
       })
     );
 
@@ -341,29 +341,10 @@
   }
 
   _cloneMenu(prefs: TopMenuItemInfo[]) {
-    const menu = [];
-    for (const item of prefs) {
-      menu.push({
-        name: item.name,
-        url: item.url,
-        target: item.target,
-      });
-    }
-    this._localMenu = menu;
-  }
-
-  _cloneChangeTableColumns(changeTable: string[]) {
-    let columns = this.getVisibleColumns(changeTable);
-
-    if (columns.length === 0) {
-      columns = this.columnNames;
-      this._changeTableColumnsNotDisplayed = [];
-    } else {
-      this._changeTableColumnsNotDisplayed = this.getComplementColumns(
-        changeTable
-      );
-    }
-    this._localChangeTableColumns = columns;
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    return prefs.map(({id, ...item}) => {
+      return item;
+    });
   }
 
   @observe('_localChangeTableColumns', '_showNumber')
@@ -437,7 +418,6 @@
   _handleSaveChangeTable() {
     this.set('prefs.change_table', this._localChangeTableColumns);
     this.set('prefs.legacycid_in_change_table', this._showNumber);
-    this._cloneChangeTableColumns(this._localChangeTableColumns);
     return this.restApiService.savePreferences(this.prefs).then(() => {
       this._changeTableChanged = false;
     });
@@ -453,7 +433,6 @@
 
   _handleSaveMenu() {
     this.set('prefs.my', this._localMenu);
-    this._cloneMenu(this._localMenu);
     return this.restApiService.savePreferences(this.prefs).then(() => {
       this._menuChanged = false;
     });
@@ -462,7 +441,7 @@
   _handleResetMenuButton() {
     return this.restApiService.getDefaultPreferences().then(data => {
       if (data?.my) {
-        this._cloneMenu(data.my);
+        this._localMenu = this._cloneMenu(data.my);
       }
     });
   }
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index d1d412f..ec8f6e7 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -340,6 +340,7 @@
       <fieldset id="changeTableColumns">
         <gr-change-table-editor
           show-number="{{_showNumber}}"
+          server-config="[[_serverConfig]]"
           displayed-columns="{{_localChangeTableColumns}}"
         >
         </gr-change-table-editor>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
index 62e2a9e..4c5a2a2 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
@@ -351,9 +351,7 @@
     let newColumns = ['Owner', 'Project', 'Branch'];
     element._localChangeTableColumns = newColumns.slice(0);
     element._showNumber = false;
-    const cloneStub = sinon.stub(element, '_cloneChangeTableColumns');
     element._handleSaveChangeTable();
-    assert.isTrue(cloneStub.calledOnce);
     assert.deepEqual(element.prefs.change_table, newColumns);
     assert.isNotOk(element.prefs.legacycid_in_change_table);
 
@@ -361,7 +359,6 @@
     element._localChangeTableColumns = newColumns;
     element._showNumber = true;
     element._handleSaveChangeTable();
-    assert.isTrue(cloneStub.calledTwice);
     assert.deepEqual(element.prefs.change_table, newColumns);
     assert.isTrue(element.prefs.legacycid_in_change_table);
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index d9a3187..fbf7f47 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -264,10 +264,16 @@
           </gr-account-label>
         </template>
         <template is="dom-if" if="[[showPortedComment]]">
-          <a href="[[_getUrlForComment(comment)]]">
-            <span class="portedMessage">
-              Ported from patchset [[comment.patch_set]]
-            </span>
+          <a href="[[_getUrlForComment(comment)]]"
+            ><span class="portedMessage"
+              >Ported from patchset [[comment.patch_set]]</span
+            ></a
+          >
+          <a
+            href="https://bugs.chromium.org/p/gerrit/issues/entry?template=Porting+Comments"
+            target="_blank"
+          >
+            <iron-icon icon="gr-icons:bug" title="report a problem"></iron-icon>
           </a>
         </template>
         <gr-tooltip-content
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index 1c85523..cc89a7e 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -731,7 +731,7 @@
         path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, line: undefined,
         range: undefined};
       element.comment = comment;
-      flushAsynchronousOperations();
+      flush();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       assert.isTrue(element.editing);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index 37b5348..3fce16e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -32,12 +32,6 @@
 import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
 
 /**
- * fired when the selected value of the dropdown changes
- *
- * @event {change}
- */
-
-/**
  * Requred values are text and value. mobileText and triggerText will
  * fall back to text if not provided.
  *
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
index f313174..c163924 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
@@ -145,7 +145,6 @@
       slot="dropdown-content"
       attr-for-selected="data-value"
       selected="{{value}}"
-      on-tap="_handleDropdownTap"
     >
       <template
         is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index 3b839cb..6f0e84a 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -136,7 +136,7 @@
     this._inputText = this.value;
     this.editing = true;
 
-    return new Promise(resolve => {
+    return new Promise<void>(resolve => {
       this._awaitOpen(resolve);
     });
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index ae6e155..00f9a40 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -18,7 +18,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {getPluginLoader} from './gr-plugin-loader';
-import {patchNumEquals} from '../../../utils/patch-set-util';
+
 import {customElement} from '@polymer/decorators';
 import {
   ChangeInfo,
@@ -154,7 +154,7 @@
 
     let revision;
     for (const rev of Object.values(change.revisions || {})) {
-      if (patchNumEquals(rev._number, patchNum)) {
+      if (rev._number === patchNum) {
         revision = rev;
         break;
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 645e166..b725d3b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -430,7 +430,7 @@
       // and the return type is NodeJS.Timeout object
       let timerId: number;
       this._loadingPromise = Promise.race([
-        new Promise(resolve => (this._loadingResolver = resolve)),
+        new Promise<void>(resolve => (this._loadingResolver = resolve)),
         new Promise(
           (_, reject) =>
             (timerId = window.setTimeout(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
index 68a73933..d29cba7 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -95,7 +95,7 @@
   open() {
     this.returnFocusTo = findActiveElement(document, true) ?? undefined;
     window.addEventListener('popstate', this._boundHandleClose);
-    return new Promise((resolve, reject) => {
+    return new Promise<void>((resolve, reject) => {
       super.open.apply(this);
       if (this._isMobile()) {
         fireEvent(this, 'fullscreen-overlay-opened');
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index 078e07f..9e77784 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -36,11 +36,7 @@
 import {parseDate} from '../../../utils/date-util';
 import {getBaseUrl} from '../../../utils/url-util';
 import {appContext} from '../../../services/app-context';
-import {
-  getParentIndex,
-  isMergeParent,
-  patchNumEquals,
-} from '../../../utils/patch-set-util';
+import {getParentIndex, isMergeParent} from '../../../utils/patch-set-util';
 import {
   ListChangesOption,
   listChangesOptionsToHex,
@@ -1354,7 +1350,7 @@
     let params = undefined;
     if (isMergeParent(patchRange.basePatchNum)) {
       params = {parent: getParentIndex(patchRange.basePatchNum)};
-    } else if (!patchNumEquals(patchRange.basePatchNum, ParentPatchSetNum)) {
+    } else if (patchRange.basePatchNum !== ParentPatchSetNum) {
       params = {base: patchRange.basePatchNum};
     }
     return this._getChangeURLAndFetch({
@@ -1401,7 +1397,7 @@
     changeNum: NumericChangeId,
     patchRange: PatchRange
   ): Promise<FileNameToFileInfoMap | undefined> {
-    if (patchNumEquals(patchRange.patchNum, EditPatchSetNum)) {
+    if (patchRange.patchNum === EditPatchSetNum) {
       return this.getChangeEditFiles(changeNum, patchRange).then(
         res => res && res.files
       );
@@ -1983,9 +1979,10 @@
       }
       return res;
     };
-    const promise = patchNumEquals(patchNum, EditPatchSetNum)
-      ? this._getFileInChangeEdit(changeNum, path)
-      : this._getFileInRevision(changeNum, path, patchNum, suppress404s);
+    const promise =
+      patchNum === EditPatchSetNum
+        ? this._getFileInChangeEdit(changeNum, path)
+        : this._getFileInRevision(changeNum, path, patchNum, suppress404s);
 
     return promise.then(res => {
       if (!res || !res.ok) {
@@ -2277,8 +2274,7 @@
     };
     if (isMergeParent(basePatchNum)) {
       params.parent = getParentIndex(basePatchNum);
-    } else if (!patchNumEquals(basePatchNum, ParentPatchSetNum)) {
-      // TODO (TS): fix as PatchSetNum in the condition above
+    } else if (basePatchNum !== ParentPatchSetNum) {
       params.base = basePatchNum;
     }
     const endpoint = `/files/${encodeURIComponent(path)}/diff`;
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.ts b/polygerrit-ui/app/elements/shared/revision-info/revision-info.ts
index fadbfa7..d4bbe16 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.ts
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.ts
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import {patchNumEquals} from '../../../utils/patch-set-util';
 import {ChangeInfo, PatchSetNum} from '../../../types/common';
 import {ParsedChangeInfo} from '../gr-rest-api-interface/gr-reviewer-updates-parser';
 
@@ -71,8 +70,8 @@
 
   getParentId(patchNum: PatchSetNum, parentIndex: number) {
     if (!this.change.revisions) return;
-    const rev = Object.values(this.change.revisions).find(rev =>
-      patchNumEquals(rev._number, patchNum)
+    const rev = Object.values(this.change.revisions).find(
+      rev => rev._number === patchNum
     );
     if (!rev || !rev.commit) return;
     return rev.commit.parents[parentIndex].commit;
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
index abbcbc8..28b7a50 100644
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
+++ b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
@@ -48,14 +48,6 @@
         'Size',
       ];
 
-      /**
-       * Returns the complement to the given column array
-       *
-       */
-      getComplementColumns(columns: string[]) {
-        return this.columnNames.filter(column => !columns.includes(column));
-      }
-
       isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
         if (!columnsToDisplay || !columnToCheck) {
           return false;
@@ -103,7 +95,7 @@
        * @return If the column was renamed, returns a new array
        * with the corrected name. Otherwise, it returns the original param.
        */
-      getVisibleColumns(columns: string[]) {
+      renameProjectToRepoColumn(columns: string[]) {
         const projectIndex = columns.indexOf('Project');
         if (projectIndex === -1) {
           return columns;
@@ -120,7 +112,6 @@
 
 export interface ChangeTableMixinInterface {
   readonly columnNames: string[];
-  getComplementColumns(columns: string[]): string[];
   isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]): boolean;
   isColumnEnabled(
     column: string,
@@ -132,5 +123,5 @@
     config: ServerInfo,
     experiments: string[]
   ): string[];
-  getVisibleColumns(columns: string[]): string[];
+  renameProjectToRepoColumn(columns: string[]): string[];
 }
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
index daf10ce..0d6b4ad 100644
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
+++ b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
@@ -37,35 +37,6 @@
     element = basicFixture.instantiate();
   });
 
-  test('getComplementColumns', () => {
-    let columns = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Reviewers',
-      'Comments',
-      'Repo',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.deepEqual(element.getComplementColumns(columns), []);
-
-    columns = [
-      'Subject',
-      'Status',
-      'Assignee',
-      'Reviewers',
-      'Comments',
-      'Repo',
-      'Branch',
-      'Size',
-    ];
-    assert.deepEqual(element.getComplementColumns(columns),
-        ['Owner', 'Updated']);
-  });
-
   test('isColumnHidden', () => {
     const columnToCheck = 'Repo';
     let columnsToDisplay = [
@@ -92,15 +63,16 @@
     assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
   });
 
-  test('getVisibleColumns maps Project to Repo', () => {
+  test('renameProjectToRepoColumn maps Project to Repo', () => {
     const columns = [
       'Subject',
       'Status',
       'Owner',
     ];
-    assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
+    assert.deepEqual(element.renameProjectToRepoColumn(columns),
+        columns.slice(0));
     assert.deepEqual(
-        element.getVisibleColumns(columns.concat(['Project'])),
+        element.renameProjectToRepoColumn(columns.concat(['Project'])),
         columns.slice(0).concat(['Repo']));
   });
 });
diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
index c8e9baa..d93b5ea 100644
--- a/polygerrit-ui/app/rollup.config.js
+++ b/polygerrit-ui/app/rollup.config.js
@@ -69,7 +69,13 @@
   output: {
     format: 'iife',
     compact: true,
-    plugins: [terser()]
+    plugins: [
+      terser({
+        output: {
+          comments: false
+        }
+      })
+    ]
   },
   //Context must be set to window to correctly processing global variables
   context: 'window',
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 36fe858..d2b7166 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -24,7 +24,6 @@
  * @desc Experiment ids used in Gerrit.
  */
 export enum KnownExperimentId {
-  PATCHSET_COMMENTS = 'UiFeature__patchset_comments',
   NEW_CONTEXT_CONTROLS = 'UiFeature__new_context_controls',
   CI_REBOOT_CHECKS = 'UiFeature__ci_reboot_checks',
   NEW_CHANGE_SUMMARY_UI = 'UiFeature__new_change_summary_ui',
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 08642af..97c220d 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -161,7 +161,7 @@
  *   ...
  */
 export function listenOnce(el: EventTarget, eventType: string) {
-  return new Promise(resolve => {
+  return new Promise<void>(resolve => {
     const listener = () => {
       removeEventListener();
       resolve();
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 1553187..2398a66 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import {EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PatchSetNum} from './common';
+import {PatchSetNum, UrlEncodedCommentId} from './common';
 import {UIComment} from '../utils/comment-util';
 import {Side} from '../constants/constants';
 import {LineNumber} from '../elements/diff/gr-diff/gr-diff-line';
@@ -214,3 +214,16 @@
   readonly keyCode: number;
   readonly repeat: boolean;
 }
+
+export interface ThreadListModifiedDetail {
+  rootId: UrlEncodedCommentId;
+  path: string;
+}
+
+export type ThreadListModifiedEvent = CustomEvent<ThreadListModifiedDetail>;
+
+declare global {
+  interface HTMLElementEventMap {
+    'thread-list-modified': ThreadListModifiedEvent;
+  }
+}
diff --git a/polygerrit-ui/app/utils/attention-set-util.ts b/polygerrit-ui/app/utils/attention-set-util.ts
index b0aefcb..5b2762c 100644
--- a/polygerrit-ui/app/utils/attention-set-util.ts
+++ b/polygerrit-ui/app/utils/attention-set-util.ts
@@ -17,6 +17,7 @@
 
 import {AccountInfo, ChangeInfo} from '../types/common';
 import {isServiceUser} from './account-util';
+import {hasOwnProperty} from './common-util';
 
 // You would typically use a ServerInfo here, but this utility does not care
 // about all the other parameters in that object.
@@ -46,7 +47,8 @@
   return (
     isAttentionSetEnabled(config) &&
     canHaveAttention(account) &&
-    !!change?.attention_set?.hasOwnProperty(account!._account_id!)
+    !!change?.attention_set &&
+    hasOwnProperty(change?.attention_set, account!._account_id!)
   );
 }
 
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 5d8ea37..f4c0692 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -29,7 +29,7 @@
 import {parseDate} from './date-util';
 import {LineNumber} from '../elements/diff/gr-diff/gr-diff-line';
 import {CommentIdToCommentThreadMap} from '../elements/diff/gr-comment-api/gr-comment-api';
-import {isMergeParent, getParentIndex, patchNumEquals} from './patch-set-util';
+import {isMergeParent, getParentIndex} from './patch-set-util';
 
 export interface DraftCommentProps {
   __draft?: boolean;
@@ -126,6 +126,7 @@
       comments: [comment],
       patchNum: comment.patch_set,
       commentSide: comment.side ?? CommentSide.REVISION,
+      mergeParentNum: comment.parent,
       path: comment.path,
       line: comment.line,
       range: comment.range,
@@ -151,6 +152,11 @@
   comments: UIComment[];
   path: string;
   commentSide: CommentSide;
+  /* mergeParentNum is the merge parent number only valid for merge commits
+     when commentSide is PARENT.
+     mergeParentNum is undefined for auto merge commits
+  */
+  mergeParentNum?: number;
   patchNum?: PatchSetNum;
   line?: LineNumber;
   /* rootId is optional since we create a empty comment thread element for
@@ -196,7 +202,7 @@
   if (
     range.basePatchNum === ParentPatchSetNum &&
     comment.side === CommentSide.PARENT &&
-    patchNumEquals(comment.patch_set, range.patchNum)
+    comment.patch_set === range.patchNum
   ) {
     return true;
   }
@@ -204,7 +210,7 @@
   return (
     range.basePatchNum !== ParentPatchSetNum &&
     comment.side !== CommentSide.PARENT &&
-    patchNumEquals(comment.patch_set, range.basePatchNum)
+    comment.patch_set === range.basePatchNum
   );
 }
 
@@ -217,8 +223,7 @@
   range: PatchRange
 ) {
   return (
-    comment.side !== CommentSide.PARENT &&
-    patchNumEquals(comment.patch_set, range.patchNum)
+    comment.side !== CommentSide.PARENT && comment.patch_set === range.patchNum
   );
 }
 
@@ -247,7 +252,7 @@
       patchNum: comment.patch_set,
       basePatchNum: ParentPatchSetNum,
     };
-  } else if (patchNumEquals(latestPatchNum, comment.patch_set)) {
+  } else if (latestPatchNum === comment.patch_set) {
     return {
       patchNum: latestPatchNum,
       basePatchNum: ParentPatchSetNum,
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 3dd1b9e..5e1ff44 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 
+import {UrlEncodedCommentId} from '../types/common';
 import {FetchRequest} from '../types/types';
 
 export enum EventType {
@@ -23,6 +24,7 @@
   SERVER_ERROR = 'server-error',
   NETWORK_ERROR = 'network-error',
   TITLE_CHANGE = 'title-change',
+  THREAD_LIST_MODIFIED = 'thread-list-modified',
 }
 
 export function fireEvent(target: EventTarget, type: string) {
@@ -83,3 +85,17 @@
     })
   );
 }
+
+export function fireThreadListModifiedEvent(
+  target: EventTarget,
+  rootId: UrlEncodedCommentId,
+  path: string
+) {
+  target.dispatchEvent(
+    new CustomEvent(EventType.THREAD_LIST_MODIFIED, {
+      detail: {rootId, path},
+      composed: true,
+      bubbles: true,
+    })
+  );
+}
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index fda302b..cb0bdec 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -60,19 +60,6 @@
 }
 
 /**
- * As patchNum can be either a string (e.g. 'edit', 'PARENT') OR a number,
- * this function checks for patchNum equality.
- *
- */
-export function patchNumEquals(a?: PatchSetNum, b?: PatchSetNum) {
-  if (a === undefined) {
-    return a === b;
-  }
-  // TODO(TS): replace with a===b when the whole code is converted to ts
-  return `${a}` === `${b}`;
-}
-
-/**
  * Whether the given patch is a numbered parent of a merge (i.e. a negative
  * number).
  */
@@ -114,7 +101,7 @@
   patchNum: PatchSetNum
 ) {
   for (const rev of revisions) {
-    if (patchNumEquals(rev._number, patchNum)) {
+    if (rev._number === patchNum) {
       return rev;
     }
   }
@@ -275,6 +262,20 @@
   return allPatchSets[0].num;
 }
 
+export function computePredecessor(
+  patchset?: PatchSetNum
+): PatchSetNum | undefined {
+  if (
+    !patchset ||
+    patchset === ParentPatchSetNum ||
+    patchset === EditPatchSetNum
+  ) {
+    return undefined;
+  }
+  if (patchset === 1) return ParentPatchSetNum;
+  return (Number(patchset) - 1) as PatchSetNum;
+}
+
 export function hasEditBasedOnCurrentPatchSet(allPatchSets: PatchSet[]) {
   if (!allPatchSets || allPatchSets.length < 2) {
     return false;
diff --git a/polygerrit-ui/app/utils/patch-set-util_test.js b/polygerrit-ui/app/utils/patch-set-util_test.js
index 29cc370..12f5ed4 100644
--- a/polygerrit-ui/app/utils/patch-set-util_test.js
+++ b/polygerrit-ui/app/utils/patch-set-util_test.js
@@ -21,7 +21,7 @@
   fetchChangeUpdates, findEditParentPatchNum, findEditParentRevision,
   getParentIndex, getRevisionByPatchNum,
   isMergeParent,
-  patchNumEquals, sortRevisions,
+  sortRevisions,
 } from './patch-set-util.js';
 
 suite('gr-patch-set-util tests', () => {
@@ -31,9 +31,9 @@
       {_number: 1},
       {_number: 2},
     ];
-    assert.deepEqual(getRevisionByPatchNum(revisions, '1'), revisions[1]);
+    assert.deepEqual(getRevisionByPatchNum(revisions, 1), revisions[1]);
     assert.deepEqual(getRevisionByPatchNum(revisions, 2), revisions[2]);
-    assert.equal(getRevisionByPatchNum(revisions, '3'), undefined);
+    assert.equal(getRevisionByPatchNum(revisions, 3), undefined);
   });
 
   test('fetchChangeUpdates on latest', done => {
@@ -231,17 +231,6 @@
         .assertWip(6, true); // PS6 was uploaded with WIP option
   });
 
-  test('patchNumEquals', () => {
-    assert.isFalse(patchNumEquals('edit', 'PARENT'));
-    assert.isFalse(patchNumEquals('edit', NaN));
-    assert.isFalse(patchNumEquals(1, '2'));
-
-    assert.isTrue(patchNumEquals(1, '1'));
-    assert.isTrue(patchNumEquals(1, 1));
-    assert.isTrue(patchNumEquals('edit', 'edit'));
-    assert.isTrue(patchNumEquals('PARENT', 'PARENT'));
-  });
-
   test('isMergeParent', () => {
     assert.isFalse(isMergeParent(1));
     assert.isFalse(isMergeParent(4321));
diff --git a/proto/cache.proto b/proto/cache.proto
index 0676f1b..8d575b7 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -599,3 +599,43 @@
   string change_type = 9; // ENUM as string
   string patch_type = 10; // ENUM as string
 }
+
+// Serialized form of
+// com.google.gerrit.server.patch.fileDiff.FileDiffCacheKey
+// Next ID: 8
+message FileDiffKeyProto {
+  string project = 1;
+  bytes old_commit = 2;
+  bytes new_commit = 3;
+  string file_path = 4;
+  int32 rename_score = 5;
+  string diff_algorithm = 6;
+  string whitespace = 7;
+}
+
+// Serialized form of
+// com.google.gerrit.server.patch.filediff.FileDiffOutput
+// Next ID: 9
+message FileDiffOutputProto {
+  // Next ID: 5
+  message Edit {
+    int32 begin_a = 1;
+    int32 end_a = 2;
+    int32 begin_b = 3;
+    int32 end_b = 4;
+  }
+  // Serialized form  of com.google.gerrit.server.patch.filediff.TaggedEdit
+  // Next ID: 3
+  message TaggedEdit {
+    Edit edit = 1;
+    bool due_to_rebase = 2;
+  }
+  string old_path = 1;
+  string new_path = 2;
+  string change_type = 3;
+  string patch_type = 4;
+  repeated string header_lines = 5;
+  int64 size = 6;
+  int64 size_delta = 7;
+  repeated TaggedEdit edits = 8;
+}
diff --git a/tools/coverage.sh b/tools/coverage.sh
index c92d5cf..3582d22 100755
--- a/tools/coverage.sh
+++ b/tools/coverage.sh
@@ -33,15 +33,24 @@
 cp -r {java,javatests}/* ${destdir}/java
 
 mkdir -p ${destdir}/plugins
-for plugin in `find plugins/ -maxdepth 1 -type d`
+for plugin in `find plugins/ -mindepth 1 -maxdepth 1 -type d`
 do
   mkdir -p ${destdir}/${plugin}/java
-  cp -r plugins/*/{java,javatests}/* ${destdir}/${plugin}/java
+  if [ -e ${plugin}/java/ ]
+    then cp -r ${plugin}/java/* ${destdir}/${plugin}/java
+  fi
+  if [ -e ${plugin}/javatests/ ]
+    then cp -r ${plugin}/javatests/* ${destdir}/${plugin}/java
+  fi
 
   # for backwards compatibility support plugins with old file structure
   mkdir -p ${destdir}/${plugin}/src/{main,test}/java
-  cp -r plugins/*/src/main/java/* ${destdir}/${plugin}/src/main/java
-  cp -r plugins/*/src/test/java/* ${destdir}/${plugin}/src/test/java
+  if [ -e ${plugin}/src/main/java/ ]
+    then cp -r ${plugin}/src/main/java/* ${destdir}/${plugin}/src/main/java/
+  fi
+  if [ -e ${plugin}/src/test/java/ ]
+    then cp -r ${plugin}/src/test/java/* ${destdir}/${plugin}/src/test/java/
+  fi
 done
 
 base=$(bazel info bazel-testlogs)
diff --git a/tools/js/BUILD b/tools/js/BUILD
index fedaf7f..1a272e2 100644
--- a/tools/js/BUILD
+++ b/tools/js/BUILD
@@ -1 +1 @@
-exports_files(["run_npm_binary.py"])
+exports_files(["run_npm_binary.py", "eslint-chdir.js"])
diff --git a/tools/js/eslint-chdir.js b/tools/js/eslint-chdir.js
new file mode 100644
index 0000000..5aea704
--- /dev/null
+++ b/tools/js/eslint-chdir.js
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Eslint 7 introduced a breaking change - it uses the current workdir instead
+// of the configuration file directory for resolving relative paths:
+// https://eslint.org/docs/user-guide/migrating-to-7.0.0#base-path-change
+// This file is loaded before the eslint and sets the current directory
+// back to the location of configuration file.
+
+const path = require('path');
+const configParamIndex =
+    process.argv.findIndex(arg => arg === '-c' || arg === '---config');
+if (configParamIndex >= 0 && configParamIndex + 1 < process.argv.length) {
+  const dirName = path.dirname(process.argv[configParamIndex + 1]);
+  process.chdir(dirName);
+}
diff --git a/tools/js/eslint.bzl b/tools/js/eslint.bzl
index 586b1c5..b32e2bc 100644
--- a/tools/js/eslint.bzl
+++ b/tools/js/eslint.bzl
@@ -57,9 +57,11 @@
         config,
         ignore,
         "//tools/js/eslint-rules:eslint-rules-srcs",
+        "//tools/js:eslint-chdir.js",
         eslint_rules_toplevel_file,
     ] + plugins + data
     common_templated_args = [
+        "--node_options=--require=$$(rlocation $(rootpath //tools/js:eslint-chdir.js))",
         "--ext",
         ",".join(extensions),
         "-c",
@@ -85,7 +87,7 @@
             "*_test_require_patch.js",
             "--ignore-pattern",
             "*_test_loader.js",
-            native.package_name(),
+            "./", # Relative to the config file location
         ],
         # Should not run sandboxed.
         tags = [
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 970a4a9..f6d1e3b 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -47,6 +47,9 @@
       <name>Joerg Zieren</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 74c4769..daf8089 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -47,6 +47,9 @@
       <name>Joerg Zieren</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index e8fae82..07895a4 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -47,6 +47,9 @@
       <name>Joerg Zieren</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index be6688a..a28adea 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -47,6 +47,9 @@
       <name>Joerg Zieren</name>
     </developer>
     <developer>
+      <name>Jacek Centkowski</name>
+    </developer>
+    <developer>
       <name>Luca Milanesio</name>
     </developer>
     <developer>
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 81448e9..fdada50 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -16,7 +16,7 @@
     "rollup": "^2.3.4",
     "rollup-plugin-node-resolve": "^5.2.0",
     "rollup-plugin-terser": "^5.1.3",
-    "typescript": "3.9.5"
+    "typescript": "4.0.5"
   },
   "devDependencies": {},
   "license": "Apache-2.0",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index b1412c4..338e490 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -7893,10 +7893,10 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@3.9.5:
-  version "3.9.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
-  integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
+typescript@4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
+  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
 
 typical@^2.6.0, typical@^2.6.1:
   version "2.6.1"
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index af5a900..4279183 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -1,5 +1,5 @@
 load("//tools/bzl:maven_jar.bzl", "maven_jar")
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
 
 GUAVA_VERSION = "29.0-jre"
 GUAVA_BIN_SHA1 = "801142b4c3d0f0770dd29abea50906cacfddd447"
@@ -98,7 +98,8 @@
     )
 
     # When upgrading elasticsearch-rest-client, also upgrade httpcore-nio
-    # and httpasyncclient as necessary.
+    # and httpasyncclient as necessary. Consider also the other
+    # org.apache.httpcomponents dependencies in ../WORKSPACE.
     maven_jar(
         name = "elasticsearch-rest-client",
         artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.8.1",
diff --git a/tools/remote-bazelrc b/tools/remote-bazelrc
index 13c498e..0aeb8d5 100644
--- a/tools/remote-bazelrc
+++ b/tools/remote-bazelrc
@@ -30,36 +30,20 @@
 
 # Set several flags related to specifying the platform, toolchain and java
 # properties.
-# These flags are duplicated rather than imported from (for example)
-# %workspace%/configs/ubuntu16_04_clang/1.2/toolchain.bazelrc to make this
-# bazelrc a standalone file that can be copied more easily.
-# These flags should only be used as is for the rbe-ubuntu16-04 container
-# and need to be adapted to work with other toolchain containers.
-build:remote --host_javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:jdk8
-build:remote --javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:jdk8
-build:remote --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
-build:remote --java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8
-build:remote --crosstool_top=@bazel_toolchains//configs/ubuntu16_04_clang/1.2/bazel_0.25.0/default:toolchain
+build:remote --host_javabase=@rbe_jdk11//java:jdk
+build:remote --javabase=@rbe_jdk11//java:jdk
+build:remote --crosstool_top=@rbe_jdk11//cc:toolchain
+build:remote --extra_toolchains=@rbe_jdk11//config:cc-toolchain
+build:remote --extra_execution_platforms=@rbe_jdk11//config:platform
+build:remote --host_platform=@rbe_jdk11//config:platform
+build:remote --platforms=@rbe_jdk11//config:platform
 build:remote --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
-# Platform flags:
-# The toolchain container used for execution is defined in the target indicated
-# by "extra_execution_platforms", "host_platform" and "platforms".
-# If you are using your own toolchain container, you need to create a platform
-# target with "constraint_values" that allow for the toolchain specified with
-# "extra_toolchains" to be selected (given constraints defined in
-# "exec_compatible_with").
-# More about platforms: https://docs.bazel.build/versions/master/platforms.html
-build:remote --extra_toolchains=@bazel_toolchains//configs/ubuntu16_04_clang/1.2/bazel_0.25.0/cpp:cc-toolchain-clang-x86_64-default
-build:remote --extra_execution_platforms=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:rbe_ubuntu1604
-build:remote --host_platform=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:rbe_ubuntu1604
-build:remote --platforms=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:rbe_ubuntu1604
 
 # Set various strategies so that all actions execute remotely. Mixing remote
 # and local execution will lead to errors unless the toolchain and remote
 # machine exactly match the host machine.
 build:remote --spawn_strategy=remote,sandboxed
 build:remote --strategy=Javac=remote
-build:remote --strategy=Closure=remote
 build:remote --strategy=Genrule=remote
 build:remote --define=EXECUTOR=remote
 
@@ -78,20 +62,6 @@
 # account credential instead.
 build:remote --auth_enabled=true
 
-# The following flags are only necessary for local docker sandboxing
-# with the rbe-ubuntu16-04 container. Use of these flags is still experimental.
-build:docker-sandbox --host_javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:jdk8
-build:docker-sandbox --javabase=@bazel_toolchains//configs/ubuntu16_04_clang/1.2:jdk8
-build:docker-sandbox --crosstool_top=@bazel_toolchains//configs/ubuntu16_04_clang/1.2/bazel_0.25.0/default:toolchain
-build:docker-sandbox --experimental_docker_image=gcr.io/cloud-marketplace/google/rbe-ubuntu16-04@sha256:da0f21c71abce3bbb92c3a0c44c3737f007a82b60f8bd2930abc55fe64fc2729
-build:docker-sandbox --spawn_strategy=docker
-build:docker-sandbox --strategy=Javac=docker
-build:docker-sandbox --strategy=Closure=docker
-build:docker-sandbox --strategy=Genrule=docker
-build:docker-sandbox --define=EXECUTOR=remote
-build:docker-sandbox --experimental_docker_verbose
-build:docker-sandbox --experimental_enable_docker_sandbox
-
 # The following flags enable the remote cache so action results can be shared
 # across machines, developers, and workspaces.
 build:remote-cache --remote_cache=remotebuildexecution.googleapis.com
@@ -100,5 +70,4 @@
 build:remote-cache --auth_enabled=true
 build:remote-cache --spawn_strategy=standalone
 build:remote-cache --strategy=Javac=standalone
-build:remote-cache --strategy=Closure=standalone
 build:remote-cache --strategy=Genrule=standalone
diff --git a/yarn.lock b/yarn.lock
index 119edf5..3653ee5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -505,6 +505,22 @@
     source-map-support "0.5.9"
     tsutils "2.27.2"
 
+"@eslint/eslintrc@^0.2.2":
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76"
+  integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.1.1"
+    espree "^7.3.0"
+    globals "^12.1.0"
+    ignore "^4.0.6"
+    import-fresh "^3.2.1"
+    js-yaml "^3.13.1"
+    lodash "^4.17.19"
+    minimatch "^3.0.4"
+    strip-json-comments "^3.1.1"
+
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -513,11 +529,32 @@
     call-me-maybe "^1.0.1"
     glob-to-regexp "^0.3.0"
 
+"@nodelib/fs.scandir@2.1.3":
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
+  integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.3"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
+  integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
+
 "@nodelib/fs.stat@^1.1.2":
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
   integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
 
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
+  integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.3"
+    fastq "^1.6.0"
+
 "@octokit/endpoint@^5.1.0":
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.2.1.tgz#e5ef98bc4a41fad62b17e71af1a1710f6076b8df"
@@ -769,11 +806,6 @@
   resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a"
   integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==
 
-"@types/eslint-visitor-keys@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
-  integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
-
 "@types/estree@0.0.39":
   version "0.0.39"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@@ -884,6 +916,11 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
   integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
 
+"@types/json5@^0.0.29":
+  version "0.0.29"
+  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+
 "@types/launchpad@^0.6.0":
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
@@ -912,9 +949,9 @@
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
 "@types/minimist@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
-  integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
+  integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
 
 "@types/mz@0.0.29":
   version "0.0.29"
@@ -1225,49 +1262,76 @@
     "@types/events" "*"
     "@types/inquirer" "*"
 
-"@typescript-eslint/eslint-plugin@2.31.0":
-  version "2.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.31.0.tgz#942c921fec5e200b79593c71fafb1e3f57aa2e36"
-  integrity sha512-iIC0Pb8qDaoit+m80Ln/aaeu9zKQdOLF4SHcGLarSeY1gurW6aU4JsOPMjKQwXlw70MvWKZQc6S2NamA8SJ/gg==
+"@typescript-eslint/eslint-plugin@^4.11.0", "@typescript-eslint/eslint-plugin@^4.2.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.11.0.tgz#bc6c1e4175c0cf42083da4314f7931ad12f731cc"
+  integrity sha512-x4arJMXBxyD6aBXLm3W7mSDZRiABzy+2PCLJbL7OPqlp53VXhaA1HKK7R2rTee5OlRhnUgnp8lZyVIqjnyPT6g==
   dependencies:
-    "@typescript-eslint/experimental-utils" "2.31.0"
+    "@typescript-eslint/experimental-utils" "4.11.0"
+    "@typescript-eslint/scope-manager" "4.11.0"
+    debug "^4.1.1"
     functional-red-black-tree "^1.0.1"
     regexpp "^3.0.0"
+    semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/experimental-utils@2.31.0":
-  version "2.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.31.0.tgz#a9ec514bf7fd5e5e82bc10dcb6a86d58baae9508"
-  integrity sha512-MI6IWkutLYQYTQgZ48IVnRXmLR/0Q6oAyJgiOror74arUMh7EWjJkADfirZhRsUMHeLJ85U2iySDwHTSnNi9vA==
+"@typescript-eslint/experimental-utils@4.11.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.11.0.tgz#d1a47cc6cfe1c080ce4ead79267574b9881a1565"
+  integrity sha512-1VC6mSbYwl1FguKt8OgPs8xxaJgtqFpjY/UzUYDBKq4pfQ5lBvN2WVeqYkzf7evW42axUHYl2jm9tNyFsb8oLg==
   dependencies:
     "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/typescript-estree" "2.31.0"
+    "@typescript-eslint/scope-manager" "4.11.0"
+    "@typescript-eslint/types" "4.11.0"
+    "@typescript-eslint/typescript-estree" "4.11.0"
     eslint-scope "^5.0.0"
     eslint-utils "^2.0.0"
 
-"@typescript-eslint/parser@2.31.0":
-  version "2.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.31.0.tgz#beddd4e8efe64995108b229b2862cd5752d40d6f"
-  integrity sha512-uph+w6xUOlyV2DLSC6o+fBDzZ5i7+3/TxAsH4h3eC64tlga57oMb96vVlXoMwjR/nN+xyWlsnxtbDkB46M2EPQ==
+"@typescript-eslint/parser@^4.2.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.11.0.tgz#1dd3d7e42708c10ce9f3aa64c63c0ab99868b4e2"
+  integrity sha512-NBTtKCC7ZtuxEV5CrHUO4Pg2s784pvavc3cnz6V+oJvVbK4tH9135f/RBP6eUA2KHiFKAollSrgSctQGmHbqJQ==
   dependencies:
-    "@types/eslint-visitor-keys" "^1.0.0"
-    "@typescript-eslint/experimental-utils" "2.31.0"
-    "@typescript-eslint/typescript-estree" "2.31.0"
-    eslint-visitor-keys "^1.1.0"
-
-"@typescript-eslint/typescript-estree@2.31.0":
-  version "2.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.31.0.tgz#ac536c2d46672aa1f27ba0ec2140d53670635cfd"
-  integrity sha512-vxW149bXFXXuBrAak0eKHOzbcu9cvi6iNcJDzEtOkRwGHxJG15chiAQAwhLOsk+86p9GTr/TziYvw+H9kMaIgA==
-  dependencies:
+    "@typescript-eslint/scope-manager" "4.11.0"
+    "@typescript-eslint/types" "4.11.0"
+    "@typescript-eslint/typescript-estree" "4.11.0"
     debug "^4.1.1"
-    eslint-visitor-keys "^1.1.0"
-    glob "^7.1.6"
+
+"@typescript-eslint/scope-manager@4.11.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.11.0.tgz#2d906537db8a3a946721699e4fc0833810490254"
+  integrity sha512-6VSTm/4vC2dHM3ySDW9Kl48en+yLNfVV6LECU8jodBHQOhO8adAVizaZ1fV0QGZnLQjQ/y0aBj5/KXPp2hBTjA==
+  dependencies:
+    "@typescript-eslint/types" "4.11.0"
+    "@typescript-eslint/visitor-keys" "4.11.0"
+
+"@typescript-eslint/types@4.11.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.11.0.tgz#86cf95e7eac4ccfd183f9fcf1480cece7caf4ca4"
+  integrity sha512-XXOdt/NPX++txOQHM1kUMgJUS43KSlXGdR/aDyEwuAEETwuPt02Nc7v+s57PzuSqMbNLclblQdv3YcWOdXhQ7g==
+
+"@typescript-eslint/typescript-estree@4.11.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.11.0.tgz#1144d145841e5987d61c4c845442a24b24165a4b"
+  integrity sha512-eA6sT5dE5RHAFhtcC+b5WDlUIGwnO9b0yrfGa1mIOIAjqwSQCpXbLiFmKTdRbQN/xH2EZkGqqLDrKUuYOZ0+Hg==
+  dependencies:
+    "@typescript-eslint/types" "4.11.0"
+    "@typescript-eslint/visitor-keys" "4.11.0"
+    debug "^4.1.1"
+    globby "^11.0.1"
     is-glob "^4.0.1"
     lodash "^4.17.15"
-    semver "^6.3.0"
+    semver "^7.3.2"
     tsutils "^3.17.1"
 
+"@typescript-eslint/visitor-keys@4.11.0":
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.11.0.tgz#906669a50f06aa744378bb84c7d5c4fdbc5b7d51"
+  integrity sha512-tRYKyY0i7cMk6v4UIOCjl1LhuepC/pc6adQqJk4Is3YcC6k46HvsV9Wl7vQoLbm9qADgeujiT7KdLrylvFIQ+A==
+  dependencies:
+    "@typescript-eslint/types" "4.11.0"
+    eslint-visitor-keys "^2.0.0"
+
 "@webcomponents/webcomponentsjs@^1.0.7":
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
@@ -1298,10 +1362,10 @@
   dependencies:
     acorn "^3.0.4"
 
-acorn-jsx@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
-  integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
+acorn-jsx@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
+  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
 
 acorn@^3.0.4:
   version "3.3.0"
@@ -1318,10 +1382,10 @@
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3"
   integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==
 
-acorn@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
-  integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
+acorn@^7.4.0:
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
 adm-zip@~0.4.3:
   version "0.4.13"
@@ -1340,7 +1404,7 @@
   dependencies:
     es6-promisify "^5.0.0"
 
-ajv@^6.10.0, ajv@^6.10.2:
+ajv@^6.10.0:
   version "6.10.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
   integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==
@@ -1350,6 +1414,16 @@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ajv@^6.12.4:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
 ajv@^6.5.5:
   version "6.10.1"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.1.tgz#ebf8d3af22552df9dd049bfbe50cc2390e823593"
@@ -1381,6 +1455,11 @@
   dependencies:
     string-width "^3.0.0"
 
+ansi-colors@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
 ansi-escapes@^1.1.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
@@ -1392,11 +1471,11 @@
   integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
 
 ansi-escapes@^4.2.1:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d"
-  integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
+  integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==
   dependencies:
-    type-fest "^0.8.1"
+    type-fest "^0.11.0"
 
 ansi-regex@^2.0.0:
   version "2.1.1"
@@ -1423,13 +1502,20 @@
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
   integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
 
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
   integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
   dependencies:
     color-convert "^1.9.0"
 
+ansi-styles@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
 ansi-styles@^4.1.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
@@ -1559,13 +1645,15 @@
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
   integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
 
-array-includes@^3.0.3:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
-  integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
+array-includes@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8"
+  integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0"
+    es-abstract "^1.18.0-next.1"
+    get-intrinsic "^1.0.1"
     is-string "^1.0.5"
 
 array-union@^1.0.1:
@@ -1575,6 +1663,11 @@
   dependencies:
     array-uniq "^1.0.1"
 
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
 array-uniq@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@@ -1590,13 +1683,14 @@
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-array.prototype.flat@^1.2.1:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
-  integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+array.prototype.flat@^1.2.3:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
+  integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
+    es-abstract "^1.18.0-next.1"
 
 arraybuffer.slice@~0.0.7:
   version "0.0.7"
@@ -1608,11 +1702,6 @@
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
 
-arrify@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
-  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
-
 asn1@~0.2.3:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -1630,10 +1719,10 @@
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
 
-astral-regex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
-  integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+astral-regex@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+  integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
 async-each@^1.0.0:
   version "1.0.3"
@@ -2183,6 +2272,13 @@
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
+braces@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
 browser-capabilities@^1.0.0:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/browser-capabilities/-/browser-capabilities-1.1.4.tgz#a6bd657a07a134532ad66c722b8949904478b973"
@@ -2292,6 +2388,14 @@
     normalize-url "^4.1.0"
     responselike "^1.0.2"
 
+call-bind@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
+  integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.0"
+
 call-me-maybe@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
@@ -2342,16 +2446,11 @@
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
   integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
 
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
-camelcase@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
-  integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
-
 cancel-token@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/cancel-token/-/cancel-token-0.1.1.tgz#c18197674bb1c84c1d6933ebf15d8d5a5ce79b4f"
@@ -2369,7 +2468,7 @@
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
-chalk@*, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@*, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2397,7 +2496,7 @@
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chalk@^4.0.0:
+chalk@^4.0.0, chalk@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
   integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
@@ -2488,9 +2587,9 @@
   integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
 
 cli-boxes@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d"
-  integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
+  integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
 
 cli-cursor@^1.0.1:
   version "1.0.2"
@@ -2525,6 +2624,11 @@
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
   integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
 
+cli-width@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
+  integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
+
 clone-buffer@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
@@ -2691,10 +2795,10 @@
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
   integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
 
-comment-parser@^0.7.2:
-  version "0.7.2"
-  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.2.tgz#baf6d99b42038678b81096f15b630d18142f4b8a"
-  integrity sha512-4Rjb1FnxtOcv9qsfuaNuVsmmVn4ooVoBHzYfyKteiXwIU84PClyGA5jASoFMwPV93+FPh9spwueXauxFJZkGAg==
+comment-parser@^0.7.6:
+  version "0.7.6"
+  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12"
+  integrity sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==
 
 commondir@^1.0.1:
   version "1.0.1"
@@ -2914,7 +3018,7 @@
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^7.0.0:
+cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -3004,6 +3108,13 @@
   dependencies:
     ms "^2.1.1"
 
+debug@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+  dependencies:
+    ms "2.1.2"
+
 debug@~3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
@@ -3019,7 +3130,7 @@
     decamelize "^1.1.0"
     map-obj "^1.0.0"
 
-decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0:
+decamelize@^1.1.0, decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3046,7 +3157,7 @@
   resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
   integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
 
-deep-is@~0.1.3:
+deep-is@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
@@ -3196,6 +3307,13 @@
     arrify "^1.0.1"
     path-type "^3.0.0"
 
+dir-glob@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+  dependencies:
+    path-type "^4.0.0"
+
 doctrine@1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -3218,12 +3336,13 @@
   dependencies:
     esutils "^2.0.2"
 
-dom-serializer@0:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
-  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+dom-serializer@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1"
+  integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==
   dependencies:
     domelementtype "^2.0.1"
+    domhandler "^4.0.0"
     entities "^2.0.0"
 
 dom-urls@^1.1.0:
@@ -3242,30 +3361,33 @@
     clone "^2.1.0"
     parse5 "^4.0.0"
 
-domelementtype@1, domelementtype@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
-  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+domelementtype@^2.0.1, domelementtype@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e"
+  integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==
 
-domelementtype@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
-  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
-
-domhandler@^2.3.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
-  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+domhandler@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a"
+  integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==
   dependencies:
-    domelementtype "1"
+    domelementtype "^2.0.1"
 
-domutils@^1.5.1:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
-  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+domhandler@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e"
+  integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==
   dependencies:
-    dom-serializer "0"
-    domelementtype "1"
+    domelementtype "^2.1.0"
+
+domutils@^2.4.2:
+  version "2.4.4"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
+  integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
 
 dot-prop@^3.0.0:
   version "3.0.0"
@@ -3282,9 +3404,9 @@
     is-obj "^1.0.0"
 
 dot-prop@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
-  integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
+  integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
   dependencies:
     is-obj "^2.0.0"
 
@@ -3422,15 +3544,17 @@
     engine.io-parser "~2.2.0"
     ws "^7.1.2"
 
-entities@^1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
-  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+enquirer@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+  dependencies:
+    ansi-colors "^4.1.1"
 
 entities@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
-  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
 
 env-variable@0.0.x:
   version "0.0.5"
@@ -3459,22 +3583,23 @@
     string-template "~0.2.1"
     xtend "~4.0.0"
 
-es-abstract@^1.17.0, es-abstract@^1.17.0-next.1:
-  version "1.17.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
-  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+es-abstract@^1.18.0-next.1:
+  version "1.18.0-next.1"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
+  integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
   dependencies:
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     has "^1.0.3"
     has-symbols "^1.0.1"
-    is-callable "^1.1.5"
-    is-regex "^1.0.5"
-    object-inspect "^1.7.0"
+    is-callable "^1.2.2"
+    is-negative-zero "^2.0.0"
+    is-regex "^1.1.1"
+    object-inspect "^1.8.0"
     object-keys "^1.1.1"
-    object.assign "^4.1.0"
-    string.prototype.trimleft "^2.1.1"
-    string.prototype.trimright "^2.1.1"
+    object.assign "^4.1.1"
+    string.prototype.trimend "^1.0.1"
+    string.prototype.trimstart "^1.0.1"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
@@ -3517,30 +3642,30 @@
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-eslint-config-google@^0.13.0:
-  version "0.13.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.13.0.tgz#e277d16d2cb25c1ffd3fd13fb0035ad7421382fe"
-  integrity sha512-ELgMdOIpn0CFdsQS+FuxO+Ttu4p+aLaXHv9wA9yVnzqlUGV7oN/eRRnJekk7TCur6Cu2FXX0fqfIXRBaM14lpQ==
+eslint-config-google@^0.14.0:
+  version "0.14.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
+  integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
 
-eslint-config-prettier@^6.10.1:
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1"
-  integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==
+eslint-config-prettier@^6.12.0:
+  version "6.15.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9"
+  integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==
   dependencies:
     get-stdin "^6.0.0"
 
-eslint-import-resolver-node@^0.3.2:
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404"
-  integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==
+eslint-import-resolver-node@^0.3.4:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
+  integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
   dependencies:
     debug "^2.6.9"
     resolve "^1.13.1"
 
-eslint-module-utils@^2.4.1:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz#7878f7504824e1b857dd2505b59a8e5eda26a708"
-  integrity sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==
+eslint-module-utils@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
+  integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
   dependencies:
     debug "^2.6.9"
     pkg-dir "^2.0.0"
@@ -3553,44 +3678,44 @@
     eslint-utils "^2.0.0"
     regexpp "^3.0.0"
 
-eslint-plugin-html@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.0.0.tgz#28e5c3e71e6f612e07e73d7c215e469766628c13"
-  integrity sha512-PQcGippOHS+HTbQCStmH5MY1BF2MaU8qW/+Mvo/8xTa/ioeMXdSP+IiaBw2+nh0KEMfYQKuTz1Zo+vHynjwhbg==
+eslint-plugin-html@^6.1.1:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.1.1.tgz#95aee151900b9bb2da5fa017b45cc64456a0a74e"
+  integrity sha512-JSe3ZDb7feKMnQM27XWGeoIjvP4oWQMJD9GZ6wW67J7/plVL87NK72RBwlvfc3tTZiYUchHhxAwtgEd1GdofDA==
   dependencies:
-    htmlparser2 "^3.10.1"
+    htmlparser2 "^5.0.1"
 
-eslint-plugin-import@^2.20.1:
-  version "2.20.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz#802423196dcb11d9ce8435a5fc02a6d3b46939b3"
-  integrity sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==
+eslint-plugin-import@^2.22.1:
+  version "2.22.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702"
+  integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==
   dependencies:
-    array-includes "^3.0.3"
-    array.prototype.flat "^1.2.1"
+    array-includes "^3.1.1"
+    array.prototype.flat "^1.2.3"
     contains-path "^0.1.0"
     debug "^2.6.9"
     doctrine "1.5.0"
-    eslint-import-resolver-node "^0.3.2"
-    eslint-module-utils "^2.4.1"
+    eslint-import-resolver-node "^0.3.4"
+    eslint-module-utils "^2.6.0"
     has "^1.0.3"
     minimatch "^3.0.4"
-    object.values "^1.1.0"
+    object.values "^1.1.1"
     read-pkg-up "^2.0.0"
-    resolve "^1.12.0"
+    resolve "^1.17.0"
+    tsconfig-paths "^3.9.0"
 
-eslint-plugin-jsdoc@^19.2.0:
-  version "19.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-19.2.0.tgz#f522b970878ae402b28ce62187305b33dfe2c834"
-  integrity sha512-QdNifBFLXCDGdy+26RXxcrqzEZarFWNybCZQVqJQYEYPlxd6lm+LPkrs6mCOhaGc2wqC6zqpedBQFX8nQJuKSw==
+eslint-plugin-jsdoc@^30.7.9:
+  version "30.7.9"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-30.7.9.tgz#b0a9881990edc1bc8a635467bad9edfae9ecbaaf"
+  integrity sha512-qMM0fNx7/6OCnIh3jRpIrEBAhTG1THNXXbr3yfJ8yqLrDbzJR98xsstX25xt9GCPlrjNc/bBpTHfJQOvn7nVMA==
   dependencies:
-    comment-parser "^0.7.2"
-    debug "^4.1.1"
-    jsdoctypeparser "^6.1.0"
-    lodash "^4.17.15"
-    object.entries-ponyfill "^1.0.1"
-    regextras "^0.7.0"
-    semver "^6.3.0"
-    spdx-expression-parse "^3.0.0"
+    comment-parser "^0.7.6"
+    debug "^4.3.1"
+    jsdoctypeparser "^9.0.0"
+    lodash "^4.17.20"
+    regextras "^0.7.1"
+    semver "^7.3.4"
+    spdx-expression-parse "^3.0.1"
 
 eslint-plugin-node@^11.1.0:
   version "11.1.0"
@@ -3604,17 +3729,10 @@
     resolve "^1.10.1"
     semver "^6.1.0"
 
-eslint-plugin-prettier@^3.1.2:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2"
-  integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==
-  dependencies:
-    prettier-linter-helpers "^1.0.0"
-
-eslint-plugin-prettier@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz#ae116a0fc0e598fdae48743a4430903de5b4e6ca"
-  integrity sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==
+eslint-plugin-prettier@^3.1.4, eslint-plugin-prettier@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz#61e295349a65688ffac0b7808ef0a8244bdd8d40"
+  integrity sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==
   dependencies:
     prettier-linter-helpers "^1.0.0"
 
@@ -3626,14 +3744,15 @@
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
-eslint-utils@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
-  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+eslint-scope@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
   dependencies:
-    eslint-visitor-keys "^1.1.0"
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
 
-eslint-utils@^2.0.0:
+eslint-utils@^2.0.0, eslint-utils@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
   integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
@@ -3645,46 +3764,56 @@
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
   integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
 
-eslint@^6.6.0, eslint@^6.8.0:
-  version "6.8.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
-  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+eslint-visitor-keys@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
+  integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
+
+eslint-visitor-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
+  integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
+
+eslint@^7.10.0, eslint@^7.16.0:
+  version "7.16.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.16.0.tgz#a761605bf9a7b32d24bb7cde59aeb0fd76f06092"
+  integrity sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==
   dependencies:
     "@babel/code-frame" "^7.0.0"
+    "@eslint/eslintrc" "^0.2.2"
     ajv "^6.10.0"
-    chalk "^2.1.0"
-    cross-spawn "^6.0.5"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
     debug "^4.0.1"
     doctrine "^3.0.0"
-    eslint-scope "^5.0.0"
-    eslint-utils "^1.4.3"
-    eslint-visitor-keys "^1.1.0"
-    espree "^6.1.2"
-    esquery "^1.0.1"
+    enquirer "^2.3.5"
+    eslint-scope "^5.1.1"
+    eslint-utils "^2.1.0"
+    eslint-visitor-keys "^2.0.0"
+    espree "^7.3.1"
+    esquery "^1.2.0"
     esutils "^2.0.2"
-    file-entry-cache "^5.0.1"
+    file-entry-cache "^6.0.0"
     functional-red-black-tree "^1.0.1"
     glob-parent "^5.0.0"
     globals "^12.1.0"
     ignore "^4.0.6"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
-    inquirer "^7.0.0"
     is-glob "^4.0.0"
     js-yaml "^3.13.1"
     json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.3.0"
-    lodash "^4.17.14"
+    levn "^0.4.1"
+    lodash "^4.17.19"
     minimatch "^3.0.4"
-    mkdirp "^0.5.1"
     natural-compare "^1.4.0"
-    optionator "^0.8.3"
+    optionator "^0.9.1"
     progress "^2.0.0"
-    regexpp "^2.0.1"
-    semver "^6.1.2"
-    strip-ansi "^5.2.0"
-    strip-json-comments "^3.0.1"
-    table "^5.2.3"
+    regexpp "^3.1.0"
+    semver "^7.2.1"
+    strip-ansi "^6.0.0"
+    strip-json-comments "^3.1.0"
+    table "^6.0.4"
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
@@ -3696,26 +3825,26 @@
     acorn "^5.5.0"
     acorn-jsx "^3.0.0"
 
-espree@^6.1.2:
-  version "6.1.2"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d"
-  integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==
+espree@^7.3.0, espree@^7.3.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
+  integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
   dependencies:
-    acorn "^7.1.0"
-    acorn-jsx "^5.1.0"
-    eslint-visitor-keys "^1.1.0"
+    acorn "^7.4.0"
+    acorn-jsx "^5.3.1"
+    eslint-visitor-keys "^1.3.0"
 
 esprima@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
-esquery@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
-  integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
+esquery@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
+  integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
   dependencies:
-    estraverse "^4.0.0"
+    estraverse "^5.1.0"
 
 esrecurse@^4.1.0:
   version "4.2.1"
@@ -3724,11 +3853,23 @@
   dependencies:
     estraverse "^4.1.0"
 
-estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.0, estraverse@^4.1.1:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
   integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
 
+estraverse@^5.1.0, estraverse@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
+  integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
+
 esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@@ -3770,19 +3911,19 @@
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
-execa@^4.0.0:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240"
-  integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q==
+execa@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
+  integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==
   dependencies:
-    cross-spawn "^7.0.0"
-    get-stream "^5.0.0"
-    human-signals "^1.1.1"
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
     is-stream "^2.0.0"
     merge-stream "^2.0.0"
-    npm-run-path "^4.0.0"
-    onetime "^5.1.0"
-    signal-exit "^3.0.2"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
 exit-hook@^1.0.0:
@@ -3953,6 +4094,11 @@
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
   integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
 
+fast-deep-equal@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
 fast-diff@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
@@ -3970,12 +4116,24 @@
     merge2 "^1.2.3"
     micromatch "^3.1.10"
 
+fast-glob@^3.1.1:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
+  integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.0"
+    merge2 "^1.3.0"
+    micromatch "^4.0.2"
+    picomatch "^2.2.1"
+
 fast-json-stable-stringify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
   integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
 
-fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -3985,6 +4143,13 @@
   resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz#04b26106cc56681f51a044cfc0d76cf0008ac2c2"
   integrity sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==
 
+fastq@^1.6.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb"
+  integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==
+  dependencies:
+    reusify "^1.0.4"
+
 fd-slicer@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -4013,18 +4178,18 @@
     escape-string-regexp "^1.0.5"
 
 figures@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec"
-  integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
   dependencies:
     escape-string-regexp "^1.0.5"
 
-file-entry-cache@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
-  integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+file-entry-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a"
+  integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==
   dependencies:
-    flat-cache "^2.0.1"
+    flat-cache "^3.0.4"
 
 filename-regex@^2.0.0:
   version "2.0.1"
@@ -4052,6 +4217,13 @@
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
 filled-array@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84"
@@ -4146,19 +4318,18 @@
   dependencies:
     readable-stream "^2.0.2"
 
-flat-cache@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
-  integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
   dependencies:
-    flatted "^2.0.0"
-    rimraf "2.6.3"
-    write "1.0.3"
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
 
-flatted@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
-  integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
+flatted@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067"
+  integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
 
 follow-redirects@^1.0.0:
   version "1.9.0"
@@ -4286,6 +4457,15 @@
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
+get-intrinsic@^1.0.0, get-intrinsic@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
+  integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+
 get-stdin@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -4308,13 +4488,18 @@
   dependencies:
     pump "^3.0.0"
 
-get-stream@^5.0.0, get-stream@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
-  integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
+get-stream@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
   dependencies:
     pump "^3.0.0"
 
+get-stream@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718"
+  integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -4372,6 +4557,13 @@
   dependencies:
     is-glob "^4.0.1"
 
+glob-parent@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-stream@^5.3.2:
   version "5.3.5"
   resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22"
@@ -4425,7 +4617,7 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+glob@^7.1.3, glob@^7.1.4:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -4445,11 +4637,11 @@
     ini "^1.3.4"
 
 global-dirs@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201"
-  integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d"
+  integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==
   dependencies:
-    ini "^1.3.5"
+    ini "1.3.7"
 
 global-modules@^0.2.3:
   version "0.2.3"
@@ -4506,6 +4698,18 @@
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
   integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
 
+globby@^11.0.1:
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
+  integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==
+  dependencies:
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.1.1"
+    ignore "^5.1.4"
+    merge2 "^1.3.0"
+    slash "^3.0.0"
+
 globby@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/globby/-/globby-4.1.0.tgz#080f54549ec1b82a6c60e631fc82e1211dbe95f8"
@@ -4634,25 +4838,25 @@
   dependencies:
     lodash "^4.17.2"
 
-gts@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/gts/-/gts-2.0.2.tgz#b8b28de99361b5c5c24db30a375a0f546bbc04a4"
-  integrity sha512-SLytzl2IqKXf6kGULwr07XQ9lVsvjrzFD3OAA7DEfIQYuD+lKBPt/cZ/RYGxaWerY4PTfmnXT7KdxEr9Ec8uHQ==
+gts@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/gts/-/gts-3.0.3.tgz#36716d87680c2d1a0e02867c91bb7169cede9369"
+  integrity sha512-XHFGhDzoyaHDVHlhNfz369erxuSEIyVMtJoRAz8Dt2NpidG5pOJzI/44lWhsLVigph64TgAWPWBsBTQFCQ1mTw==
   dependencies:
-    "@typescript-eslint/eslint-plugin" "2.31.0"
-    "@typescript-eslint/parser" "2.31.0"
-    chalk "^4.0.0"
-    eslint "^6.8.0"
-    eslint-config-prettier "^6.10.1"
+    "@typescript-eslint/eslint-plugin" "^4.2.0"
+    "@typescript-eslint/parser" "^4.2.0"
+    chalk "^4.1.0"
+    eslint "^7.10.0"
+    eslint-config-prettier "^6.12.0"
     eslint-plugin-node "^11.1.0"
-    eslint-plugin-prettier "^3.1.2"
-    execa "^4.0.0"
-    inquirer "^7.1.0"
-    meow "^7.0.0"
+    eslint-plugin-prettier "^3.1.4"
+    execa "^5.0.0"
+    inquirer "^7.3.3"
+    meow "^8.0.0"
     ncp "^2.0.0"
-    prettier "^2.0.4"
+    prettier "^2.1.2"
     rimraf "^3.0.2"
-    update-notifier "^4.1.0"
+    update-notifier "^5.0.0"
     write-file-atomic "^3.0.3"
 
 gulp-if@^2.0.2:
@@ -4838,6 +5042,13 @@
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
   integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
 
+hosted-git-info@^3.0.6:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c"
+  integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
 hpack.js@^2.1.6:
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -4861,17 +5072,15 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
-htmlparser2@^3.10.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
-  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+htmlparser2@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7"
+  integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==
   dependencies:
-    domelementtype "^1.3.1"
-    domhandler "^2.3.0"
-    domutils "^1.5.1"
-    entities "^1.1.1"
-    inherits "^2.0.1"
-    readable-stream "^3.1.1"
+    domelementtype "^2.0.1"
+    domhandler "^3.3.0"
+    domutils "^2.4.2"
+    entities "^2.0.0"
 
 http-cache-semantics@^4.0.0:
   version "4.1.0"
@@ -4948,10 +5157,10 @@
     agent-base "^4.3.0"
     debug "^3.1.0"
 
-human-signals@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
-  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+human-signals@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 
 iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
   version "0.4.24"
@@ -4982,7 +5191,7 @@
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
-ignore@^5.1.1:
+ignore@^5.1.1, ignore@^5.1.4:
   version "5.1.8"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
   integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@@ -4995,6 +5204,14 @@
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
 
+import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
 import-lazy@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@@ -5045,7 +5262,12 @@
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
-ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+ini@1.3.7:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
+  integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
+
+ini@^1.3.4, ini@~1.3.0:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
@@ -5089,40 +5311,21 @@
     strip-ansi "^5.1.0"
     through "^2.3.6"
 
-inquirer@^7.0.0:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.3.tgz#f9b4cd2dff58b9f73e8d43759436ace15bed4567"
-  integrity sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw==
+inquirer@^7.3.3:
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
+  integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
   dependencies:
     ansi-escapes "^4.2.1"
-    chalk "^2.4.2"
+    chalk "^4.1.0"
     cli-cursor "^3.1.0"
-    cli-width "^2.0.0"
+    cli-width "^3.0.0"
     external-editor "^3.0.3"
     figures "^3.0.0"
-    lodash "^4.17.15"
-    mute-stream "0.0.8"
-    run-async "^2.2.0"
-    rxjs "^6.5.3"
-    string-width "^4.1.0"
-    strip-ansi "^5.1.0"
-    through "^2.3.6"
-
-inquirer@^7.1.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.2.0.tgz#63ce99d823090de7eb420e4bb05e6f3449aa389a"
-  integrity sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ==
-  dependencies:
-    ansi-escapes "^4.2.1"
-    chalk "^3.0.0"
-    cli-cursor "^3.1.0"
-    cli-width "^2.0.0"
-    external-editor "^3.0.3"
-    figures "^3.0.0"
-    lodash "^4.17.15"
+    lodash "^4.17.19"
     mute-stream "0.0.8"
     run-async "^2.4.0"
-    rxjs "^6.5.3"
+    rxjs "^6.6.0"
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
     through "^2.3.6"
@@ -5185,10 +5388,10 @@
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
-  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+is-callable@^1.1.4, is-callable@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
+  integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
 
 is-ci@^1.0.10:
   version "1.2.1"
@@ -5204,6 +5407,13 @@
   dependencies:
     ci-info "^2.0.0"
 
+is-core-module@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
+  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+  dependencies:
+    has "^1.0.3"
+
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -5338,7 +5548,7 @@
     global-dirs "^0.1.0"
     is-path-inside "^1.0.0"
 
-is-installed-globally@^0.3.1:
+is-installed-globally@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
   integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
@@ -5346,15 +5556,20 @@
     global-dirs "^2.0.1"
     is-path-inside "^3.0.1"
 
+is-negative-zero@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+
 is-npm@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
   integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
 
-is-npm@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
-  integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
+is-npm@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8"
+  integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==
 
 is-number@^2.1.0:
   version "2.1.0"
@@ -5375,6 +5590,11 @@
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
   integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
 
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
 is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -5458,12 +5678,12 @@
   resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
   integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
 
-is-regex@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
-  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+is-regex@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
+  integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
   dependencies:
-    has "^1.0.3"
+    has-symbols "^1.0.1"
 
 is-retry-allowed@^1.0.0:
   version "1.1.0"
@@ -5618,10 +5838,10 @@
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
-jsdoctypeparser@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz#acfb936c26300d98f1405cb03e20b06748e512a8"
-  integrity sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA==
+jsdoctypeparser@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26"
+  integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==
 
 jsesc@^1.3.0:
   version "1.3.0"
@@ -5648,6 +5868,11 @@
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -5668,6 +5893,13 @@
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
 json5@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
@@ -5747,7 +5979,7 @@
   dependencies:
     package-json "^4.0.0"
 
-latest-version@^5.0.0:
+latest-version@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
   integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
@@ -5780,13 +6012,13 @@
   dependencies:
     readable-stream "^2.0.5"
 
-levn@^0.3.0, levn@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
-  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
   dependencies:
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
 
 lines-and-columns@^1.1.6:
   version "1.1.6"
@@ -5947,6 +6179,11 @@
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.12.tgz#a712c74fdc31f7ecb20fe44f157d802d208097ef"
   integrity sha512-+CiwtLnsJhX03p20mwXuvhoebatoh5B3tt+VvYlrPgZC1g36y+RRbkufX95Xa+X4I59aWEacDFYwnJZiyBh9gA==
 
+lodash@^4.17.19, lodash@^4.17.20:
+  version "4.17.20"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+  integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
 log-symbols@^1.0.0, log-symbols@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -6031,6 +6268,13 @@
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
 macos-release@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
@@ -6147,24 +6391,22 @@
     redent "^1.0.0"
     trim-newlines "^1.0.0"
 
-meow@^7.0.0:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/meow/-/meow-7.0.1.tgz#1ed4a0a50b3844b451369c48362eb0515f04c1dc"
-  integrity sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==
+meow@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-8.0.0.tgz#1aa10ee61046719e334ffdc038bb5069250ec99a"
+  integrity sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg==
   dependencies:
     "@types/minimist" "^1.2.0"
-    arrify "^2.0.1"
-    camelcase "^6.0.0"
     camelcase-keys "^6.2.2"
     decamelize-keys "^1.1.0"
     hard-rejection "^2.1.0"
-    minimist-options "^4.0.2"
-    normalize-package-data "^2.5.0"
+    minimist-options "4.1.0"
+    normalize-package-data "^3.0.0"
     read-pkg-up "^7.0.1"
     redent "^3.0.0"
     trim-newlines "^3.0.0"
-    type-fest "^0.13.1"
-    yargs-parser "^18.1.3"
+    type-fest "^0.18.0"
+    yargs-parser "^20.2.3"
 
 merge-descriptors@1.0.1:
   version "1.0.1"
@@ -6188,6 +6430,11 @@
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
   integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==
 
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@@ -6231,6 +6478,14 @@
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
+micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
 mime-db@1.40.0, mime-db@^1.28.0:
   version "1.40.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
@@ -6309,7 +6564,7 @@
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist-options@^4.0.2:
+minimist-options@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
   integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
@@ -6373,7 +6628,7 @@
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
-ms@^2.1.1:
+ms@2.1.2, ms@^2.1.1:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
@@ -6549,6 +6804,16 @@
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-package-data@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.0.tgz#1f8a7c423b3d2e85eb36985eaf81de381d01301a"
+  integrity sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw==
+  dependencies:
+    hosted-git-info "^3.0.6"
+    resolve "^1.17.0"
+    semver "^7.3.2"
+    validate-npm-package-license "^3.0.1"
+
 normalize-path@^2.0.0, normalize-path@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@@ -6586,7 +6851,7 @@
   dependencies:
     path-key "^2.0.0"
 
-npm-run-path@^4.0.0:
+npm-run-path@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
   integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
@@ -6632,10 +6897,10 @@
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
-  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+object-inspect@^1.8.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
+  integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
 
 object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
@@ -6659,10 +6924,15 @@
     has-symbols "^1.0.0"
     object-keys "^1.0.11"
 
-object.entries-ponyfill@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz#29abdf77cbfbd26566dd1aa24e9d88f65433d256"
-  integrity sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY=
+object.assign@^4.1.1:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    has-symbols "^1.0.1"
+    object-keys "^1.1.1"
 
 object.omit@^2.0.0:
   version "2.0.1"
@@ -6679,14 +6949,14 @@
   dependencies:
     isobject "^3.0.1"
 
-object.values@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
-  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+object.values@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731"
+  integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
 obuf@^1.0.0, obuf@^1.1.1:
@@ -6735,10 +7005,10 @@
   dependencies:
     mimic-fn "^1.0.0"
 
-onetime@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
-  integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
+onetime@^5.1.0, onetime@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
   dependencies:
     mimic-fn "^2.1.0"
 
@@ -6757,17 +7027,17 @@
     minimist "~0.0.1"
     wordwrap "~0.0.2"
 
-optionator@^0.8.3:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
-  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
   dependencies:
-    deep-is "~0.1.3"
-    fast-levenshtein "~2.0.6"
-    levn "~0.3.0"
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-    word-wrap "~1.2.3"
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
 
 ordered-read-streams@^0.3.0:
   version "0.3.0"
@@ -6962,13 +7232,13 @@
     json-parse-better-errors "^1.0.1"
 
 parse-json@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f"
-  integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
+  integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     error-ex "^1.3.1"
-    json-parse-better-errors "^1.0.1"
+    json-parse-even-better-errors "^2.3.0"
     lines-and-columns "^1.1.6"
 
 parse-passwd@^1.0.0:
@@ -7094,6 +7364,11 @@
   dependencies:
     pify "^3.0.0"
 
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
 peek-stream@^1.1.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67"
@@ -7123,6 +7398,11 @@
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
+picomatch@^2.0.5, picomatch@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
 pify@^2.0.0, pify@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -7446,10 +7726,10 @@
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
   integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
 
-prelude-ls@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
-  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
 prepend-http@^1.0.1:
   version "1.0.4"
@@ -7473,11 +7753,16 @@
   dependencies:
     fast-diff "^1.1.2"
 
-prettier@2.0.5, prettier@^2.0.4:
+prettier@2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
   integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
 
+prettier@^2.1.2:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
+  integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
+
 pretty-bytes@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
@@ -7583,10 +7868,10 @@
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-pupa@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726"
-  integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==
+pupa@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62"
+  integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==
   dependencies:
     escape-goat "^2.0.0"
 
@@ -7861,12 +8146,7 @@
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexpp@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
-  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
-
-regexpp@^3.0.0:
+regexpp@^3.0.0, regexpp@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
   integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
@@ -7883,10 +8163,10 @@
     unicode-match-property-ecmascript "^1.0.4"
     unicode-match-property-value-ecmascript "^1.1.0"
 
-regextras@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.0.tgz#2298bef8cfb92b1b7e3b9b12aa8f69547b7d71e4"
-  integrity sha512-ds+fL+Vhl918gbAUb0k2gVKbTZLsg84Re3DI6p85Et0U0tYME3hyW4nMK8Px4dtDaBA2qNjvG5uWyW7eK5gfmw==
+regextras@^0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2"
+  integrity sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==
 
 registry-auth-token@^3.0.1:
   version "3.4.0"
@@ -7897,9 +8177,9 @@
     safe-buffer "^5.0.1"
 
 registry-auth-token@^4.0.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479"
-  integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250"
+  integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==
   dependencies:
     rc "^1.2.8"
 
@@ -8042,11 +8322,12 @@
   dependencies:
     path-parse "^1.0.6"
 
-resolve@^1.12.0, resolve@^1.13.1:
-  version "1.15.1"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
-  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+resolve@^1.13.1, resolve@^1.17.0:
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
+  integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
   dependencies:
+    is-core-module "^2.1.0"
     path-parse "^1.0.6"
 
 resolve@^1.5.0:
@@ -8092,7 +8373,12 @@
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
-rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
   integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
@@ -8146,6 +8432,11 @@
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
   integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
 
+run-parallel@^1.1.9:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef"
+  integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==
+
 rx@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
@@ -8158,10 +8449,10 @@
   dependencies:
     tslib "^1.9.0"
 
-rxjs@^6.5.3:
-  version "6.5.4"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
-  integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
+rxjs@^6.6.0:
+  version "6.6.3"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
+  integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
   dependencies:
     tslib "^1.9.0"
 
@@ -8261,11 +8552,18 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
   integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
 
-semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
+semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
+semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
+  version "7.3.4"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
+  integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
+  dependencies:
+    lru-cache "^6.0.0"
+
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -8392,6 +8690,11 @@
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
   integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
 
+signal-exit@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
 simple-swizzle@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@@ -8423,14 +8726,19 @@
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
   integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
 
-slice-ansi@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
-  integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+slice-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+  integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
   dependencies:
-    ansi-styles "^3.2.0"
-    astral-regex "^1.0.0"
-    is-fullwidth-code-point "^2.0.0"
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
 
 slide@^1.1.5:
   version "1.1.6"
@@ -8607,6 +8915,14 @@
     spdx-exceptions "^2.1.0"
     spdx-license-ids "^3.0.0"
 
+spdx-expression-parse@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+  integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
 spdx-license-ids@^3.0.0:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1"
@@ -8748,7 +9064,7 @@
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
-string-width@^4.0.0, string-width@^4.1.0:
+string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
   integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
@@ -8757,21 +9073,21 @@
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimleft@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
-  integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==
+string.prototype.trimend@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
+  integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    function-bind "^1.1.1"
 
-string.prototype.trimright@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
-  integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==
+string.prototype.trimstart@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
+  integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
   dependencies:
+    call-bind "^1.0.0"
     define-properties "^1.1.3"
-    function-bind "^1.1.1"
 
 string_decoder@^1.1.1:
   version "1.3.0"
@@ -8806,7 +9122,7 @@
   dependencies:
     ansi-regex "^3.0.0"
 
-strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+strip-ansi@^5.1.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
   integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@@ -8882,10 +9198,10 @@
   dependencies:
     min-indent "^1.0.0"
 
-strip-json-comments@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
-  integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
+strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
 strip-json-comments@~2.0.1:
   version "2.0.1"
@@ -8946,15 +9262,15 @@
     typical "^2.6.1"
     wordwrapjs "^3.0.0"
 
-table@^5.2.3:
-  version "5.4.6"
-  resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
-  integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+table@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/table/-/table-6.0.4.tgz#c523dd182177e926c723eb20e1b341238188aa0d"
+  integrity sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==
   dependencies:
-    ajv "^6.10.2"
-    lodash "^4.17.14"
-    slice-ansi "^2.1.0"
-    string-width "^3.0.0"
+    ajv "^6.12.4"
+    lodash "^4.17.20"
+    slice-ansi "^4.0.0"
+    string-width "^4.2.0"
 
 tar-fs@^1.12.0:
   version "1.16.3"
@@ -9037,9 +9353,9 @@
     execa "^0.7.0"
 
 term-size@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753"
-  integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
+  integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
 
 ternary-stream@^2.0.1:
   version "2.0.1"
@@ -9209,6 +9525,13 @@
     is-number "^3.0.0"
     repeat-string "^1.6.1"
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
 to-regex@^3.0.1, to-regex@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -9259,6 +9582,16 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
+tsconfig-paths@^3.9.0:
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
+  integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
+  dependencies:
+    "@types/json5" "^0.0.29"
+    json5 "^1.0.1"
+    minimist "^1.2.0"
+    strip-bom "^3.0.0"
+
 tslib@^1.8.1:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
@@ -9295,22 +9628,27 @@
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
-type-check@~0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
-  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
   dependencies:
-    prelude-ls "~1.1.2"
+    prelude-ls "^1.2.1"
 
 type-detect@^4.0.0:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-fest@^0.13.1:
-  version "0.13.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
-  integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
+type-fest@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
+  integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
+
+type-fest@^0.18.0:
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
+  integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
 
 type-fest@^0.6.0:
   version "0.6.0"
@@ -9342,10 +9680,10 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@3.9.5:
-  version "3.9.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
-  integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
+typescript@4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
+  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
 
 typical@^2.6.1:
   version "2.6.1"
@@ -9507,22 +9845,23 @@
     semver-diff "^2.0.0"
     xdg-basedir "^3.0.0"
 
-update-notifier@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3"
-  integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==
+update-notifier@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.0.1.tgz#1f92d45fb1f70b9e33880a72dd262bc12d22c20d"
+  integrity sha512-BuVpRdlwxeIOvmc32AGYvO1KVdPlsmqSh8KDDBxS6kDE5VR7R8OMP1d8MdhaVBvxl4H3551k9akXr0Y1iIB2Wg==
   dependencies:
     boxen "^4.2.0"
-    chalk "^3.0.0"
+    chalk "^4.1.0"
     configstore "^5.0.1"
     has-yarn "^2.1.0"
     import-lazy "^2.1.0"
     is-ci "^2.0.0"
-    is-installed-globally "^0.3.1"
-    is-npm "^4.0.0"
+    is-installed-globally "^0.3.2"
+    is-npm "^5.0.0"
     is-yarn-global "^0.3.0"
-    latest-version "^5.0.0"
-    pupa "^2.0.1"
+    latest-version "^5.1.0"
+    pupa "^2.1.1"
+    semver "^7.3.2"
     semver-diff "^3.1.1"
     xdg-basedir "^4.0.0"
 
@@ -9893,7 +10232,7 @@
     p-try "^2.1.0"
     pify "^4.0.1"
 
-word-wrap@~1.2.3:
+word-wrap@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
@@ -9944,13 +10283,6 @@
     signal-exit "^3.0.2"
     typedarray-to-buffer "^3.1.5"
 
-write@1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
-  integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
-  dependencies:
-    mkdirp "^0.5.1"
-
 ws@^7.1.2:
   version "7.2.1"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
@@ -10010,13 +10342,15 @@
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
   integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
 
-yargs-parser@^18.1.3:
-  version "18.1.3"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
-  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yargs-parser@^20.2.3:
+  version "20.2.4"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
+  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
 
 yauzl@^2.10.0:
   version "2.10.0"