Merge "Add frontend option to skip diff computations for change details."
diff --git a/.bazelrc b/.bazelrc
index 8b1abd6..d6d4ce6 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,7 +4,6 @@
 build --action_env=PATH
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
 build --java_toolchain //tools:error_prone_warnings_toolchain
-build --incompatible_disallow_load_labels_to_cross_package_boundaries=false
 
 test --build_tests_only
 test --test_output=errors
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 5b1c151..91cb87f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2046,14 +2046,6 @@
 By default unset, meaning no bug report URL will be displayed. Administrators
 should set this to the URL of their issue tracker, if necessary.
 
-[[gerrit.reportBugText]]gerrit.reportBugText::
-+
-Text to be displayed in the link to the bug report URL.
-+
-Only used when `gerrit.reportBugUrl` is set.
-+
-Defaults to "Report Bug".
-
 [[gerrit.enableReverseDnsLookup]]gerrit.enableReverseDnsLookup::
 +
 Enable reverse DNS lookup during computing ref log entry for identified user,
@@ -3516,17 +3508,6 @@
 If no groups are added, any user will be allowed to execute
 'receive-pack' on the server.
 
-[[receive.allowPushToRefsChanges]]receive.allowPushToRefsChanges::
-+
-If true, it is possible to push directly to a change using `refs/changes/`.
-The possibility to push to `refs/changes/` is deprecated and it might be
-removed in future releases.
-See link:user-upload.html#manual_replacement_mapping[Manual Replacement Mapping].
-+
-False means pushing to `refs/changes/` is prohibited.
-+
-Defaults to false.
-
 [[receive.certNonceSeed]]receive.certNonceSeed::
 +
 If set to a non-empty value and server-side signed push validation is
@@ -3809,6 +3790,14 @@
 Defaults to link:#retry.timeout[`retry.timeout`]; unit suffixes are supported,
 and assumes milliseconds if not specified.
 
+[[retry.retryWithTraceOnFailure]]retry.retryWithTraceOnFailure::
++
+Whether Gerrit should automatically retry operations on failure with tracing
+enabled. The automatically generated traces can help with debugging. Please
+note that only some of the REST endpoints support automatic retry.
++
+By default this is set to false.
+
 [[rules]]
 === Section rules
 
@@ -4571,6 +4560,52 @@
 +
 By default, true.
 
+[[tracing.traceid]]
+==== Subsection tracing.<trace-id>
+
+There can be multiple `tracing.<trace-id>` subsections to configure
+automatic tracing of requests. To be traced a request must match all
+conditions of one `tracing.<trace-id>` subsection. The subsection name
+is used as trace ID. Using this trace ID administrators can find
+matching log entries.
+
+[[tracing.traceid.requestType]]tracing.<trace-id>.requestType::
++
+Type of request for which request tracing should be always enabled (can
+be `GIT_RECEIVE`, `GIT_UPLOAD`, `REST` and `SSH`).
++
+May be specified multiple times.
++
+By default, unset (all request types are matched).
+
+[[tracing.traceid.requestUriPattern]]tracing.<trace-id>.requestUriPattern::
++
+Regular expression to match request URIs for which request tracing
+should be always enabled. Request URIs are only available for REST
+requests. Request URIs never include the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (all request URIs are matched).
+
+[[tracing.traceid.account]]tracing.<trace-id>.account::
++
+Account ID of an account for which request tracing should be always
+enabled.
++
+May be specified multiple times.
++
+By default, unset (all accounts are matched).
+
+[[tracing.traceid.projectPattern]]tracing.<trace-id>.projectPattern::
++
+Regular expression to match project names for which request tracing
+should be always enabled.
++
+May be specified multiple times.
++
+By default, unset (all projects are matched).
+
 [[trackingid]]
 === Section trackingid
 
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index ff43520..9c90ba7 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -297,12 +297,13 @@
 [[label_copyAllScoresOnTrivialRebase]]
 === `label.Label-Name.copyAllScoresOnTrivialRebase`
 
-If true, all scores for the label are copied forward when a new patch
-set is uploaded that is a trivial rebase. A new patch set is considered
-as trivial rebase if the commit message is the same as in the previous
-patch set and if it has the same code delta as the previous patch set.
-This is the case if the change was rebased onto a different parent, or
-if the parent did not change at all.
+If true, all scores for the label are copied forward when a new patch set is
+uploaded that is a trivial rebase. A new patch set is considered to be trivial
+rebase if the commit message is the same as in the previous patch set and if it
+has the same diff (including context lines) as the previous patch set. This is
+the case if the change was rebased onto a different parent and that rebase did
+not require git to perform any conflict resolution, or if the parent did not
+change at all.
 
 This can be used to enable sticky approvals, reducing turn-around for
 trivial rebases prior to submitting a change.
@@ -313,13 +314,13 @@
 [[label_copyAllScoresIfNoCodeChange]]
 === `label.Label-Name.copyAllScoresIfNoCodeChange`
 
-If true, all scores for the label are copied forward when a new patch
-set is uploaded that has the same parent tree as the previous patch
-set and the same code delta as the previous patch set. This means only
-the commit message is different. This can be used to enable sticky
-approvals on labels that only depend on the code, reducing turn-around
-if only the commit message is changed prior to submitting a change.
-For the Verified label that is optionally installed by the
+If true, all scores for the label are copied forward when a new patch set is
+uploaded that has the same parent tree as the previous patch set and the same
+code diff (including context lines) as the previous patch set. This means only
+the commit message is different; the change hasn't even been rebased. This can
+be used to enable sticky approvals on labels that only depend on the code,
+reducing turn-around if only the commit message is changed prior to submitting a
+change. For the Verified label that is optionally installed by the
 link:pgm-init.html[init] site program this is enabled by default.
 
 Defaults to false.
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 91a054a..08c26d4 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -157,32 +157,6 @@
 did  what, especially with patches. Default is `INHERIT`, which means that this
 property is inherited from the parent project.
 
-[[receive.requireChangeId]]receive.requireChangeId::
-+
-The `Require Change-Id in commit message` option defines whether a
-link:user-changeid.html[Change-Id] in the commit message is required
-for pushing a commit for review. If this option is set, trying to push
-a commit for review that doesn't contain a Change-Id in the commit
-message fails with link:error-missing-changeid.html[missing Change-Id
-in commit message footer].
-
-It is recommended to set this option and use a
-link:user-changeid.html#create[commit-msg hook] (or other client side
-tooling like EGit) to automatically generate Change-Id's for new
-commits. This way the Change-Id is automatically in place when changes
-are reworked or rebased and uploading new patch sets gets easy.
-
-If this option is not set, commits can be uploaded without a Change-Id,
-but then users have to remember to copy the assigned Change-Id from the
-change screen and insert it manually into the commit message when they
-want to upload a second patch set.
-
-Default is `INHERIT`, which means that this property is inherited from
-the parent project. The global default for new hosts is `true`
-
-This option is deprecated and future releases will behave as if this
-is always `true`.
-
 [[receive.maxObjectSizeLimit]]receive.maxObjectSizeLimit::
 +
 Maximum allowed Git object size that receive-pack will accept. If an object
@@ -321,7 +295,7 @@
 
 - 'action': defines the link:#submit-type[submit type].  Valid
 values are 'fast forward only', 'merge if necessary', 'rebase if necessary',
-'merge always' and 'cherry pick'.  The default is 'merge if necessary'.
+'rebase always', 'merge always' and 'cherry pick'.  The default is 'merge if necessary'.
 
 - 'matchAuthorToCommitterDate': Defines whether to the author date will be changed to match the
 submitter date upon submit, so that git log shows when the change was submitted instead of when the
@@ -494,8 +468,9 @@
 [[fast_forward_only]]
 * Fast Forward Only
 +
-With this method no merge commits are produced. All merges must
-be handled on the client, prior to uploading to Gerrit for review.
+With this method Gerrit does not create merge commits on submitting a
+change. Merge commits may still be submitted, but they must be created
+on the client prior to uploading to Gerrit for review.
 +
 To submit a change, the change must be a strict superset of the
 destination branch.  That is, the change must already contain the
@@ -545,7 +520,7 @@
 branch, then the branch is fast-forwarded to the change.  If not,
 then the change is automatically rebased and then the branch is
 fast-forwarded to the change.
-
++
 When Gerrit tries to do a merge, by default the merge will only
 succeed if there is no path conflict.  A path conflict occurs when
 the same file has also been changed on the other side of the merge.
@@ -557,7 +532,7 @@
 if fast forward is possible AND like Cherry Pick it ensures footers such as
 Change-Id, Reviewed-On, and others are present in resulting commit that is
 merged.
-
++
 Thus, Rebase Always can be considered similar to Cherry Pick, but with
 the important distinction that Rebase Always does not ignore dependencies.
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 72b92a7..198e000 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2678,7 +2678,7 @@
     // Implement your submitability logic here
 
     // Assuming we want to prevent this change from being submitted:
-    SubmitRecord record;
+    SubmitRecord record = new SubmitRecord();
     record.status = Status.NOT_READY;
     return record;
   }
@@ -2810,6 +2810,12 @@
 end of a request (REST call, SSH call, git push). Implementors can write the
 execution times into a performance log for further analysis.
 
+[[request-listener]]
+== Request Listener
+
+`com.google.gerrit.server.RequestListener` is an extension point that is
+invoked each time the server executes a request from a user.
+
 [[plugins_hosting]]
 == Plugins source code hosting
 
diff --git a/Documentation/dev-roles.txt b/Documentation/dev-roles.txt
index b039489..89b1436 100644
--- a/Documentation/dev-roles.txt
+++ b/Documentation/dev-roles.txt
@@ -199,6 +199,8 @@
 * create events in the
   link:https://calendar.google.com/calendar?cid=Z29vZ2xlLmNvbV91YmIxcGxhNmlqNzg1b3FianI2MWg0dmRpc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t[
   community calendar]
+* discuss with other maintainers on the private maintainers mailing
+  list and Slack channel
 
 In addition, maintainers from Google can:
 
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index e5404c7..8fb5655 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -30,15 +30,13 @@
 
 ``` html
 <dom-module id="my-plugin">
-  <template>
-    <script>
-      Gerrit.install(plugin => {
-        'use strict';
+  <script>
+    Gerrit.install(plugin => {
+      'use strict';
 
-        // Your code here.
-      });
-    </script>
-  </template>
+      // Your code here.
+    });
+  </script>
 </dom-module>
 ```
 
diff --git a/Documentation/pg-plugin-styling.txt b/Documentation/pg-plugin-styling.txt
index c1e398e..2453bad 100644
--- a/Documentation/pg-plugin-styling.txt
+++ b/Documentation/pg-plugin-styling.txt
@@ -25,7 +25,7 @@
   <dom-module id="some-style">
     <template>
       <style>
-        :root {
+        html {
           --css-mixin-name: {
             property: value;
           }
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 3ea3ba1..96b376d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1799,9 +1799,6 @@
 Whether to enable the web UI for editing GPG keys.
 |`report_bug_url`    |optional|
 link:config-gerrit.html#gerrit.reportBugUrl[URL to report bugs].
-|`report_bug_text`   |optional, not set if default|
-link:config-gerrit.html#gerrit.reportBugText[Display text for report
-bugs link].
 |=================================
 
 [[hit-ration-info]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index f69c4ae..1544aae 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -824,11 +824,6 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
-    "require_change_id": {
-      "value": false,
-      "configured_value": "FALSE",
-      "inherited_value": true
-    },
     "max_object_size_limit": {
       "value": "15m",
       "configured_value": "15m",
@@ -887,7 +882,6 @@
     "enable_signed_push": "INHERIT",
     "require_signed_push": "INHERIT",
     "reject_implicit_merges": "INHERIT",
-    "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
     "state": "ACTIVE"
@@ -925,11 +919,6 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
-    "require_change_id": {
-      "value": true,
-      "configured_value": "TRUE",
-      "inherited_value": true
-    },
     "enable_signed_push": {
       "value": true,
       "configured_value": "INHERIT",
@@ -3094,12 +3083,6 @@
 |`create_new_change_for_all_not_in_target` |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 a new change is created for every commit not in target branch.
-|`require_change_id`                       |optional|
-link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
-valid link:user-changeid.html[Change-Id] footer in any commit uploaded
-for review is required. This does not apply to commits pushed directly
-to a branch or tag. This property is deprecated and will be removed in
-a future release.
 |`enable_signed_push`|optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is enabled on the project.
@@ -3180,14 +3163,6 @@
 branch. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
-|`require_change_id`                       |optional|
-Whether a valid link:user-changeid.html[Change-Id] footer in any commit
-uploaded for review is required. This does not apply to commits pushed
-directly to a branch or tag. +
-Can be `TRUE`, `FALSE` or `INHERIT`. +
-If not set, this setting is not updated.
-This property is deprecated and will be removed in
-a future release.
 |`reject_implicit_merges`                  |optional|
 Whether a check for implicit merges will be performed when changes
 are pushed for review. +
@@ -3553,11 +3528,6 @@
 Whether content merge should be enabled for the project (`TRUE`,
 `FALSE`, `INHERIT`). +
 `FALSE`, if the `submit_type` is `FAST_FORWARD_ONLY`.
-|`require_change_id`                           |`INHERIT` if not set|
-Whether the usage of Change-Ids is required for the project (`TRUE`,
-`FALSE`, `INHERIT`).
-This property is deprecated and will be removed in
-a future release.
 |`enable_signed_push`                           |`INHERIT` if not set|
 Whether signed push validation is enabled on the project  (`TRUE`,
 `FALSE`, `INHERIT`).
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index cc3ac42..bee723e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -53,7 +53,7 @@
 +
 Amount of time that has expired since the change was last updated
 with a review comment or new patch set.  The age must be specified
-to include a unit suffix, for example `age:2d`:
+to include a unit suffix, for example `-age:2d`:
 +
 * s, sec, second, seconds
 * m, min, minute, minutes
@@ -63,6 +63,10 @@
 * mon, month, months (`1 month` is treated as `30 days`)
 * y, year, years (`1 year` is treated as `365 days`)
 
+`age` can be used both forward and backward looking: `age:2d`
+means 'everything older than 2 days' while `-age:2d` means
+'everything with an age of at most 2 days'.
+
 [[assignee]]
 assignee:'USER'::
 +
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 56602e2..5bf49cd 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -470,69 +470,6 @@
 
 For more about Change-Ids, see link:user-changeid.html[Change-Id Lines].
 
-[[manual_replacement_mapping]]
-==== Manual Replacement Mapping
-
-[NOTE]
---
-The remainder of this section describes a manual method of replacing
-changes by matching each commit name to an existing change number.
-End-users should instead prefer to use Change-Id lines in their
-commit messages, as the process is then fully automated by Gerrit
-during normal uploads.
-
-See above for the preferred technique of replacing changes.
-
-Pushing directly to `refs/changes/` is deprecated. If you see the error
-message 'upload to refs/changes not allowed', it means that pushing directly
-to `refs/changes` is disabled on the Gerrit server and the below section does
-not apply to you.
---
-
-To add an additional patch set to a change, replacing it with an
-updated version of the same logical modification, send the new
-commit to the change's ref.  For example, to add the commit whose
-SHA-1 starts with `c0ffee` as a new patch set for change number
-`1979`, use the push refspec `c0ffee:refs/changes/1979` as below:
-
-----
-  git push ssh://sshusername@hostname:29418/projectname c0ffee:refs/changes/1979
-----
-
-This form can be combined together with `refs/for/'branchname'`
-(above) to simultaneously create new changes and replace changes
-during one network transaction.
-
-For example, consider the following sequence of events:
-
-----
-  $ git commit -m A                    ; # create 3 commits
-  $ git commit -m B
-  $ git commit -m C
-
-  $ git push ... HEAD:refs/for/master  ; # upload for review
-  ... A is 1500 ...
-  ... B is 1501 ...
-  ... C is 1502 ...
-
-  $ git rebase -i HEAD~3               ; # edit "A", insert D before B
-                                       ; # now series is A'-D-B'-C'
-  $ git push ...
-      HEAD:refs/for/master
-      HEAD~3:refs/changes/1500
-      HEAD~1:refs/changes/1501
-      HEAD~0:refs/changes/1502         ; # upload replacements
-----
-
-At the final step during the push Gerrit will attach A' as a new
-patch set on change 1500; B' as a new patch set on change 1501; C'
-as a new patch set on 1502; and D will be created as a new change.
-
-Ensuring D is created as a new change requires passing the refspec
-`HEAD:refs/for/branchname`, otherwise Gerrit will ignore D and
-won't do anything with it.  For this reason it is a good idea to
-always include the create change refspec when uploading replacements.
-
 
 [[bypass_review]]
 === Bypass Review
diff --git a/WORKSPACE b/WORKSPACE
index 53daa1c..ece7706 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -752,8 +752,8 @@
 # Keep this version of Soy synchronized with the version used in Gitiles.
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2019-04-18",
-    sha1 = "5750208855562d74f29eee39ee497d5cf6df1490",
+    artifact = "com.google.template:soy:2019-07-14",
+    sha1 = "547dee679bac6011126f3a54619d3aec336216d0",
 )
 
 maven_jar(
@@ -882,30 +882,30 @@
     sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
 )
 
-TRUTH_VERS = "0.46"
+TRUTH_VERS = "1.0"
 
 maven_jar(
     name = "truth",
     artifact = "com.google.truth:truth:" + TRUTH_VERS,
-    sha1 = "5907b14d1af802644e7f4fb7230419b709e06c6b",
+    sha1 = "998e5fb3fa31df716574b4c9e8d374855e800451",
 )
 
 maven_jar(
     name = "truth-java8-extension",
     artifact = "com.google.truth.extensions:truth-java8-extension:" + TRUTH_VERS,
-    sha1 = "e5ef57a6d1ab57695d373754df1efdddffc8484c",
+    sha1 = "d85fbc1daf0510821f552f2aa71d9605e97aa438",
 )
 
 maven_jar(
     name = "truth-liteproto-extension",
     artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
-    sha1 = "c624d921293426bac9e5c9780b01eaef914c1a22",
+    sha1 = "7a279c50a0f93da15533cef4993b45606cf67d72",
 )
 
 maven_jar(
     name = "truth-proto-extension",
     artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
-    sha1 = "585a0cedb3dac53ad3349826f163b28e59000d39",
+    sha1 = "8c0c2ea61750f02d0d5ce9c653106b6a5dc82d12",
 )
 
 maven_jar(
@@ -1062,8 +1062,8 @@
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.1.1",
-    sha1 = "ca04d8012f92cac561be343b931ec73302b2ff3e",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.2.0",
+    sha1 = "39cf34068b0af284eaa9b8bd86a131cb24b322d5",
 )
 
 maven_jar(
@@ -1072,18 +1072,18 @@
     sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
 )
 
-TESTCONTAINERS_VERSION = "1.11.3"
+TESTCONTAINERS_VERSION = "1.11.4"
 
 maven_jar(
     name = "testcontainers",
     artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-    sha1 = "154b69dd976416734b2fc809fb86e173ad9aa25b",
+    sha1 = "b0c70b1a3608f43deafba7649b344a422a442585",
 )
 
 maven_jar(
     name = "testcontainers-elasticsearch",
     artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-    sha1 = "90713b61f5748d8894c31a20f955bd7f81ac2ece",
+    sha1 = "faab09a8876b8dbb326cbc10bbaa5ea86f5f5299",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 197a6a3..8818ade 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -888,15 +888,6 @@
     }
   }
 
-  protected void setRequireChangeId(InheritableBoolean value) throws Exception {
-    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
-      ProjectConfig config = projectConfigFactory.read(md);
-      config.getProject().setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value);
-      config.commit(md);
-      projectCache.evict(config.getProject());
-    }
-  }
-
   protected PushOneCommit.Result pushTo(String ref) throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     return push.to(ref);
diff --git a/java/com/google/gerrit/acceptance/TestProjectInput.java b/java/com/google/gerrit/acceptance/TestProjectInput.java
index 0a3686b..7deb88a 100644
--- a/java/com/google/gerrit/acceptance/TestProjectInput.java
+++ b/java/com/google/gerrit/acceptance/TestProjectInput.java
@@ -43,8 +43,6 @@
 
   InheritableBoolean useContentMerge() default InheritableBoolean.INHERIT;
 
-  InheritableBoolean requireChangeId() default InheritableBoolean.INHERIT;
-
   InheritableBoolean rejectEmptyCommit() default InheritableBoolean.INHERIT;
 
   InheritableBoolean enableSignedPush() default InheritableBoolean.INHERIT;
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index b65f64b..8ac0de1 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -21,6 +21,7 @@
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 public class GroupReferenceSubject extends Subject {
 
@@ -39,7 +40,7 @@
     this.group = group;
   }
 
-  public ComparableSubject groupUuid() {
+  public ComparableSubject<AccountGroup.UUID> groupUuid() {
     isNotNull();
     return check("getUUID()").that(group.getUUID());
   }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 6be41c8..e608e93 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -26,7 +26,8 @@
   V6_6("6.6.*"),
   V6_7("6.7.*"),
   V7_0("7.0.*"),
-  V7_1("7.1.*");
+  V7_1("7.1.*"),
+  V7_2("7.2.*");
 
   private final String version;
   private final Pattern pattern;
diff --git a/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java b/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
index ef49651..14e0cdc 100644
--- a/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
+++ b/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
@@ -20,6 +20,10 @@
 public class NotifyInfo {
   public List<String> accounts;
 
+  /**
+   * @param accounts may be either just a list of: account IDs, Full names, usernames, or emails.
+   *     Also could be a list of those: "Full name <email@example.com>" or "Full name (<ID>)"
+   */
   public NotifyInfo(List<String> accounts) {
     this.accounts = accounts;
   }
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index fb2a0fe..eddfb09 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -29,7 +29,6 @@
   public InheritedBooleanInfo useContentMerge;
   public InheritedBooleanInfo useSignedOffBy;
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
-  public InheritedBooleanInfo requireChangeId;
   public InheritedBooleanInfo enableSignedPush;
   public InheritedBooleanInfo requireSignedPush;
   public InheritedBooleanInfo rejectImplicitMerges;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 1a6d77b..44a5258 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -25,7 +25,6 @@
   public InheritableBoolean useContentMerge;
   public InheritableBoolean useSignedOffBy;
   public InheritableBoolean createNewChangeForAllNotInTarget;
-  public InheritableBoolean requireChangeId;
   public InheritableBoolean enableSignedPush;
   public InheritableBoolean requireSignedPush;
   public InheritableBoolean rejectImplicitMerges;
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
index e61d316..2dec2b9 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -31,7 +31,6 @@
   public InheritableBoolean useContributorAgreements;
   public InheritableBoolean useSignedOffBy;
   public InheritableBoolean useContentMerge;
-  public InheritableBoolean requireChangeId;
   public InheritableBoolean createNewChangeForAllNotInTarget;
   public InheritableBoolean rejectEmptyCommit;
   public InheritableBoolean enableSignedPush;
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index 3bca4bb..d5fbf89 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -30,7 +30,9 @@
   public String path;
   public Side side;
   public Integer parent;
-  public Integer line; // value 0 or null indicates a file comment, normal lines start at 1
+  /** Value 0 or null indicates a file comment, normal lines start at 1. */
+  public Integer line;
+
   public Range range;
   public String inReplyTo;
   public Timestamp updated;
diff --git a/java/com/google/gerrit/extensions/common/AvatarInfo.java b/java/com/google/gerrit/extensions/common/AvatarInfo.java
index 00f1819..de609eb 100644
--- a/java/com/google/gerrit/extensions/common/AvatarInfo.java
+++ b/java/com/google/gerrit/extensions/common/AvatarInfo.java
@@ -21,7 +21,7 @@
    * <p>The web UI prefers avatar images to be square, both the height and width of the image should
    * be this size. The height is the more important dimension to match than the width.
    */
-  public static final int DEFAULT_SIZE = 26;
+  public static final int DEFAULT_SIZE = 32;
 
   public String url;
   public Integer height;
diff --git a/java/com/google/gerrit/extensions/common/GerritInfo.java b/java/com/google/gerrit/extensions/common/GerritInfo.java
index 4746273..5c462d9 100644
--- a/java/com/google/gerrit/extensions/common/GerritInfo.java
+++ b/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -21,6 +21,5 @@
   public String docUrl;
   public Boolean editGpgKeys;
   public String reportBugUrl;
-  public String reportBugText;
   public String primaryWeblinkName;
 }
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index c94dc27..8853a30 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -21,6 +21,7 @@
 import com.google.common.truth.ComparableSubject;
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.Subject;
+import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
 import com.google.gerrit.truth.ListSubject;
@@ -45,7 +46,7 @@
         .thatCustom(diffInfo.content, ContentEntrySubject.contentEntries());
   }
 
-  public ComparableSubject changeType() {
+  public ComparableSubject<ChangeType> changeType() {
     isNotNull();
     return check("changeType").that(diffInfo.changeType);
   }
diff --git a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
index 35e67a6..d011d5d 100644
--- a/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/FileInfoSubject.java
@@ -45,7 +45,7 @@
     return check("linesDeleted").that(fileInfo.linesDeleted);
   }
 
-  public ComparableSubject status() {
+  public ComparableSubject<Character> status() {
     isNotNull();
     return check("status").that(fileInfo.status);
   }
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index 5564642..d827d5d 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -23,6 +23,7 @@
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.common.GitPerson;
+import java.sql.Timestamp;
 import java.util.Date;
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -53,7 +54,7 @@
     return check("email").that(gitPerson.email);
   }
 
-  public ComparableSubject date() {
+  public ComparableSubject<Timestamp> date() {
     isNotNull();
     return check("date").that(gitPerson.date);
   }
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 5009211..f86b35d5 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
+        "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/json",
         "//java/com/google/gerrit/launcher",
         "//java/com/google/gerrit/lifecycle",
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 23afbd3..d66e8ac 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -26,6 +26,8 @@
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.audit.HttpAuditEvent;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
@@ -35,6 +37,7 @@
 import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupAuditService;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -339,6 +342,7 @@
     private final Provider<CurrentUser> userProvider;
     private final GroupAuditService groupAuditService;
     private final Metrics metrics;
+    private final PluginSetContext<RequestListener> requestListeners;
 
     @Inject
     UploadFilter(
@@ -346,12 +350,14 @@
         PermissionBackend permissionBackend,
         Provider<CurrentUser> userProvider,
         GroupAuditService groupAuditService,
-        Metrics metrics) {
+        Metrics metrics,
+        PluginSetContext<RequestListener> requestListeners) {
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
       this.userProvider = userProvider;
       this.groupAuditService = groupAuditService;
       this.metrics = metrics;
+      this.requestListeners = requestListeners;
     }
 
     @Override
@@ -369,7 +375,14 @@
       HttpServletRequest httpRequest = (HttpServletRequest) request;
       String sessionId = httpRequest.getSession().getId();
 
-      try {
+      try (TraceContext traceContext = TraceContext.open()) {
+        RequestInfo requestInfo =
+            RequestInfo.builder(
+                    RequestInfo.RequestType.GIT_UPLOAD, userProvider.get(), traceContext)
+                .project(state.getNameKey())
+                .build();
+        requestListeners.runEach(l -> l.onRequest(requestInfo));
+
         try {
           perm.check(ProjectPermission.RUN_UPLOAD_PACK);
         } catch (AuthException e) {
diff --git a/java/com/google/gerrit/httpd/RequestMetrics.java b/java/com/google/gerrit/httpd/RequestMetrics.java
index 2e8e6e7..e0f9b6a 100644
--- a/java/com/google/gerrit/httpd/RequestMetrics.java
+++ b/java/com/google/gerrit/httpd/RequestMetrics.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -29,7 +30,9 @@
   @Inject
   public RequestMetrics(MetricMaker metricMaker) {
     Field<Integer> statusCodeField =
-        Field.ofInteger("status").description("HTTP status code").build();
+        Field.ofInteger("status", Metadata.Builder::httpStatus)
+            .description("HTTP status code")
+            .build();
 
     errors =
         metricMaker.newCounter(
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index d7e9e44..4c125a7 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -123,6 +123,15 @@
     if (urlParameterMap.containsKey("p2")) {
       data.put("polymer2", "true");
     }
+    if (urlParameterMap.containsKey("ce")) {
+      data.put("polyfillCE", "true");
+    }
+    if (urlParameterMap.containsKey("sd")) {
+      data.put("polyfillSD", "true");
+    }
+    if (urlParameterMap.containsKey("sc")) {
+      data.put("polyfillSC", "true");
+    }
     return data.build();
   }
 
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index 4c9fc3b..a0b41b21 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -74,11 +74,7 @@
       ImmutableMap<String, Object> templateData =
           IndexHtmlUtil.templateData(
               gerritApi, canonicalUrl, cdnPath, faviconPath, parameterMap, urlOrdainer);
-      renderer =
-          soySauce
-              .renderTemplate("com.google.gerrit.httpd.raw.Index")
-              .setExpectedContentKind(SanitizedContent.ContentKind.HTML)
-              .setData(templateData);
+      renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
     } catch (URISyntaxException | RestApiException e) {
       throw new IOException(e);
     }
@@ -87,7 +83,7 @@
     rsp.setContentType("text/html");
     rsp.setStatus(SC_OK);
     try (OutputStream w = rsp.getOutputStream()) {
-      w.write(renderer.render().get().getBytes(UTF_8));
+      w.write(renderer.renderHtml().get().toString().getBytes(UTF_8));
     }
   }
 }
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
index 1c34f1d..fc099a6 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -42,7 +43,9 @@
   @Inject
   RestApiMetrics(MetricMaker metrics) {
     Field<String> viewField =
-        Field.ofString("view").description("view implementation class").build();
+        Field.ofString("view", Metadata.Builder::className)
+            .description("view implementation class")
+            .build();
     count =
         metrics.newCounter(
             "http/server/rest_api/count",
@@ -54,7 +57,9 @@
             "http/server/rest_api/error_count",
             new Description("REST API errors by view").setRate(),
             viewField,
-            Field.ofInteger("error_code").description("HTTP status code").build());
+            Field.ofInteger("error_code", Metadata.Builder::httpStatus)
+                .description("HTTP status code")
+                .build());
 
     serverLatency =
         metrics.newTimer(
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 592330f..2128777b 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -102,22 +102,30 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
 import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.logging.PerformanceLogContext;
 import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.notedb.ChangeNotes;
 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.plugincontext.PluginSetContext;
 import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.server.restapi.change.ChangesCollection;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.util.http.CacheHeaders;
@@ -227,6 +235,7 @@
     final Provider<CurrentUser> currentUser;
     final DynamicItem<WebSession> webSession;
     final Provider<ParameterParser> paramParser;
+    final PluginSetContext<RequestListener> requestListeners;
     final PermissionBackend permissionBackend;
     final GroupAuditService auditService;
     final RestApiMetrics metrics;
@@ -234,27 +243,32 @@
     final RestApiQuotaEnforcer quotaChecker;
     final Config config;
     final DynamicSet<PerformanceLogger> performanceLoggers;
+    final ChangeFinder changeFinder;
 
     @Inject
     Globals(
         Provider<CurrentUser> currentUser,
         DynamicItem<WebSession> webSession,
         Provider<ParameterParser> paramParser,
+        PluginSetContext<RequestListener> requestListeners,
         PermissionBackend permissionBackend,
         GroupAuditService auditService,
         RestApiMetrics metrics,
         RestApiQuotaEnforcer quotaChecker,
         @GerritServerConfig Config config,
-        DynamicSet<PerformanceLogger> performanceLoggers) {
+        DynamicSet<PerformanceLogger> performanceLoggers,
+        ChangeFinder changeFinder) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
+      this.requestListeners = requestListeners;
       this.permissionBackend = permissionBackend;
       this.auditService = auditService;
       this.metrics = metrics;
       this.quotaChecker = quotaChecker;
       this.config = config;
       this.performanceLoggers = performanceLoggers;
+      this.changeFinder = changeFinder;
       allowOrigin = makeAllowOrigin(config);
     }
 
@@ -301,6 +315,11 @@
     ViewData viewData = null;
 
     try (TraceContext traceContext = enableTracing(req, res)) {
+      List<IdString> path = splitPath(req);
+
+      RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
+      globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
+
       try (PerThreadCache ignored = PerThreadCache.create()) {
         // It's important that the PerformanceLogContext is closed before the response is sent to
         // the client. Only this way it is ensured that the invocation of the PerformanceLogger
@@ -329,7 +348,6 @@
           }
           checkUserSession(req);
 
-          List<IdString> path = splitPath(req);
           RestCollection<RestResource, RestResource> rc = members.get();
           globals
               .permissionBackend
@@ -627,9 +645,7 @@
         Throwable t = e.getCause();
         if (t instanceof LockFailureException) {
           logger.atSevere().withCause(t).log("Error in %s %s", req.getMethod(), uriForLogging(req));
-          responseBytes =
-              replyError(
-                  req, res, status = SC_SERVICE_UNAVAILABLE, messageOr(t, "Lock failure"), e);
+          responseBytes = replyError(req, res, status = SC_SERVICE_UNAVAILABLE, "Lock failure", e);
         } else {
           status = SC_INTERNAL_SERVER_ERROR;
           responseBytes = handleException(e, req, res);
@@ -1408,6 +1424,29 @@
     return traceContext;
   }
 
+  private RequestInfo createRequestInfo(
+      TraceContext traceContext, String requestUri, List<IdString> path) {
+    RequestInfo.Builder requestInfo =
+        RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext)
+            .requestUri(requestUri);
+
+    if (path.size() < 1) {
+      return requestInfo.build();
+    }
+
+    RestCollection<?, ?> rootCollection = members.get();
+    String resourceId = path.get(0).get();
+    if (rootCollection instanceof ProjectsCollection) {
+      requestInfo.project(Project.nameKey(resourceId));
+    } else if (rootCollection instanceof ChangesCollection) {
+      ChangeNotes changeNotes = globals.changeFinder.findOne(resourceId);
+      if (changeNotes != null) {
+        requestInfo.project(changeNotes.getProjectName());
+      }
+    }
+    return requestInfo.build();
+  }
+
   private boolean isDelete(HttpServletRequest req) {
     return "DELETE".equals(req.getMethod());
   }
@@ -1492,7 +1531,7 @@
     configureCaching(req, res, null, null, c);
     checkArgument(statusCode >= 400, "non-error status: %s", statusCode);
     res.setStatus(statusCode);
-    logger.atFinest().log("REST call failed: %d", statusCode);
+    logger.atFinest().withCause(err).log("REST call failed: %d", statusCode);
     return replyText(req, res, true, msg);
   }
 
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 346a306..61d609b 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.logging.CallerFinder;
+import com.google.gerrit.server.logging.Metadata;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -66,7 +67,9 @@
               new Description("Successful query latency, accumulated over the life of the process")
                   .setCumulative()
                   .setUnit(Description.Units.MILLISECONDS),
-              Field.ofString("index").description("index name").build());
+              Field.ofString("index", Metadata.Builder::indexName)
+                  .description("index name")
+                  .build());
     }
   }
 
diff --git a/java/com/google/gerrit/metrics/Field.java b/java/com/google/gerrit/metrics/Field.java
index ad8eae9..bdae854 100644
--- a/java/com/google/gerrit/metrics/Field.java
+++ b/java/com/google/gerrit/metrics/Field.java
@@ -17,7 +17,9 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.server.logging.Metadata;
 import java.util.Optional;
+import java.util.function.BiConsumer;
 import java.util.function.Function;
 
 /**
@@ -27,17 +29,23 @@
  */
 @AutoValue
 public abstract class Field<T> {
+  public static <T> BiConsumer<Metadata.Builder, T> ignoreMetadata() {
+    return (metadataBuilder, fieldValue) -> {};
+  }
+
   /**
    * Break down metrics by boolean true/false.
    *
    * @param name field name
    * @return builder for the boolean field
    */
-  public static Field.Builder<Boolean> ofBoolean(String name) {
+  public static Field.Builder<Boolean> ofBoolean(
+      String name, BiConsumer<Metadata.Builder, Boolean> metadataMapper) {
     return new AutoValue_Field.Builder<Boolean>()
         .valueType(Boolean.class)
         .formatter(Object::toString)
-        .name(name);
+        .name(name)
+        .metadataMapper(metadataMapper);
   }
 
   /**
@@ -47,8 +55,15 @@
    * @param name field name
    * @return builder for the enum field
    */
-  public static <E extends Enum<E>> Field.Builder<E> ofEnum(Class<E> enumType, String name) {
-    return new AutoValue_Field.Builder<E>().valueType(enumType).formatter(Enum::name).name(name);
+  public static <E extends Enum<E>> Field.Builder<E> ofEnum(
+      Class<E> enumType, String name, BiConsumer<Metadata.Builder, String> metadataMapper) {
+    return new AutoValue_Field.Builder<E>()
+        .valueType(enumType)
+        .formatter(Enum::name)
+        .name(name)
+        .metadataMapper(
+            (metadataBuilder, fieldValue) ->
+                metadataMapper.accept(metadataBuilder, fieldValue.name()));
   }
 
   /**
@@ -60,11 +75,13 @@
    * @param name field name
    * @return builder for the integer field
    */
-  public static Field.Builder<Integer> ofInteger(String name) {
+  public static Field.Builder<Integer> ofInteger(
+      String name, BiConsumer<Metadata.Builder, Integer> metadataMapper) {
     return new AutoValue_Field.Builder<Integer>()
         .valueType(Integer.class)
         .formatter(Object::toString)
-        .name(name);
+        .name(name)
+        .metadataMapper(metadataMapper);
   }
 
   /**
@@ -76,11 +93,13 @@
    * @param name field name
    * @return builder for the string field
    */
-  public static Field.Builder<String> ofString(String name) {
+  public static Field.Builder<String> ofString(
+      String name, BiConsumer<Metadata.Builder, String> metadataMapper) {
     return new AutoValue_Field.Builder<String>()
         .valueType(String.class)
         .formatter(s -> s)
-        .name(name);
+        .name(name)
+        .metadataMapper(metadataMapper);
   }
 
   /** @return name of this field within the metric. */
@@ -89,6 +108,9 @@
   /** @return type of value used within the field. */
   public abstract Class<T> valueType();
 
+  /** @return mapper that maps a field value to a field in the {@link Metadata} class. */
+  public abstract BiConsumer<Metadata.Builder, T> metadataMapper();
+
   /** @return description text for the field explaining its range of values. */
   public abstract Optional<String> description();
 
@@ -103,6 +125,8 @@
 
     abstract Builder<T> formatter(Function<T, String> formatter);
 
+    abstract Builder<T> metadataMapper(BiConsumer<Metadata.Builder, T> metadataMapper);
+
     public abstract Builder<T> description(String description);
 
     abstract Field<T> autoBuild();
diff --git a/java/com/google/gerrit/metrics/Timer1.java b/java/com/google/gerrit/metrics/Timer1.java
index c420e9a..a8fb1a2 100644
--- a/java/com/google/gerrit/metrics/Timer1.java
+++ b/java/com/google/gerrit/metrics/Timer1.java
@@ -19,6 +19,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
@@ -80,9 +81,12 @@
   public final void record(F1 fieldValue, long value, TimeUnit unit) {
     long durationMs = unit.toMillis(value);
 
+    Metadata.Builder metadataBuilder = Metadata.builder();
+    field.metadataMapper().accept(metadataBuilder, fieldValue);
+    Metadata metadata = metadataBuilder.build();
+
     LoggingContext.getInstance()
-        .addPerformanceLogRecord(
-            () -> PerformanceLogRecord.create(name, durationMs, field.name(), fieldValue));
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs, metadata));
 
     logger.atFinest().log("%s (%s = %s) took %dms", name, field.name(), fieldValue, durationMs);
     doRecord(fieldValue, value, unit);
diff --git a/java/com/google/gerrit/metrics/Timer2.java b/java/com/google/gerrit/metrics/Timer2.java
index fef2d9a..8a4a793 100644
--- a/java/com/google/gerrit/metrics/Timer2.java
+++ b/java/com/google/gerrit/metrics/Timer2.java
@@ -19,6 +19,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
@@ -87,11 +88,13 @@
   public final void record(F1 fieldValue1, F2 fieldValue2, long value, TimeUnit unit) {
     long durationMs = unit.toMillis(value);
 
+    Metadata.Builder metadataBuilder = Metadata.builder();
+    field1.metadataMapper().accept(metadataBuilder, fieldValue1);
+    field2.metadataMapper().accept(metadataBuilder, fieldValue2);
+    Metadata metadata = metadataBuilder.build();
+
     LoggingContext.getInstance()
-        .addPerformanceLogRecord(
-            () ->
-                PerformanceLogRecord.create(
-                    name, durationMs, field1.name(), fieldValue1, field2.name(), fieldValue2));
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs, metadata));
 
     logger.atFinest().log(
         "%s (%s = %s, %s = %s) took %dms",
diff --git a/java/com/google/gerrit/metrics/Timer3.java b/java/com/google/gerrit/metrics/Timer3.java
index 40c4882..2044da6 100644
--- a/java/com/google/gerrit/metrics/Timer3.java
+++ b/java/com/google/gerrit/metrics/Timer3.java
@@ -19,6 +19,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
 
@@ -95,18 +96,14 @@
       F1 fieldValue1, F2 fieldValue2, F3 fieldValue3, long value, TimeUnit unit) {
     long durationMs = unit.toMillis(value);
 
+    Metadata.Builder metadataBuilder = Metadata.builder();
+    field1.metadataMapper().accept(metadataBuilder, fieldValue1);
+    field2.metadataMapper().accept(metadataBuilder, fieldValue2);
+    field3.metadataMapper().accept(metadataBuilder, fieldValue3);
+    Metadata metadata = metadataBuilder.build();
+
     LoggingContext.getInstance()
-        .addPerformanceLogRecord(
-            () ->
-                PerformanceLogRecord.create(
-                    name,
-                    durationMs,
-                    field1.name(),
-                    fieldValue1,
-                    field2.name(),
-                    fieldValue2,
-                    field3.name(),
-                    fieldValue3));
+        .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs, metadata));
 
     logger.atFinest().log(
         "%s (%s = %s, %s = %s, %s = %s) took %dms",
diff --git a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
index 55a9ec3..d9781b5 100644
--- a/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import java.lang.management.GarbageCollectorMXBean;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryMXBean;
@@ -149,7 +150,9 @@
 
   private void procJvmGc(MetricMaker metrics) {
     Field<String> gcNameField =
-        Field.ofString("gc_name").description("The name of the garbage collector").build();
+        Field.ofString("gc_name", Metadata.Builder::garbageCollectorName)
+            .description("The name of the garbage collector")
+            .build();
 
     CallbackMetric1<String, Long> gcCount =
         metrics.newCallbackMetric(
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 7575fdb..a7a8c58 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.pgm.init.index.elasticsearch.ElasticIndexModuleOnInit;
 import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
@@ -52,7 +51,6 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.TypeLiteral;
 import com.google.inject.spi.Message;
@@ -69,7 +67,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import org.eclipse.jgit.lib.Config;
 
 /** Initialize a new Gerrit installation. */
 public class BaseInit extends SiteProgram {
@@ -80,7 +77,6 @@
   private final List<String> pluginsToInstall;
 
   private Injector sysInjector;
-  private Config config;
 
   protected BaseInit(PluginsDistribution pluginsDistribution, List<String> pluginsToInstall) {
     this.standalone = true;
@@ -125,9 +121,6 @@
         try {
           run.upgradeSchema();
         } catch (StorageException e) {
-          if (config.getBoolean("container", "slave", false)) {
-            throw e;
-          }
           String msg = "Couldn't upgrade schema. Expected if slave and read-only database";
           System.err.println(msg);
           logger.atWarning().withCause(e).log(msg);
@@ -419,7 +412,6 @@
             }
           });
       Injector dbInjector = createDbInjector();
-      config = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
 
       IndexType indexType = IndexModule.getIndexType(dbInjector);
       if (indexType.isLucene()) {
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 674f9c1..45c5ea6 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
-import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -29,7 +28,6 @@
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -49,7 +47,6 @@
 public class InitAdminUser implements InitStep {
   private final InitFlags flags;
   private final ConsoleUI ui;
-  private final AllUsersNameOnInitProvider allUsers;
   private final AccountsOnInit accounts;
   private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
   private final ExternalIdsOnInit externalIds;
@@ -62,7 +59,6 @@
   InitAdminUser(
       InitFlags flags,
       ConsoleUI ui,
-      AllUsersNameOnInitProvider allUsers,
       AccountsOnInit accounts,
       VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
       ExternalIdsOnInit externalIds,
@@ -70,7 +66,6 @@
       GroupsOnInit groupsOnInit) {
     this.flags = flags;
     this.ui = ui;
-    this.allUsers = allUsers;
     this.accounts = accounts;
     this.authorizedKeysFactory = authorizedKeysFactory;
     this.externalIds = externalIds;
@@ -140,7 +135,7 @@
           authorizedKeys.save("Add SSH key for initial admin user\n");
         }
 
-        AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
+        AccountState as = AccountState.forAccount(a, extIds);
         for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
           accountIndex.replace(as);
         }
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
index a70d254..bea5abe 100644
--- a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
@@ -32,7 +32,6 @@
   USE_CONTRIBUTOR_AGREEMENTS("receive", "requireContributorAgreement"),
   USE_SIGNED_OFF_BY("receive", "requireSignedOffBy"),
   USE_CONTENT_MERGE("submit", "mergeContent"),
-  REQUIRE_CHANGE_ID("receive", "requireChangeId"),
   CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET("receive", "createNewChangeForAllNotInTarget"),
   ENABLE_SIGNED_PUSH("receive", "enableSignedPush"),
   REQUIRE_SIGNED_PUSH("receive", "requireSignedPush"),
diff --git a/java/com/google/gerrit/reviewdb/client/Comment.java b/java/com/google/gerrit/reviewdb/client/Comment.java
index d1f97fb..94e7583 100644
--- a/java/com/google/gerrit/reviewdb/client/Comment.java
+++ b/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -185,7 +185,9 @@
   }
 
   public Key key;
+  /** The line number (1-based) to which the comment refers, or 0 for a file comment. */
   public int lineNbr;
+
   public Identity author;
   protected Identity realAuthor;
   public Timestamp writtenOn;
diff --git a/java/com/google/gerrit/server/ApprovalCopier.java b/java/com/google/gerrit/server/ApprovalCopier.java
index 979cc11..85a6079 100644
--- a/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/java/com/google/gerrit/server/ApprovalCopier.java
@@ -73,49 +73,27 @@
 
   Iterable<PatchSetApproval> getForPatchSet(
       ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
-    return getForPatchSet(notes, psId, rw, repoConfig, Collections.emptyList());
-  }
 
-  Iterable<PatchSetApproval> getForPatchSet(
-      ChangeNotes notes,
-      PatchSet.Id psId,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy) {
     PatchSet ps = psUtil.get(notes, psId);
     if (ps == null) {
       return Collections.emptyList();
     }
-    return getForPatchSet(notes, ps, rw, repoConfig, dontCopy);
-  }
 
-  private Iterable<PatchSetApproval> getForPatchSet(
-      ChangeNotes notes,
-      PatchSet ps,
-      @Nullable RevWalk rw,
-      @Nullable Config repoConfig,
-      Iterable<PatchSetApproval> dontCopy) {
-    requireNonNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(notes);
     try {
       ProjectState project = projectCache.checkedGet(cd.change().getDest().project());
       ListMultimap<PatchSet.Id, PatchSetApproval> all = cd.approvals();
       requireNonNull(all, "all should not be null");
 
-      Table<String, Account.Id, PatchSetApproval> wontCopy = HashBasedTable.create();
-      for (PatchSetApproval psa : dontCopy) {
-        wontCopy.put(psa.label(), psa.accountId(), psa);
-      }
-
       Table<String, Account.Id, PatchSetApproval> byUser = HashBasedTable.create();
       for (PatchSetApproval psa : all.get(ps.id())) {
-        if (!wontCopy.contains(psa.label(), psa.accountId())) {
-          byUser.put(psa.label(), psa.accountId(), psa);
-        }
+        byUser.put(psa.label(), psa.accountId(), psa);
       }
 
       TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
 
+      Table<String, Account.Id, PatchSetApproval> wontCopy = HashBasedTable.create();
+
       // Walk patch sets strictly less than current in descending order.
       Collection<PatchSet> allPrior =
           patchSets.descendingMap().tailMap(ps.id().get(), false).values();
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
new file mode 100644
index 0000000..f5749fc
--- /dev/null
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.logging.TraceContext;
+import java.util.Optional;
+
+/** Information about a request that was received from a user. */
+@AutoValue
+public abstract class RequestInfo {
+  /** Channel through which a user request was received. */
+  public enum RequestType {
+    /** request type for git push */
+    GIT_RECEIVE,
+
+    /** request type for git fetch */
+    GIT_UPLOAD,
+
+    /** request type for call to REST API */
+    REST,
+
+    /** request type for call to SSH API */
+    SSH
+  }
+
+  /**
+   * Type of the request, telling through which channel the request was coming in.
+   *
+   * <p>See {@link RequestType} for the types that are used by Gerrit core. Other request types are
+   * possible, e.g. if a plugin supports receiving requests through another channel.
+   */
+  public abstract String requestType();
+
+  /**
+   * Request URI.
+   *
+   * <p>Only set if request type is {@link RequestType#REST}.
+   *
+   * <p>Never includes the "/a" prefix.
+   */
+  public abstract Optional<String> requestUri();
+
+  /** The user that has sent the request. */
+  public abstract CurrentUser callingUser();
+
+  /** The trace context of the request. */
+  public abstract TraceContext traceContext();
+
+  /**
+   * The name of the project for which the request is being done. Only available if the request is
+   * tied to a project or change. If a project is available it's not guaranteed that it actually
+   * exists (e.g. if a user made a request for a project that doesn't exist).
+   */
+  public abstract Optional<Project.NameKey> project();
+
+  public static RequestInfo.Builder builder(
+      RequestType requestType, CurrentUser callingUser, TraceContext traceContext) {
+    return new AutoValue_RequestInfo.Builder()
+        .requestType(requestType)
+        .callingUser(callingUser)
+        .traceContext(traceContext);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder requestType(String requestType);
+
+    public Builder requestType(RequestType requestType) {
+      return requestType(requestType.name());
+    }
+
+    public abstract Builder requestUri(String requestUri);
+
+    public abstract Builder callingUser(CurrentUser callingUser);
+
+    public abstract Builder traceContext(TraceContext traceContext);
+
+    public abstract Builder project(Project.NameKey projectName);
+
+    public abstract RequestInfo build();
+  }
+}
diff --git a/java/com/google/gerrit/server/RequestListener.java b/java/com/google/gerrit/server/RequestListener.java
new file mode 100644
index 0000000..461b91a
--- /dev/null
+++ b/java/com/google/gerrit/server/RequestListener.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface RequestListener {
+  void onRequest(RequestInfo requestInfo);
+}
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index b81e0bd..2cb670e 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -376,7 +377,9 @@
   }
 
   public static StarRef readLabels(Repository repo, String refName) throws IOException {
-    try (TraceTimer traceTimer = TraceContext.newTimer("Read star labels", "ref", refName)) {
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
       Ref ref = repo.exactRef(refName);
       if (ref == null) {
         return StarRef.MISSING;
@@ -451,7 +454,8 @@
       throws IOException, InvalidLabelsException {
     try (TraceTimer traceTimer =
             TraceContext.newTimer(
-                "Update star labels", "ref", refName, "labelCount", labels.size());
+                "Update star labels",
+                Metadata.builder().noteDbRefName(refName).resourceCount(labels.size()).build());
         RevWalk rw = new RevWalk(repo)) {
       RefUpdate u = repo.updateRef(refName);
       u.setExpectedOldObjectId(oldObjectId);
@@ -488,7 +492,9 @@
       return;
     }
 
-    try (TraceTimer traceTimer = TraceContext.newTimer("Delete star labels", "ref", refName)) {
+    try (TraceTimer traceTimer =
+        TraceContext.newTimer(
+            "Delete star labels", Metadata.builder().noteDbRefName(refName).build())) {
       RefUpdate u = repo.updateRef(refName);
       u.setForceUpdate(true);
       u.setExpectedOldObjectId(oldObjectId);
diff --git a/java/com/google/gerrit/server/TraceRequestListener.java b/java/com/google/gerrit/server/TraceRequestListener.java
new file mode 100644
index 0000000..773f712
--- /dev/null
+++ b/java/com/google/gerrit/server/TraceRequestListener.java
@@ -0,0 +1,228 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Request listener that sets additional logging tags and enables tracing automatically if the
+ * request matches any tracing configuration in gerrit.config (see description of
+ * 'tracing.<trace-id>' subsection in config-gerrit.txt).
+ */
+@Singleton
+public class TraceRequestListener implements RequestListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final Config cfg;
+  private final ImmutableList<TraceConfig> traceConfigs;
+
+  @Inject
+  TraceRequestListener(@GerritServerConfig Config cfg) {
+    this.cfg = cfg;
+    this.traceConfigs = parseTraceConfigs();
+  }
+
+  @Override
+  public void onRequest(RequestInfo requestInfo) {
+    requestInfo.project().ifPresent(p -> requestInfo.traceContext().addTag("project", p));
+    traceConfigs.stream()
+        .filter(traceConfig -> traceConfig.matches(requestInfo))
+        .forEach(
+            traceConfig ->
+                requestInfo
+                    .traceContext()
+                    .forceLogging()
+                    .addTag(RequestId.Type.TRACE_ID, traceConfig.traceId()));
+  }
+
+  private ImmutableList<TraceConfig> parseTraceConfigs() {
+    ImmutableList.Builder<TraceConfig> traceConfigs = ImmutableList.builder();
+
+    for (String traceId : cfg.getSubsections("tracing")) {
+      try {
+        TraceConfig.Builder traceConfig = TraceConfig.builder();
+        traceConfig.traceId(traceId);
+        traceConfig.requestTypes(parseRequestTypes(traceId));
+        traceConfig.requestUriPatterns(parseRequestUriPatterns(traceId));
+        traceConfig.accountIds(parseAccounts(traceId));
+        traceConfig.projectPatterns(parseProjectPatterns(traceId));
+        traceConfigs.add(traceConfig.build());
+      } catch (ConfigInvalidException e) {
+        logger.atWarning().log("Ignoring invalid tracing configuration:\n %s", e.getMessage());
+      }
+    }
+
+    return traceConfigs.build();
+  }
+
+  private ImmutableSet<String> parseRequestTypes(String traceId) {
+    return ImmutableSet.copyOf(cfg.getStringList("tracing", traceId, "requestType"));
+  }
+
+  private ImmutableSet<Pattern> parseRequestUriPatterns(String traceId)
+      throws ConfigInvalidException {
+    return parsePatterns(traceId, "requestUriPattern");
+  }
+
+  private ImmutableSet<Account.Id> parseAccounts(String traceId) throws ConfigInvalidException {
+    ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
+    String[] accounts = cfg.getStringList("tracing", traceId, "account");
+    for (String account : accounts) {
+      Optional<Account.Id> accountId = Account.Id.tryParse(account);
+      if (!accountId.isPresent()) {
+        throw new ConfigInvalidException(
+            String.format(
+                "Invalid tracing config ('tracing.%s.account = %s'): invalid account ID",
+                traceId, account));
+      }
+      accountIds.add(accountId.get());
+    }
+    return accountIds.build();
+  }
+
+  private ImmutableSet<Pattern> parseProjectPatterns(String traceId) throws ConfigInvalidException {
+    return parsePatterns(traceId, "projectPattern");
+  }
+
+  private ImmutableSet<Pattern> parsePatterns(String traceId, String name)
+      throws ConfigInvalidException {
+    ImmutableSet.Builder<Pattern> patterns = ImmutableSet.builder();
+    String[] patternRegExs = cfg.getStringList("tracing", traceId, name);
+    for (String patternRegEx : patternRegExs) {
+      try {
+        patterns.add(Pattern.compile(patternRegEx));
+      } catch (PatternSyntaxException e) {
+        throw new ConfigInvalidException(
+            String.format(
+                "Invalid tracing config ('tracing.%s.%s = %s'): %s",
+                traceId, name, patternRegEx, e.getMessage()));
+      }
+    }
+    return patterns.build();
+  }
+
+  @AutoValue
+  abstract static class TraceConfig {
+    /** ID for the trace */
+    abstract String traceId();
+
+    /** request types that should be traced */
+    abstract ImmutableSet<String> requestTypes();
+
+    /** pattern matching request URIs */
+    abstract ImmutableSet<Pattern> requestUriPatterns();
+
+    /** accounts IDs matching calling user */
+    abstract ImmutableSet<Account.Id> accountIds();
+
+    /** pattern matching projects names */
+    abstract ImmutableSet<Pattern> projectPatterns();
+
+    static Builder builder() {
+      return new AutoValue_TraceRequestListener_TraceConfig.Builder();
+    }
+
+    /**
+     * Whether this trace config matches a given request.
+     *
+     * @param requestInfo request info
+     * @return whether this trace config matches
+     */
+    boolean matches(RequestInfo requestInfo) {
+      // If in the trace config request types are set and none of them matches, then the request is
+      // not matched.
+      if (!requestTypes().isEmpty()
+          && requestTypes().stream()
+              .noneMatch(type -> type.equalsIgnoreCase(requestInfo.requestType()))) {
+        return false;
+      }
+
+      // If in the trace config request URI patterns are set and none of them matches, then the
+      // request is not matched.
+      if (!requestUriPatterns().isEmpty()) {
+        if (!requestInfo.requestUri().isPresent()) {
+          // The request has no request URI, hence it cannot match a request URI pattern.
+          return false;
+        }
+
+        if (requestUriPatterns().stream()
+            .noneMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
+          return false;
+        }
+      }
+
+      // If in the trace config accounts are set and none of them matches, then the request is not
+      // matched.
+      if (!accountIds().isEmpty()) {
+        try {
+          if (accountIds().stream()
+              .noneMatch(id -> id.equals(requestInfo.callingUser().getAccountId()))) {
+            return false;
+          }
+        } catch (UnsupportedOperationException e) {
+          // The calling user is not logged in, hence it cannot match an account.
+          return false;
+        }
+      }
+
+      // If in the trace config project patterns are set and none of them matches, then the request
+      // is not matched.
+      if (!projectPatterns().isEmpty()) {
+        if (!requestInfo.project().isPresent()) {
+          // The request is not for a project, hence it cannot match a project pattern.
+          return false;
+        }
+
+        if (projectPatterns().stream()
+            .noneMatch(p -> p.matcher(requestInfo.project().get().get()).matches())) {
+          return false;
+        }
+      }
+
+      // For any match criteria (request type, request URI pattern, account, project pattern) that
+      // was specified in the trace config, at least one of the configured value matched the
+      // request.
+      return true;
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder traceId(String traceId);
+
+      abstract Builder requestTypes(ImmutableSet<String> requestTypes);
+
+      abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
+
+      abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
+
+      abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
+
+      abstract TraceConfig build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 3f3f27d..3d81052 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -26,7 +26,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -68,18 +68,15 @@
     };
   }
 
-  private final AllUsersName allUsersName;
   private final ExternalIds externalIds;
   private final LoadingCache<Account.Id, Optional<AccountState>> byId;
   private final ExecutorService executor;
 
   @Inject
   AccountCacheImpl(
-      AllUsersName allUsersName,
       ExternalIds externalIds,
       @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId,
       @FanOutExecutor ExecutorService executor) {
-    this.allUsersName = allUsersName;
     this.externalIds = externalIds;
     this.byId = byId;
     this.executor = executor;
@@ -170,7 +167,7 @@
   private AccountState missing(Account.Id accountId) {
     Account account = new Account(accountId, TimeUtil.nowTs());
     account.setActive(false);
-    return AccountState.forAccount(allUsersName, account);
+    return AccountState.forAccount(account);
   }
 
   static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> {
@@ -183,7 +180,9 @@
 
     @Override
     public Optional<AccountState> load(Account.Id who) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading account", "accountId", who)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading account", Metadata.builder().accountId(who.get()).build())) {
         return accounts.get(who);
       }
     }
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 46fde8c..556185e 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -36,7 +36,6 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.config.AllUsersName;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Optional;
@@ -56,16 +55,14 @@
   /**
    * Creates an AccountState from the given account config.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param externalIds class to access external IDs
    * @param accountConfig the account config, must already be loaded
    * @return the account state, {@link Optional#empty()} if the account doesn't exist
    * @throws IOException if accessing the external IDs fails
    */
   public static Optional<AccountState> fromAccountConfig(
-      AllUsersName allUsersName, ExternalIds externalIds, AccountConfig accountConfig)
-      throws IOException {
-    return fromAccountConfig(allUsersName, externalIds, accountConfig, null);
+      ExternalIds externalIds, AccountConfig accountConfig) throws IOException {
+    return fromAccountConfig(externalIds, accountConfig, null);
   }
 
   /**
@@ -78,7 +75,6 @@
    * updated the revision of the external IDs branch in account config is outdated. Hence after
    * updating external IDs the external ID notes must be provided.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param externalIds class to access external IDs
    * @param accountConfig the account config, must already be loaded
    * @param extIdNotes external ID notes, must already be loaded, may be {@code null}
@@ -86,10 +82,7 @@
    * @throws IOException if accessing the external IDs fails
    */
   public static Optional<AccountState> fromAccountConfig(
-      AllUsersName allUsersName,
-      ExternalIds externalIds,
-      AccountConfig accountConfig,
-      @Nullable ExternalIdNotes extIdNotes)
+      ExternalIds externalIds, AccountConfig accountConfig, @Nullable ExternalIdNotes extIdNotes)
       throws IOException {
     if (!accountConfig.getLoadedAccount().isPresent()) {
       return Optional.empty();
@@ -115,39 +108,29 @@
 
     return Optional.of(
         new AccountState(
-            allUsersName,
-            account,
-            extIds,
-            projectWatches,
-            generalPreferences,
-            diffPreferences,
-            editPreferences));
+            account, extIds, projectWatches, generalPreferences, diffPreferences, editPreferences));
   }
 
   /**
    * Creates an AccountState for a given account with no external IDs, no project watches and
    * default preferences.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param account the account
    * @return the account state
    */
-  public static AccountState forAccount(AllUsersName allUsersName, Account account) {
-    return forAccount(allUsersName, account, ImmutableSet.of());
+  public static AccountState forAccount(Account account) {
+    return forAccount(account, ImmutableSet.of());
   }
 
   /**
    * Creates an AccountState for a given account with no project watches and default preferences.
    *
-   * @param allUsersName the name of the All-Users repository
    * @param account the account
    * @param extIds the external IDs
    * @return the account state
    */
-  public static AccountState forAccount(
-      AllUsersName allUsersName, Account account, Collection<ExternalId> extIds) {
+  public static AccountState forAccount(Account account, Collection<ExternalId> extIds) {
     return new AccountState(
-        allUsersName,
         account,
         ImmutableSet.copyOf(extIds),
         ImmutableMap.of(),
@@ -156,7 +139,6 @@
         EditPreferencesInfo.defaults());
   }
 
-  private final AllUsersName allUsersName;
   private final Account account;
   private final ImmutableSet<ExternalId> externalIds;
   private final Optional<String> userName;
@@ -167,14 +149,12 @@
   private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
 
   private AccountState(
-      AllUsersName allUsersName,
       Account account,
       ImmutableSet<ExternalId> externalIds,
       ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches,
       GeneralPreferencesInfo generalPreferences,
       DiffPreferencesInfo diffPreferences,
       EditPreferencesInfo editPreferences) {
-    this.allUsersName = allUsersName;
     this.account = account;
     this.externalIds = externalIds;
     this.userName = ExternalId.getUserName(externalIds);
@@ -184,10 +164,6 @@
     this.editPreferences = editPreferences;
   }
 
-  public AllUsersName getAllUsersNameForIndexing() {
-    return allUsersName;
-  }
-
   /** Get the cached account metadata. */
   public Account getAccount() {
     return account;
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index f3758bf..dbe7ba7 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -134,9 +134,7 @@
   private Optional<AccountState> read(Repository allUsersRepository, Account.Id accountId)
       throws IOException, ConfigInvalidException {
     return AccountState.fromAccountConfig(
-        allUsersName,
-        externalIds,
-        new AccountConfig(accountId, allUsersName, allUsersRepository).load());
+        externalIds, new AccountConfig(accountId, allUsersName, allUsersRepository).load());
   }
 
   public static Stream<Account.Id> readUserRefs(Repository repo) throws IOException {
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 20a1c97..2920cef 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -321,7 +321,7 @@
               AccountConfig accountConfig = read(r, accountId);
               Account account =
                   accountConfig.getNewAccount(new Timestamp(committerIdent.getWhen().getTime()));
-              AccountState accountState = AccountState.forAccount(allUsersName, account);
+              AccountState accountState = AccountState.forAccount(account);
               InternalAccountUpdate.Builder updateBuilder = InternalAccountUpdate.builder();
               updater.update(accountState, updateBuilder);
 
@@ -330,7 +330,7 @@
               ExternalIdNotes extIdNotes =
                   createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update);
               UpdatedAccount updatedAccounts =
-                  new UpdatedAccount(allUsersName, externalIds, message, accountConfig, extIdNotes);
+                  new UpdatedAccount(externalIds, message, accountConfig, extIdNotes);
               updatedAccounts.setCreated(true);
               return updatedAccounts;
             })
@@ -377,7 +377,7 @@
         r -> {
           AccountConfig accountConfig = read(r, accountId);
           Optional<AccountState> account =
-              AccountState.fromAccountConfig(allUsersName, externalIds, accountConfig);
+              AccountState.fromAccountConfig(externalIds, accountConfig);
           if (!account.isPresent()) {
             return null;
           }
@@ -390,7 +390,7 @@
           ExternalIdNotes extIdNotes =
               createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update);
           UpdatedAccount updatedAccounts =
-              new UpdatedAccount(allUsersName, externalIds, message, accountConfig, extIdNotes);
+              new UpdatedAccount(externalIds, message, accountConfig, extIdNotes);
           return updatedAccounts;
         });
   }
@@ -561,7 +561,6 @@
   }
 
   private static class UpdatedAccount {
-    private final AllUsersName allUsersName;
     private final ExternalIds externalIds;
     private final String message;
     private final AccountConfig accountConfig;
@@ -570,13 +569,11 @@
     private boolean created;
 
     private UpdatedAccount(
-        AllUsersName allUsersName,
         ExternalIds externalIds,
         String message,
         AccountConfig accountConfig,
         ExternalIdNotes extIdNotes) {
       checkState(!Strings.isNullOrEmpty(message), "message for account update must be set");
-      this.allUsersName = requireNonNull(allUsersName);
       this.externalIds = requireNonNull(externalIds);
       this.message = requireNonNull(message);
       this.accountConfig = requireNonNull(accountConfig);
@@ -592,8 +589,7 @@
     }
 
     public AccountState getAccount() throws IOException {
-      return AccountState.fromAccountConfig(allUsersName, externalIds, accountConfig, extIdNotes)
-          .get();
+      return AccountState.fromAccountConfig(externalIds, accountConfig, extIdNotes).get();
     }
 
     public ExternalIdNotes getExternalIdNotes() {
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 7ed7ebc..4618835 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
@@ -149,7 +150,9 @@
 
     @Override
     public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading group by ID", "groupId", key)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading group by ID", Metadata.builder().groupId(key.get()).build())) {
         return groupQueryProvider.get().byId(key);
       }
     }
@@ -165,7 +168,9 @@
 
     @Override
     public Optional<InternalGroup> load(String name) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading group by name", "groupName", name)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading group by name", Metadata.builder().groupName(name).build())) {
         return groupQueryProvider.get().byName(AccountGroup.nameKey(name));
       }
     }
@@ -181,7 +186,9 @@
 
     @Override
     public Optional<InternalGroup> load(String uuid) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading group by UUID", "groupUuid", uuid)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading group by UUID", Metadata.builder().groupUuid(uuid).build())) {
         return groups.getGroup(AccountGroup.uuid(uuid));
       }
     }
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 3507ba2..f3c19a8 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.group.InternalGroupQuery;
@@ -153,7 +154,8 @@
     @Override
     public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) {
       try (TraceTimer timer =
-          TraceContext.newTimer("Loading groups with member", "memberId", memberId)) {
+          TraceContext.newTimer(
+              "Loading groups with member", Metadata.builder().accountId(memberId.get()).build())) {
         return groupQueryProvider.get().byMember(memberId).stream()
             .map(InternalGroup::getGroupUUID)
             .collect(toImmutableSet());
@@ -172,7 +174,9 @@
 
     @Override
     public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) {
-      try (TraceTimer timer = TraceContext.newTimer("Loading parent groups", "groupUuid", key)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading parent groups", Metadata.builder().groupUuid(key.get()).build())) {
         return groupQueryProvider.get().bySubgroup(key).stream()
             .map(InternalGroup::getGroupUUID)
             .collect(toImmutableList());
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 49afaf2..99ad570 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -150,20 +150,19 @@
     if (options.contains(FillOptions.AVATARS)) {
       AvatarProvider ap = avatar.get();
       if (ap != null) {
-        info.avatars = new ArrayList<>(3);
+        info.avatars = new ArrayList<>();
         IdentifiedUser user = userFactory.create(account.getId());
 
-        // GWT UI uses DEFAULT_SIZE (26px).
+        // PolyGerrit UI uses the following sizes for avatars:
+        // - 32px for avatars next to names e.g. on the dashboard. This is also Gerrit's default.
+        // - 56px for the user's own avatar in the menu
+        // - 100ox for other user's avatars on dashboards
+        // - 120px for the user's own profile settings page
         addAvatar(ap, info, user, AvatarInfo.DEFAULT_SIZE);
-
-        // PolyGerrit UI prefers 32px and 100px.
         if (!info.avatars.isEmpty()) {
-          if (32 != AvatarInfo.DEFAULT_SIZE) {
-            addAvatar(ap, info, user, 32);
-          }
-          if (100 != AvatarInfo.DEFAULT_SIZE) {
-            addAvatar(ap, info, user, 100);
-          }
+          addAvatar(ap, info, user, 56);
+          addAvatar(ap, info, user, 100);
+          addAvatar(ap, info, user, 120);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index ad861c0..25fca4d 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.Inject;
@@ -157,7 +158,9 @@
 
     @Override
     public AllExternalIds load(ObjectId notesRev) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading external IDs", "revision", notesRev)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading external IDs", Metadata.builder().revision(notesRev.name()).build())) {
         ImmutableSet<ExternalId> externalIds = externalIdReader.all(notesRev);
         externalIds.forEach(ExternalId::checkThatBlobIdIsSet);
         return AllExternalIds.create(externalIds);
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index c12aaf5..3b84fb8 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.Inject;
@@ -354,7 +355,8 @@
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
       try (TraceTimer timer =
-          TraceContext.newTimer("Loading account for username", "username", username)) {
+          TraceContext.newTimer(
+              "Loading account for username", Metadata.builder().username(username).build())) {
         return externalIds
             .get(ExternalId.Key.create(SCHEME_GERRIT, username))
             .map(ExternalId::accountId);
@@ -373,7 +375,9 @@
     @Override
     public Set<AccountGroup.UUID> load(String username) throws Exception {
       try (TraceTimer timer =
-          TraceContext.newTimer("Loading group for member with username", "username", username)) {
+          TraceContext.newTimer(
+              "Loading group for member with username",
+              Metadata.builder().username(username).build())) {
         final DirContext ctx = helper.open();
         try {
           return helper.queryForGroups(ctx, username, null);
@@ -394,7 +398,9 @@
 
     @Override
     public Boolean load(String groupDn) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading groupDn", "groupDn", groupDn)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading groupDn", Metadata.builder().authDomainName(groupDn).build())) {
         final DirContext ctx = helper.open();
         try {
           Name compositeGroupName = new CompositeName().add(groupDn);
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index 502dfa4..1ef5a3b 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Set;
@@ -33,7 +34,7 @@
 public class CacheMetrics {
   @Inject
   public CacheMetrics(MetricMaker metrics, DynamicMap<Cache<?, ?>> cacheMap) {
-    Field<String> F_NAME = Field.ofString("cache_name").build();
+    Field<String> F_NAME = Field.ofString("cache_name", Metadata.Builder::cacheName).build();
 
     CallbackMetric1<String, Long> memEnt =
         metrics.newCallbackMetric(
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 2b44019..ef4e44c 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -237,7 +238,9 @@
 
     @Override
     public ValueHolder<V> load(K key) throws Exception {
-      try (TraceTimer timer = TraceContext.newTimer("Loading value from cache", "key", key)) {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Loading value from cache", Metadata.builder().cacheKey(key.toString()).build())) {
         if (store.mightContain(key)) {
           ValueHolder<V> h = store.getIfPresent(key);
           if (h != null) {
diff --git a/java/com/google/gerrit/server/change/ChangeFinder.java b/java/com/google/gerrit/server/change/ChangeFinder.java
index 1cdb621..2d4f105 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -98,7 +99,8 @@
             new Description("Total number of API calls per identifier type.")
                 .setRate()
                 .setUnit("requests"),
-            Field.ofEnum(ChangeIdType.class, "change_id_type").build());
+            Field.ofEnum(ChangeIdType.class, "change_id_type", Metadata.Builder::changeIdType)
+                .build());
     List<ChangeIdType> configuredChangeIdTypes =
         ConfigUtil.getEnumList(config, "change", "api", "allowedIdentifier", ChangeIdType.ALL);
     // Ensure that PROJECT_NUMERIC_ID can't be removed
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index afe4e52..bbc5748 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,8 @@
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.RequestListener;
+import com.google.gerrit.server.TraceRequestListener;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDeactivator;
@@ -384,6 +386,8 @@
     DynamicSet.setOf(binder(), SubmitRule.class);
     DynamicSet.setOf(binder(), QuotaEnforcer.class);
     DynamicSet.setOf(binder(), PerformanceLogger.class);
+    DynamicSet.setOf(binder(), RequestListener.class);
+    DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/events/EventsMetrics.java b/java/com/google/gerrit/server/events/EventsMetrics.java
index ae70bab..3c87cca 100644
--- a/java/com/google/gerrit/server/events/EventsMetrics.java
+++ b/java/com/google/gerrit/server/events/EventsMetrics.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -31,7 +32,7 @@
         metricMaker.newCounter(
             "events",
             new Description("Triggered events").setRate().setUnit("triggered events"),
-            Field.ofString("type").build());
+            Field.ofString("type", Metadata.Builder::eventType).build());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/extensions/webui/UiActions.java b/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 41fcd95..0bc3d5c 100644
--- a/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendCondition;
@@ -71,7 +72,7 @@
             new com.google.gerrit.metrics.Description("Latency for RestView#getDescription calls")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            Field.ofString("view").build());
+            Field.ofString("view", Metadata.Builder::restViewName).build());
   }
 
   public <R extends RestResource> Iterable<UiAction.Description> from(
diff --git a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
index a8594e7..d1a6df6 100644
--- a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import java.io.IOException;
@@ -32,6 +33,8 @@
  * implements {@link org.eclipse.jgit.transport.AdvertiseRefsHook}.
  */
 public class DefaultAdvertiseRefsHook extends AbstractAdvertiseRefsHook {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final PermissionBackend.ForProject perm;
   private final PermissionBackend.RefFilterOptions opts;
 
@@ -44,6 +47,7 @@
   @Override
   protected Map<String, Ref> getAdvertisedRefs(Repository repo, RevWalk revWalk)
       throws ServiceMayNotContinueException {
+    logger.atFine().log("ref filter options = %s", opts);
     try {
       List<String> prefixes =
           !opts.prefixes().isEmpty() ? opts.prefixes() : ImmutableList.of(RefDatabase.ALL);
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 4088c81..e464e81 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -17,15 +17,17 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.naturalOrder;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
 
-import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
@@ -68,7 +70,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -847,26 +848,22 @@
       return String.format("Merge \"%s\"", c.getShortMessage());
     }
 
-    LinkedHashSet<String> topics = new LinkedHashSet<>(4);
-    for (CodeReviewCommit c : merged) {
-      if (!Strings.isNullOrEmpty(c.change().getTopic())) {
-        topics.add(c.change().getTopic());
-      }
-    }
+    ImmutableSortedSet<String> topics =
+        merged.stream()
+            .map(c -> c.change().getTopic())
+            .filter(t -> !Strings.isNullOrEmpty(t))
+            .map(t -> "\"" + t + "\"")
+            .collect(toImmutableSortedSet(naturalOrder()));
 
-    if (topics.size() == 1) {
-      return String.format("Merge changes from topic \"%s\"", Iterables.getFirst(topics, null));
-    } else if (topics.size() > 1) {
-      return String.format("Merge changes from topics \"%s\"", Joiner.on("\", \"").join(topics));
-    } else {
+    if (!topics.isEmpty()) {
       return String.format(
-          "Merge changes %s%s",
-          FluentIterable.from(merged)
-              .limit(5)
-              .transform(c -> c.change().getKey().abbreviate())
-              .join(Joiner.on(',')),
-          merged.size() > 5 ? ", ..." : "");
+          "Merge changes from topic%s %s",
+          topics.size() > 1 ? "s" : "", topics.stream().collect(joining(", ")));
     }
+    return merged.stream()
+        .limit(5)
+        .map(c -> c.change().getKey().abbreviate())
+        .collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : ""));
   }
 
   public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) {
diff --git a/java/com/google/gerrit/server/git/NotifyConfig.java b/java/com/google/gerrit/server/git/NotifyConfig.java
index d39cf12..2ca2744a 100644
--- a/java/com/google/gerrit/server/git/NotifyConfig.java
+++ b/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.mail.Address;
@@ -113,6 +114,13 @@
 
   @Override
   public String toString() {
-    return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+    return MoreObjects.toStringHelper(this)
+        .add("name", name)
+        .add("addresses", addresses)
+        .add("groups", groups)
+        .add("header", header)
+        .add("types", types)
+        .add("filter", filter)
+        .toString();
   }
 }
diff --git a/java/com/google/gerrit/server/git/PureRevertCache.java b/java/com/google/gerrit/server/git/PureRevertCache.java
index 4937713..6679b52 100644
--- a/java/com/google/gerrit/server/git/PureRevertCache.java
+++ b/java/com/google/gerrit/server/git/PureRevertCache.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
@@ -35,7 +36,6 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import com.google.protobuf.ByteString;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -49,6 +49,7 @@
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
 
 /** Computes and caches if a change is a pure revert of another change. */
 @Singleton
@@ -151,7 +152,9 @@
     @Override
     public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
       try (TraceContext.TraceTimer ignored =
-          TraceContext.newTimer("Loading pure revert", "key", key)) {
+          TraceContext.newTimer(
+              "Loading pure revert",
+              Metadata.builder().cacheKey(key.toString()).projectName(key.getProject()).build())) {
         ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
         ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
         Project.NameKey project = Project.nameKey(key.getProject());
@@ -185,9 +188,8 @@
           }
 
           // Any differences between claimed original's parent and the rebase result indicate that
-          // the
-          // claimedRevert is not a pure revert but made content changes
-          try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+          // the claimedRevert is not a pure revert but made content changes
+          try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
             df.setReader(oi.newReader(), repo.getConfig());
             List<DiffEntry> entries =
                 df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 15284fe..44c0ee3 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -153,7 +154,8 @@
     @Override
     public List<CachedChange> load(Project.NameKey key) throws Exception {
       try (TraceTimer timer =
-              TraceContext.newTimer("Loading changes of project", "projectName", key);
+              TraceContext.newTimer(
+                  "Loading changes of project", Metadata.builder().projectName(key.get()).build());
           ManualRequestContext ctx = requestContext.open()) {
         List<ChangeData> cds =
             queryProvider
diff --git a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
index 8be68bf..4afff2b 100644
--- a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
+++ b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.metrics.Histogram1;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.storage.pack.PackStatistics;
@@ -43,7 +44,8 @@
 
   @Inject
   UploadPackMetricsHook(MetricMaker metricMaker) {
-    Field<Operation> operationField = Field.ofEnum(Operation.class, "operation").build();
+    Field<Operation> operationField =
+        Field.ofEnum(Operation.class, "operation", Metadata.Builder::gitOperation).build();
     requestCount =
         metricMaker.newCounter(
             "git/upload-pack/request_count",
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 6356068..f2180d7 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.io.BufferedReader;
@@ -500,14 +501,12 @@
     try (TraceTimer timer =
             TraceContext.newTimer(
                 "Read file",
-                "fileName",
-                fileName,
-                "ref",
-                getRefName(),
-                "projectName",
-                projectName,
-                "revision",
-                revision.name());
+                Metadata.builder()
+                    .projectName(projectName.get())
+                    .noteDbRefName(getRefName())
+                    .revision(revision.name())
+                    .noteDbFilePath(fileName)
+                    .build());
         TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree())) {
       if (tw != null) {
         ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
@@ -582,7 +581,12 @@
   protected void saveFile(String fileName, byte[] raw) throws IOException {
     try (TraceTimer timer =
         TraceContext.newTimer(
-            "Save file", "fileName", fileName, "ref", getRefName(), "projectName", projectName)) {
+            "Save file",
+            Metadata.builder()
+                .projectName(projectName.get())
+                .noteDbRefName(getRefName())
+                .noteDbFilePath(fileName)
+                .build())) {
       DirCacheEditor editor = newTree.editor();
       if (raw != null && 0 < raw.length) {
         final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 64aecf5..bce5b0a 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -199,12 +200,12 @@
           metricMaker.newHistogram(
               "receivecommits/changes_per_push",
               new Description("number of changes uploaded in a single push.").setCumulative(),
-              Field.ofEnum(PushType.class, "type")
+              Field.ofEnum(PushType.class, "type", Metadata.Builder::pushType)
                   .description("type of push (create/replace, autoclose)")
                   .build());
 
       Field<PushType> pushTypeField =
-          Field.ofEnum(PushType.class, "type")
+          Field.ofEnum(PushType.class, "type", Metadata.Builder::pushType)
               .description("type of push (create/replace, autoclose, normal)")
               .build();
 
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 1444070..f8e6751 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.git.receive;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
@@ -68,6 +67,7 @@
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
@@ -101,6 +101,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.PublishCommentUtil;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -126,6 +128,7 @@
 import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.git.validators.ValidationMessage;
 import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogContext;
 import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.RequestId;
@@ -195,7 +198,6 @@
 import java.util.TreeSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
-import java.util.regex.Matcher;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -329,6 +331,7 @@
   private final ReceiveConfig receiveConfig;
   private final RefOperationValidators.Factory refValidatorsFactory;
   private final ReplaceOp.Factory replaceOpFactory;
+  private final PluginSetContext<RequestListener> requestListeners;
   private final RetryHelper retryHelper;
   private final RequestScopePropagator requestScopePropagator;
   private final Sequences seq;
@@ -346,7 +349,6 @@
 
   // Immutable fields derived from constructor arguments.
   private final boolean allowProjectOwnersToChangeParent;
-  private final boolean allowPushToRefsChanges;
   private final LabelTypes labelTypes;
   private final NoteMap rejectCommits;
   private final PermissionBackend.ForProject permissions;
@@ -408,6 +410,7 @@
       ReceiveConfig receiveConfig,
       RefOperationValidators.Factory refValidatorsFactory,
       ReplaceOp.Factory replaceOpFactory,
+      PluginSetContext<RequestListener> requestListeners,
       RetryHelper retryHelper,
       RequestScopePropagator requestScopePropagator,
       Sequences seq,
@@ -453,6 +456,7 @@
     this.receiveConfig = receiveConfig;
     this.refValidatorsFactory = refValidatorsFactory;
     this.replaceOpFactory = replaceOpFactory;
+    this.requestListeners = requestListeners;
     this.retryHelper = retryHelper;
     this.requestScopePropagator = requestScopePropagator;
     this.seq = seq;
@@ -468,7 +472,6 @@
     this.receivePack = rp;
 
     // Immutable fields derived from constructor arguments.
-    allowPushToRefsChanges = config.getBoolean("receive", "allowPushToRefsChanges", false);
     repo = rp.getRepository();
     project = projectState.getProject();
     labelTypes = projectState.getLabelTypes();
@@ -529,14 +532,21 @@
 
   void processCommands(Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
     parsePushOptions();
+    int commandCount = commands.size();
     try (TraceContext traceContext =
             TraceContext.newTrace(
                 tracePushOption.isPresent(),
                 tracePushOption.orElse(null),
                 (tagName, traceId) -> addMessage(tagName + ": " + traceId));
-        TraceTimer traceTimer = newTimer("processCommands", "commandCount", commands.size());
+        TraceTimer traceTimer =
+            newTimer("processCommands", Metadata.builder().resourceCount(commandCount));
         PerformanceLogContext performanceLogContext =
             new PerformanceLogContext(config, performanceLoggers)) {
+      RequestInfo requestInfo =
+          RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
+              .project(project.getNameKey())
+              .build();
+      requestListeners.runEach(l -> l.onRequest(requestInfo));
       traceContext.addTag(RequestId.Type.RECEIVE_ID, new RequestId(project.getNameKey().get()));
 
       // Log the push options here, rather than in parsePushOptions(), so that they are included
@@ -576,23 +586,17 @@
     logger.atFine().log("Parsing %d commands", commands.size());
 
     List<ReceiveCommand> magicCommands = new ArrayList<>();
-    List<ReceiveCommand> directPatchSetPushCommands = new ArrayList<>();
     List<ReceiveCommand> regularCommands = new ArrayList<>();
 
     for (ReceiveCommand cmd : commands) {
       if (MagicBranch.isMagicBranch(cmd.getRefName())) {
         magicCommands.add(cmd);
-      } else if (isDirectChangesPush(cmd.getRefName())) {
-        directPatchSetPushCommands.add(cmd);
       } else {
         regularCommands.add(cmd);
       }
     }
 
-    int commandTypes =
-        (magicCommands.isEmpty() ? 0 : 1)
-            + (directPatchSetPushCommands.isEmpty() ? 0 : 1)
-            + (regularCommands.isEmpty() ? 0 : 1);
+    int commandTypes = (magicCommands.isEmpty() ? 0 : 1) + (regularCommands.isEmpty() ? 0 : 1);
 
     if (commandTypes > 1) {
       rejectRemaining(commands, "cannot combine normal pushes and magic pushes");
@@ -605,8 +609,6 @@
         return;
       }
 
-      parseDirectChangesPushCommands(directPatchSetPushCommands);
-
       boolean first = true;
       for (ReceiveCommand cmd : magicCommands) {
         if (first) {
@@ -656,7 +658,8 @@
 
   private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
       throws PermissionBackendException, IOException, NoSuchProjectException {
-    try (TraceTimer traceTimer = newTimer("handleRegularCommands", "commandCount", cmds.size())) {
+    try (TraceTimer traceTimer =
+        newTimer("handleRegularCommands", Metadata.builder().resourceCount(cmds.size()))) {
       resultChangeIds.setMagicPush(false);
       for (ReceiveCommand cmd : cmds) {
         parseRegularCommand(cmd);
@@ -840,7 +843,8 @@
 
   private void insertChangesAndPatchSets(List<CreateRequest> newChanges, Task replaceProgress) {
     try (TraceTimer traceTimer =
-        newTimer("insertChangesAndPatchSets", "changeCount", newChanges.size())) {
+        newTimer(
+            "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
       ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
       if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
         logger.atWarning().log(
@@ -984,37 +988,6 @@
     }
   }
 
-  private static boolean isDirectChangesPush(String refname) {
-    Matcher m = NEW_PATCHSET_PATTERN.matcher(refname);
-    return m.matches();
-  }
-
-  private void parseDirectChangesPushCommands(List<ReceiveCommand> cmds) {
-    try (TraceTimer traceTimer =
-        newTimer("parseDirectChangesPushCommands", "commandCount", cmds.size())) {
-      for (ReceiveCommand cmd : cmds) {
-        parseDirectChangesPush(cmd);
-      }
-    }
-  }
-
-  private void parseDirectChangesPush(ReceiveCommand cmd) {
-    if (allowPushToRefsChanges) {
-      try (TraceTimer traceTimer = newTimer("parseDirectChangesPush")) {
-        Matcher m = NEW_PATCHSET_PATTERN.matcher(cmd.getRefName());
-        checkArgument(m.matches());
-
-        // The referenced change must exist and must still be open.
-        Change.Id changeId = Change.Id.parse(m.group(1));
-        parseReplaceCommand(cmd, changeId);
-        messages.add(
-            new ValidationMessage("warning: pushes to refs/changes are deprecated", false));
-      }
-    } else {
-      reject(cmd, "upload to refs/changes not allowed");
-    }
-  }
-
   // Wrap ReceiveCommand so the progress counter works automatically.
   private ReceiveCommand wrapReceiveCommand(ReceiveCommand cmd, Task progress) {
     String refname = cmd.getRefName();
@@ -1415,10 +1388,13 @@
   static class MagicBranchInput {
     private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
 
+    private final IdentifiedUser user;
+    private final ProjectState projectState;
+    private final boolean defaultPublishComments;
+
     boolean deprecatedTopicSeen;
     final ReceiveCommand cmd;
     final LabelTypes labelTypes;
-    private final boolean defaultPublishComments;
     /**
      * Result of running {@link CommentValidator}-s on drafts that are published with the commit
      * (which happens iff {@code --publish-comments} is set). Remains {@code true} if none are
@@ -1582,7 +1558,10 @@
     @Option(name = "--create-cod-token", usage = "create a token for consistency-on-demand")
     private boolean createCodToken;
 
-    MagicBranchInput(IdentifiedUser user, ReceiveCommand cmd, LabelTypes labelTypes) {
+    MagicBranchInput(
+        IdentifiedUser user, ProjectState projectState, ReceiveCommand cmd, LabelTypes labelTypes) {
+      this.user = user;
+      this.projectState = projectState;
       this.deprecatedTopicSeen = false;
       this.cmd = cmd;
       this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
@@ -1695,9 +1674,24 @@
       return ref.substring(0, split);
     }
 
+    public boolean shouldSetWorkInProgressOnNewChanges() {
+      // When wip or ready explicitly provided, leave it as is.
+      if (workInProgress) {
+        return true;
+      }
+      if (ready) {
+        return false;
+      }
+
+      return projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
+          || firstNonNull(user.state().getGeneralPreferences().workInProgressByDefault, false);
+    }
+
     NotifyResolver.Result getNotifyForNewChange() {
       return NotifyResolver.Result.create(
-          firstNonNull(notifyHandling, workInProgress ? NotifyHandling.OWNER : NotifyHandling.ALL),
+          firstNonNull(
+              notifyHandling,
+              shouldSetWorkInProgressOnNewChanges() ? NotifyHandling.OWNER : NotifyHandling.ALL),
           ImmutableSetMultimap.<RecipientType, Account.Id>builder()
               .putAll(RecipientType.TO, notifyTo)
               .putAll(RecipientType.CC, notifyCc)
@@ -1726,7 +1720,7 @@
   private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException {
     try (TraceTimer traceTimer = newTimer("parseMagicBranch")) {
       logger.atFine().log("Found magic branch %s", cmd.getRefName());
-      MagicBranchInput magicBranch = new MagicBranchInput(user, cmd, labelTypes);
+      MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes);
 
       String ref;
       magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
@@ -1934,7 +1928,8 @@
   // looking to see if we can compute a merge base between the new
   // commits and the target branch head.
   private boolean validateConnected(ReceiveCommand cmd, BranchNameKey dest, RevCommit tip) {
-    try (TraceTimer traceTimer = newTimer("validateConnected", "branch", dest.branch())) {
+    try (TraceTimer traceTimer =
+        newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) {
       RevWalk walk = receivePack.getRevWalk();
       try {
         Ref targetRef = receivePack.getAdvertisedRefs().get(dest.branch());
@@ -1992,64 +1987,6 @@
     return receivePack.getRevWalk().parseCommit(r.getObjectId());
   }
 
-  // Handle an upload to refs/changes/XX/CHANGED-NUMBER.
-  private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) {
-    try (TraceTimer traceTimer = newTimer("parseReplaceCommand")) {
-      logger.atFine().log("Parsing replace command");
-      if (cmd.getType() != ReceiveCommand.Type.CREATE) {
-        reject(cmd, "invalid usage");
-        return;
-      }
-
-      RevCommit newCommit;
-      try {
-        newCommit = receivePack.getRevWalk().parseCommit(cmd.getNewId());
-        logger.atFine().log("Replacing with %s", newCommit);
-      } catch (IOException e) {
-        logger.atSevere().withCause(e).log("Cannot parse %s as commit", cmd.getNewId().name());
-        reject(cmd, "invalid commit");
-        return;
-      }
-
-      Change changeEnt;
-      try {
-        changeEnt = notesFactory.createChecked(project.getNameKey(), changeId).getChange();
-      } catch (NoSuchChangeException e) {
-        logger.atSevere().withCause(e).log("Change not found %s", changeId);
-        reject(cmd, "change " + changeId + " not found");
-        return;
-      } catch (StorageException e) {
-        logger.atSevere().withCause(e).log("Cannot lookup existing change %s", changeId);
-        reject(cmd, "database error");
-        return;
-      }
-      if (!project.getNameKey().equals(changeEnt.getProject())) {
-        reject(cmd, "change " + changeId + " does not belong to project " + project.getName());
-        return;
-      }
-
-      BranchCommitValidator validator =
-          commitValidatorFactory.create(projectState, changeEnt.getDest(), user);
-      try {
-        BranchCommitValidator.Result validationResult =
-            validator.validateCommit(
-                receivePack.getRevWalk().getObjectReader(),
-                cmd,
-                newCommit,
-                false,
-                rejectCommits,
-                changeEnt);
-        messages.addAll(validationResult.messages());
-        if (validationResult.isValid()) {
-          logger.atFine().log("Replacing change %s", changeEnt.getId());
-          requestReplaceAndValidateComments(cmd, true, changeEnt, newCommit);
-        }
-      } catch (IOException e) {
-        reject(cmd, "I/O exception validating commit");
-      }
-    }
-  }
-
   /**
    * Update an existing change. If draft comments are to be published, these are validated and may
    * be withheld.
@@ -2484,7 +2421,8 @@
   // Mark all branch tips as uninteresting in the given revwalk,
   // so we get only the new commits when walking rw.
   private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
-    try (TraceTimer traceTimer = newTimer("markHeadsAsUninteresting", "forRef", forRef)) {
+    try (TraceTimer traceTimer =
+        newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) {
       int i = 0;
       for (Ref ref : allRefs().values()) {
         if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
@@ -2549,15 +2487,13 @@
 
     private void setChangeId(int id) {
       try (TraceTimer traceTimer = newTimer(CreateRequest.class, "setChangeId")) {
-        possiblyOverrideWorkInProgress();
-
         changeId = Change.id(id);
         ins =
             changeInserterFactory
                 .create(changeId, commit, refName)
                 .setTopic(magicBranch.topic)
                 .setPrivate(setChangeAsPrivate)
-                .setWorkInProgress(magicBranch.workInProgress)
+                .setWorkInProgress(magicBranch.shouldSetWorkInProgressOnNewChanges())
                 // Changes already validated in validateNewCommits.
                 .setValidate(false);
 
@@ -2571,16 +2507,6 @@
       }
     }
 
-    private void possiblyOverrideWorkInProgress() {
-      // When wip or ready explicitly provided, leave it as is.
-      if (magicBranch.workInProgress || magicBranch.ready) {
-        return;
-      }
-      magicBranch.workInProgress =
-          projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
-              || firstNonNull(user.state().getGeneralPreferences().workInProgressByDefault, false);
-    }
-
     private void addOps(BatchUpdate bu) throws RestApiException {
       try (TraceTimer traceTimer = newTimer(CreateRequest.class, "addOps")) {
         checkState(changeId != null, "must call setChangeId before addOps");
@@ -2679,14 +2605,27 @@
           "Processing submit with tip change %s (%s)",
           tipChange.getId(), magicBranch.cmd.getNewId());
       try (MergeOp op = mergeOpProvider.get()) {
-        op.merge(tipChange, user, false, new SubmitInput(), false);
+        SubmitInput submitInput = new SubmitInput();
+        submitInput.notify = magicBranch.notifyHandling;
+        submitInput.notifyDetails = new HashMap<>();
+        submitInput.notifyDetails.put(
+            RecipientType.TO,
+            new NotifyInfo(magicBranch.notifyTo.stream().map(Object::toString).collect(toList())));
+        submitInput.notifyDetails.put(
+            RecipientType.CC,
+            new NotifyInfo(magicBranch.notifyCc.stream().map(Object::toString).collect(toList())));
+        submitInput.notifyDetails.put(
+            RecipientType.BCC,
+            new NotifyInfo(magicBranch.notifyBcc.stream().map(Object::toString).collect(toList())));
+        op.merge(tipChange, user, false, submitInput, false);
       }
     }
   }
 
   private void preparePatchSetsForReplace(List<CreateRequest> newChanges) {
     try (TraceTimer traceTimer =
-        newTimer("preparePatchSetsForReplace", "changeCount", newChanges.size())) {
+        newTimer(
+            "preparePatchSetsForReplace", Metadata.builder().resourceCount(newChanges.size()))) {
       try {
         readChangesForReplace();
         for (ReplaceRequest req : replaceByChange.values()) {
@@ -3210,7 +3149,7 @@
   private void validateRegularPushCommits(BranchNameKey branch, ReceiveCommand cmd)
       throws PermissionBackendException {
     try (TraceTimer traceTimer =
-        newTimer("validateRegularPushCommits", "branch", branch.branch())) {
+        newTimer("validateRegularPushCommits", Metadata.builder().branchName(branch.branch()))) {
       boolean skipValidation =
           !RefNames.REFS_CONFIG.equals(cmd.getRefName())
               && !(MagicBranch.isMagicBranch(cmd.getRefName())
@@ -3412,7 +3351,8 @@
   }
 
   private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(BranchNameKey branch) {
-    try (TraceTimer traceTimer = newTimer("openChangesByKeyByBranch", "branch", branch.branch())) {
+    try (TraceTimer traceTimer =
+        newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) {
       Map<Change.Key, ChangeNotes> r = new HashMap<>();
       for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
         try {
@@ -3437,16 +3377,16 @@
   }
 
   private TraceTimer newTimer(Class<?> clazz, String name) {
-    return TraceContext.newTimer(clazz.getSimpleName() + "#" + name, "projectName", project);
+    return newTimer(clazz, name, Metadata.builder());
   }
 
-  private TraceTimer newTimer(String name, String key, @Nullable Object value) {
-    return newTimer(getClass(), name, key, value);
+  private TraceTimer newTimer(String name, Metadata.Builder metadataBuilder) {
+    return newTimer(getClass(), name, metadataBuilder);
   }
 
-  private TraceTimer newTimer(Class<?> clazz, String name, String key, @Nullable Object value) {
-    return TraceContext.newTimer(
-        clazz.getSimpleName() + "#" + name, "projectName", project, key, value);
+  private TraceTimer newTimer(Class<?> clazz, String name, Metadata.Builder metadataBuilder) {
+    metadataBuilder.projectName(project.getName());
+    return TraceContext.newTimer(clazz.getSimpleName() + "#" + name, metadataBuilder.build());
   }
 
   private static void reject(ReceiveCommand cmd, String why) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index a9a1a5d..02a24f7 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -146,12 +146,7 @@
               new CommitterUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectState),
               new ChangeIdValidator(
-                  projectState,
-                  user,
-                  urlFormatter.get(),
-                  installCommitMsgHookCommand,
-                  sshInfo,
-                  change),
+                  user, urlFormatter.get(), installCommitMsgHookCommand, sshInfo, change),
               new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators, skipValidation),
@@ -178,12 +173,7 @@
               new AuthorUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectCache.checkedGet(branch.project())),
               new ChangeIdValidator(
-                  projectState,
-                  user,
-                  urlFormatter.get(),
-                  installCommitMsgHookCommand,
-                  sshInfo,
-                  change),
+                  user, urlFormatter.get(), installCommitMsgHookCommand, sshInfo, change),
               new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects),
               new PluginCommitValidationListener(pluginValidators),
               new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker),
@@ -257,7 +247,6 @@
 
     private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
 
-    private final ProjectState projectState;
     private final UrlFormatter urlFormatter;
     private final String installCommitMsgHookCommand;
     private final SshInfo sshInfo;
@@ -265,13 +254,11 @@
     private final Change change;
 
     public ChangeIdValidator(
-        ProjectState projectState,
         IdentifiedUser user,
         UrlFormatter urlFormatter,
         String installCommitMsgHookCommand,
         SshInfo sshInfo,
         Change change) {
-      this.projectState = projectState;
       this.urlFormatter = urlFormatter;
       this.installCommitMsgHookCommand = installCommitMsgHookCommand;
       this.sshInfo = sshInfo;
@@ -307,10 +294,8 @@
                   Type.ERROR));
           throw new CommitValidationException(CHANGE_ID_ABOVE_FOOTER_MSG, messages);
         }
-        if (projectState.is(BooleanProjectConfig.REQUIRE_CHANGE_ID)) {
-          messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
-          throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
-        }
+        messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
+        throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
       } else if (idList.size() > 1) {
         throw new CommitValidationException(MULTIPLE_CHANGE_ID_MSG, messages);
       } else {
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 2f9ce01..ed7eab8 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.group.GroupAuditService;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.update.RetryHelper;
@@ -266,8 +267,9 @@
     try (TraceTimer timer =
         TraceContext.newTimer(
             "Creating group",
-            "groupName",
-            groupUpdate.getName().orElseGet(groupCreation::getNameKey))) {
+            Metadata.builder()
+                .groupName(groupUpdate.getName().orElseGet(groupCreation::getNameKey).get())
+                .build())) {
       InternalGroup createdGroup = createGroupInNoteDbWithRetry(groupCreation, groupUpdate);
       evictCachesOnGroupCreation(createdGroup);
       dispatchAuditEventsOnGroupCreation(createdGroup);
@@ -287,7 +289,9 @@
    */
   public void updateGroup(AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
       throws DuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException {
-    try (TraceTimer timer = TraceContext.newTimer("Updating group", "groupUuid", groupUuid)) {
+    try (TraceTimer timer =
+        TraceContext.newTimer(
+            "Updating group", Metadata.builder().groupUuid(groupUuid.get()).build())) {
       Optional<Timestamp> updatedOn = groupUpdate.getUpdatedOn();
       if (!updatedOn.isPresent()) {
         updatedOn = Optional.of(TimeUtil.nowTs());
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index 79bd005..a2f6002 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -22,7 +22,10 @@
 import com.google.common.truth.IterableSubject;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.InternalGroup;
+import java.sql.Timestamp;
+import org.eclipse.jgit.lib.ObjectId;
 
 public class InternalGroupSubject extends Subject {
 
@@ -41,12 +44,12 @@
     this.group = group;
   }
 
-  public ComparableSubject groupUuid() {
+  public ComparableSubject<AccountGroup.UUID> groupUuid() {
     isNotNull();
     return check("getGroupUUID()").that(group.getGroupUUID());
   }
 
-  public ComparableSubject nameKey() {
+  public ComparableSubject<AccountGroup.NameKey> nameKey() {
     isNotNull();
     return check("getNameKey()").that(group.getNameKey());
   }
@@ -66,7 +69,7 @@
     return check("getDescription()").that(group.getDescription());
   }
 
-  public ComparableSubject ownerGroupUuid() {
+  public ComparableSubject<AccountGroup.UUID> ownerGroupUuid() {
     isNotNull();
     return check("getOwnerGroupUUID()").that(group.getOwnerGroupUUID());
   }
@@ -76,7 +79,7 @@
     return check("isVisibleToAll()").that(group.isVisibleToAll());
   }
 
-  public ComparableSubject createdOn() {
+  public ComparableSubject<Timestamp> createdOn() {
     isNotNull();
     return check("getCreatedOn()").that(group.getCreatedOn());
   }
@@ -91,7 +94,7 @@
     return check("getSubgroups()").that(group.getSubgroups());
   }
 
-  public ComparableSubject refState() {
+  public ComparableSubject<ObjectId> refState() {
     isNotNull();
     return check("getRefState()").that(group.getRefState());
   }
diff --git a/java/com/google/gerrit/server/index/OnlineReindexMode.java b/java/com/google/gerrit/server/index/OnlineReindexMode.java
new file mode 100644
index 0000000..123229a
--- /dev/null
+++ b/java/com/google/gerrit/server/index/OnlineReindexMode.java
@@ -0,0 +1,33 @@
+// 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.index;
+
+import java.util.Optional;
+
+public class OnlineReindexMode {
+  private static ThreadLocal<Boolean> isOnlineReindex = new ThreadLocal<>();
+
+  public static boolean isActive() {
+    return Optional.ofNullable(isOnlineReindex.get()).orElse(Boolean.FALSE);
+  }
+
+  public static void begin() {
+    isOnlineReindex.set(Boolean.TRUE);
+  }
+
+  public static void end() {
+    isOnlineReindex.set(Boolean.FALSE);
+  }
+}
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index ab3a96d..b881a35 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -31,6 +31,8 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.Collections;
@@ -144,7 +146,11 @@
                     RefState.create(
                             RefNames.refsUsers(a.getAccount().getId()),
                             ObjectId.fromString(a.getAccount().getMetaId()))
-                        .toByteArray(a.getAllUsersNameForIndexing()));
+                        // We use the default AllUsers name to avoid having to pass around that
+                        // variable just for indexing.
+                        // This field is only used for staleness detection which will discover the
+                        // default name and replace it with the actually configured name.
+                        .toByteArray(new AllUsersName(AllUsersNameProvider.DEFAULT)));
               });
 
   /**
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index da22eac..5c2f551 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -91,20 +92,20 @@
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Replacing account in index",
-                "accountId",
-                id.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .accountId(id.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.replace(accountState.get());
         }
       } else {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Deleting account in index",
-                "accountId",
-                id.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .accountId(id.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(id);
         }
       }
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
index 4664700..0423bb9 100644
--- a/java/com/google/gerrit/server/index/account/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.inject.Inject;
@@ -106,7 +107,11 @@
 
     for (Map.Entry<Project.NameKey, RefState> e :
         RefState.parseStates(result.get().getValue(AccountField.REF_STATE)).entries()) {
-      try (Repository repo = repoManager.openRepository(e.getKey())) {
+      // Custom All-Users repository names are not indexed. Instead, the default name is used.
+      // Therefore, defer to the currently configured All-Users name.
+      Project.NameKey repoName =
+          e.getKey().get().equals(AllUsersNameProvider.DEFAULT) ? allUsersName : e.getKey();
+      try (Repository repo = repoManager.openRepository(repoName)) {
         if (!e.getValue().match(repo)) {
           // Ref was modified since the account was indexed.
           return true;
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 3015187..dde7c1f 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.OnlineReindexMode;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
 import com.google.gerrit.server.project.ProjectCache;
@@ -214,6 +215,8 @@
     @Override
     public Void call() throws Exception {
       try (Repository repo = repoManager.openRepository(project)) {
+        OnlineReindexMode.begin();
+
         // Order of scanning changes is undefined. This is ok if we assume that packfile locality is
         // not important for indexing, since sites should have a fully populated DiffSummary cache.
         // It does mean that reindexing after invalidating the DiffSummary cache will be expensive,
@@ -222,6 +225,8 @@
         notesFactory.scan(repo, project).forEach(r -> index(r));
       } catch (RepositoryNotFoundException rnfe) {
         logger.atSevere().log(rnfe.getMessage());
+      } finally {
+        OnlineReindexMode.end();
       }
       return null;
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index c2fbb85..87ee27f 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -187,10 +188,10 @@
       try (TraceTimer traceTimer =
           TraceContext.newTimer(
               "Replacing change in index",
-              "changeId",
-              cd.getId().get(),
-              "indexVersion",
-              i.getSchema().getVersion())) {
+              Metadata.builder()
+                  .changeId(cd.getId().get())
+                  .indexVersion(i.getSchema().getVersion())
+                  .build())) {
         i.replace(cd);
       }
     }
@@ -378,10 +379,10 @@
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Deleting change in index",
-                "changeId",
-                id.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .changeId(id.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(id);
         }
       }
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index c49a55e..8bcc52c 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -91,20 +92,20 @@
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Replacing group",
-                "groupUuid",
-                uuid.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .groupUuid(uuid.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.replace(internalGroup.get());
         }
       } else {
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Deleting group",
-                "groupUuid",
-                uuid.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .groupUuid(uuid.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(uuid);
         }
       }
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
index 5f022f2..199119a 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -79,10 +80,10 @@
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Replacing project",
-                "projectName",
-                nameKey.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .projectName(nameKey.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.replace(projectData);
         }
       }
@@ -93,10 +94,10 @@
         try (TraceTimer traceTimer =
             TraceContext.newTimer(
                 "Deleting project",
-                "projectName",
-                nameKey.get(),
-                "indexVersion",
-                i.getSchema().getVersion())) {
+                Metadata.builder()
+                    .projectName(nameKey.get())
+                    .indexVersion(i.getSchema().getVersion())
+                    .build())) {
           i.delete(nameKey);
         }
       }
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
new file mode 100644
index 0000000..400f48f
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -0,0 +1,218 @@
+// 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.logging;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
+/** Metadata that is provided to {@link PerformanceLogger}s as context for performance records. */
+@AutoValue
+public abstract class Metadata {
+  // The numeric ID of an account.
+  public abstract Optional<Integer> accountId();
+
+  // The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
+  // PLUGIN_UPDATE).
+  public abstract Optional<String> actionType();
+
+  // An authentication domain name.
+  public abstract Optional<String> authDomainName();
+
+  // The name of a branch.
+  public abstract Optional<String> branchName();
+
+  // Key of an entity in a cache.
+  public abstract Optional<String> cacheKey();
+
+  // The name of a cache.
+  public abstract Optional<String> cacheName();
+
+  // The name of the implementation class.
+  public abstract Optional<String> className();
+
+  // The numeric ID of a change.
+  public abstract Optional<Integer> changeId();
+
+  // The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
+  public abstract Optional<String> changeIdType();
+
+  // The type of an event.
+  public abstract Optional<String> eventType();
+
+  // The value of the @Export annotation which was used to register a plugin extension.
+  public abstract Optional<String> exportValue();
+
+  // Path of a file in a repository.
+  public abstract Optional<String> filePath();
+
+  // Garbage collector name.
+  public abstract Optional<String> garbageCollectorName();
+
+  // Git operation (CLONE, FETCH).
+  public abstract Optional<String> gitOperation();
+
+  // The numeric ID of an internal group.
+  public abstract Optional<Integer> groupId();
+
+  // The name of a group.
+  public abstract Optional<String> groupName();
+
+  // The UUID of a group.
+  public abstract Optional<String> groupUuid();
+
+  // HTTP status response code.
+  public abstract Optional<Integer> httpStatus();
+
+  // The name of a secondary index.
+  public abstract Optional<String> indexName();
+
+  // The version of a secondary index.
+  public abstract Optional<Integer> indexVersion();
+
+  // The name of the implementation method.
+  public abstract Optional<String> methodName();
+
+  // Boolean: one or more
+  public abstract Optional<Boolean> multiple();
+
+  // Path of a metadata file in NoteDb.
+  public abstract Optional<String> noteDbFilePath();
+
+  // Name of a metadata ref in NoteDb.
+  public abstract Optional<String> noteDbRefName();
+
+  // Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS).
+  public abstract Optional<String> noteDbSequenceType();
+
+  // Name of a "table" in NoteDb (if set, always CHANGES).
+  public abstract Optional<String> noteDbTable();
+
+  // The ID of a patch set.
+  public abstract Optional<Integer> patchSetId();
+
+  // Plugin metadata that doesn't fit into any other category.
+  public abstract ImmutableList<PluginMetadata> pluginMetadata();
+
+  // The name of a plugin.
+  public abstract Optional<String> pluginName();
+
+  // The name of a Gerrit project (aka Git repository).
+  public abstract Optional<String> projectName();
+
+  // The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE).
+  public abstract Optional<String> pushType();
+
+  // The number of resources that is processed.
+  public abstract Optional<Integer> resourceCount();
+
+  // The name of a REST view.
+  public abstract Optional<String> restViewName();
+
+  // The SHA1 of Git commit.
+  public abstract Optional<String> revision();
+
+  // The username of an account.
+  public abstract Optional<String> username();
+
+  public static Metadata.Builder builder() {
+    return new AutoValue_Metadata.Builder();
+  }
+
+  public static Metadata empty() {
+    return builder().build();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder accountId(int accountId);
+
+    public abstract Builder actionType(@Nullable String actionType);
+
+    public abstract Builder authDomainName(@Nullable String authDomainName);
+
+    public abstract Builder branchName(@Nullable String branchName);
+
+    public abstract Builder cacheKey(@Nullable String cacheKey);
+
+    public abstract Builder cacheName(@Nullable String cacheName);
+
+    public abstract Builder className(@Nullable String className);
+
+    public abstract Builder changeId(int changeId);
+
+    public abstract Builder changeIdType(@Nullable String changeIdType);
+
+    public abstract Builder eventType(@Nullable String eventType);
+
+    public abstract Builder exportValue(@Nullable String exportValue);
+
+    public abstract Builder filePath(@Nullable String filePath);
+
+    public abstract Builder garbageCollectorName(@Nullable String garbageCollectorName);
+
+    public abstract Builder gitOperation(@Nullable String gitOperation);
+
+    public abstract Builder groupId(int groupId);
+
+    public abstract Builder groupName(@Nullable String groupName);
+
+    public abstract Builder groupUuid(@Nullable String groupUuid);
+
+    public abstract Builder httpStatus(int httpStatus);
+
+    public abstract Builder indexName(@Nullable String indexName);
+
+    public abstract Builder indexVersion(int indexVersion);
+
+    public abstract Builder methodName(@Nullable String methodName);
+
+    public abstract Builder multiple(boolean multiple);
+
+    public abstract Builder noteDbFilePath(@Nullable String noteDbFilePath);
+
+    public abstract Builder noteDbRefName(@Nullable String noteDbRefName);
+
+    public abstract Builder noteDbSequenceType(@Nullable String noteDbSequenceType);
+
+    public abstract Builder noteDbTable(@Nullable String noteDbTable);
+
+    public abstract Builder patchSetId(int patchSetId);
+
+    abstract ImmutableList.Builder<PluginMetadata> pluginMetadataBuilder();
+
+    public Builder addPluginMetadata(PluginMetadata pluginMetadata) {
+      pluginMetadataBuilder().add(pluginMetadata);
+      return this;
+    }
+
+    public abstract Builder pluginName(@Nullable String pluginName);
+
+    public abstract Builder projectName(@Nullable String projectName);
+
+    public abstract Builder pushType(@Nullable String pushType);
+
+    public abstract Builder resourceCount(int resourceCount);
+
+    public abstract Builder restViewName(@Nullable String restViewName);
+
+    public abstract Builder revision(@Nullable String revision);
+
+    public abstract Builder username(@Nullable String username);
+
+    public abstract Metadata build();
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogRecord.java b/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
index d30f862..046eeb3 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogRecord.java
@@ -14,16 +14,14 @@
 
 package com.google.gerrit.server.logging;
 
-import static java.util.Objects.requireNonNull;
-
 import com.google.auto.value.AutoValue;
-import com.google.gerrit.common.Nullable;
+import java.util.Optional;
 
 /**
  * The record of an operation for which the execution time was measured.
  *
- * <p>Meta data is stored in separate key/value fields to avoid expensive instantiations of Map
- * objects.
+ * <p>Metadata to provide additional context can be included by providing a {@link Metadata}
+ * instance.
  */
 @AutoValue
 public abstract class PerformanceLogRecord {
@@ -35,8 +33,7 @@
    * @return the performance log record
    */
   public static PerformanceLogRecord create(String operation, long durationMs) {
-    return new AutoValue_PerformanceLogRecord(
-        operation, durationMs, null, null, null, null, null, null, null, null);
+    return new AutoValue_PerformanceLogRecord(operation, durationMs, Optional.empty());
   }
 
   /**
@@ -44,175 +41,22 @@
    *
    * @param operation the name of operation the is was performed
    * @param durationMs the execution time in milliseconds
-   * @param key meta data key
-   * @param value meta data value
+   * @param metadata metadata
    * @return the performance log record
    */
-  public static PerformanceLogRecord create(
-      String operation, long durationMs, String key, @Nullable Object value) {
-    return new AutoValue_PerformanceLogRecord(
-        operation, durationMs, requireNonNull(key), value, null, null, null, null, null, null);
-  }
-
-  /**
-   * Creates a performance log record with meta data.
-   *
-   * @param operation the name of operation the is was performed
-   * @param durationMs the execution time in milliseconds
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @return the performance log record
-   */
-  public static PerformanceLogRecord create(
-      String operation,
-      long durationMs,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2) {
-    return new AutoValue_PerformanceLogRecord(
-        operation,
-        durationMs,
-        requireNonNull(key1),
-        value1,
-        requireNonNull(key2),
-        value2,
-        null,
-        null,
-        null,
-        null);
-  }
-
-  /**
-   * Creates a performance log record with meta data.
-   *
-   * @param operation the name of operation the is was performed
-   * @param durationMs the execution time in milliseconds
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @param key3 third meta data key
-   * @param value3 third meta data value
-   * @return the performance log record
-   */
-  public static PerformanceLogRecord create(
-      String operation,
-      long durationMs,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2,
-      String key3,
-      @Nullable Object value3) {
-    return new AutoValue_PerformanceLogRecord(
-        operation,
-        durationMs,
-        requireNonNull(key1),
-        value1,
-        requireNonNull(key2),
-        value2,
-        requireNonNull(key3),
-        value3,
-        null,
-        null);
-  }
-
-  /**
-   * Creates a performance log record with meta data.
-   *
-   * @param operation the name of operation the is was performed
-   * @param durationMs the execution time in milliseconds
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @param key3 third meta data key
-   * @param value3 third meta data value
-   * @param key4 forth meta data key
-   * @param value4 forth meta data value
-   * @return the performance log record
-   */
-  public static PerformanceLogRecord create(
-      String operation,
-      long durationMs,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2,
-      String key3,
-      @Nullable Object value3,
-      String key4,
-      @Nullable Object value4) {
-    return new AutoValue_PerformanceLogRecord(
-        operation,
-        durationMs,
-        requireNonNull(key1),
-        value1,
-        requireNonNull(key2),
-        value2,
-        requireNonNull(key3),
-        value3,
-        requireNonNull(key4),
-        value4);
+  public static PerformanceLogRecord create(String operation, long durationMs, Metadata metadata) {
+    return new AutoValue_PerformanceLogRecord(operation, durationMs, Optional.of(metadata));
   }
 
   public abstract String operation();
 
   public abstract long durationMs();
 
-  @Nullable
-  public abstract String key1();
-
-  @Nullable
-  public abstract Object value1();
-
-  @Nullable
-  public abstract String key2();
-
-  @Nullable
-  public abstract Object value2();
-
-  @Nullable
-  public abstract String key3();
-
-  @Nullable
-  public abstract Object value3();
-
-  @Nullable
-  public abstract String key4();
-
-  @Nullable
-  public abstract Object value4();
+  public abstract Optional<Metadata> metadata();
 
   void writeTo(PerformanceLogger performanceLogger) {
-    if (key4() != null) {
-      requireNonNull(key1());
-      requireNonNull(key2());
-      requireNonNull(key3());
-      performanceLogger.log(
-          operation(),
-          durationMs(),
-          key1(),
-          value1(),
-          key2(),
-          value2(),
-          key3(),
-          value3(),
-          key4(),
-          value4());
-    } else if (key3() != null) {
-      requireNonNull(key1());
-      requireNonNull(key2());
-      performanceLogger.log(
-          operation(), durationMs(), key1(), value1(), key2(), value2(), key3(), value3());
-    } else if (key2() != null) {
-      requireNonNull(key1());
-      performanceLogger.log(operation(), durationMs(), key1(), value1(), key2(), value2());
-    } else if (key1() != null) {
-      performanceLogger.log(operation(), durationMs(), key1(), value1());
+    if (metadata().isPresent()) {
+      performanceLogger.log(operation(), durationMs(), metadata().get());
     } else {
       performanceLogger.log(operation(), durationMs());
     }
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogger.java b/java/com/google/gerrit/server/logging/PerformanceLogger.java
index 3e33a3a..74a1684 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogger.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogger.java
@@ -14,11 +14,7 @@
 
 package com.google.gerrit.server.logging;
 
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import java.util.Map;
-import java.util.Optional;
 
 /**
  * Extension point for logging performance records.
@@ -29,7 +25,7 @@
  * performance log for further analysis.
  *
  * <p>For optimal performance implementors should overwrite the default <code>log</code> methods to
- * avoid unneeded instantiation of Map objects.
+ * avoid an unneeded instantiation of Metadata.
  */
 @ExtensionPoint
 public interface PerformanceLogger {
@@ -40,7 +36,7 @@
    * @param durationMs time that the execution of the operation took (in milliseconds)
    */
   default void log(String operation, long durationMs) {
-    log(operation, durationMs, ImmutableMap.of());
+    log(operation, durationMs, Metadata.empty());
   }
 
   /**
@@ -48,117 +44,7 @@
    *
    * @param operation operation that was performed
    * @param durationMs time that the execution of the operation took (in milliseconds)
-   * @param key meta data key
-   * @param value meta data value
+   * @param metadata metadata
    */
-  default void log(String operation, long durationMs, String key, @Nullable Object value) {
-    log(operation, durationMs, ImmutableMap.of(key, Optional.ofNullable(value)));
-  }
-
-  /**
-   * Record the execution time of an operation in a performance log.
-   *
-   * @param operation operation that was performed
-   * @param durationMs time that the execution of the operation took (in milliseconds)
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   */
-  default void log(
-      String operation,
-      long durationMs,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2) {
-    log(
-        operation,
-        durationMs,
-        ImmutableMap.of(key1, Optional.ofNullable(value1), key2, Optional.ofNullable(value2)));
-  }
-
-  /**
-   * Record the execution time of an operation in a performance log.
-   *
-   * @param operation operation that was performed
-   * @param durationMs time that the execution of the operation took (in milliseconds)
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @param key3 third meta data key
-   * @param value3 third meta data value
-   */
-  default void log(
-      String operation,
-      long durationMs,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2,
-      String key3,
-      @Nullable Object value3) {
-    log(
-        operation,
-        durationMs,
-        ImmutableMap.of(
-            key1,
-            Optional.ofNullable(value1),
-            key2,
-            Optional.ofNullable(value2),
-            key3,
-            Optional.ofNullable(value3)));
-  }
-
-  /**
-   * Record the execution time of an operation in a performance log.
-   *
-   * @param operation operation that was performed
-   * @param durationMs time that the execution of the operation took (in milliseconds)
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @param key3 third meta data key
-   * @param value3 third meta data value
-   * @param key4 fourth meta data key
-   * @param value4 fourth meta data value
-   */
-  default void log(
-      String operation,
-      long durationMs,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2,
-      String key3,
-      @Nullable Object value3,
-      String key4,
-      @Nullable Object value4) {
-    log(
-        operation,
-        durationMs,
-        ImmutableMap.of(
-            key1,
-            Optional.ofNullable(value1),
-            key2,
-            Optional.ofNullable(value2),
-            key3,
-            Optional.ofNullable(value3),
-            key4,
-            Optional.ofNullable(value4)));
-  }
-
-  /**
-   * Record the execution time of an operation in a performance log.
-   *
-   * <p>For small numbers of meta data entries the instantiation of a map should avoided by using
-   * one of the <code>log</code> methods that allows to pass in meta data entries directly.
-   *
-   * @param operation operation that was performed
-   * @param durationMs time that the execution of the operation took (in milliseconds)
-   * @param metaData key-value map with meta data
-   */
-  void log(String operation, long durationMs, Map<String, Optional<Object>> metaData);
+  void log(String operation, long durationMs, Metadata metadata);
 }
diff --git a/java/com/google/gerrit/server/logging/PluginMetadata.java b/java/com/google/gerrit/server/logging/PluginMetadata.java
new file mode 100644
index 0000000..21f7359
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/PluginMetadata.java
@@ -0,0 +1,39 @@
+// 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.logging;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+
+/**
+ * Key-value pair for custom metadata that is provided by plugins.
+ *
+ * <p>PluginMetadata allows plugins to include custom metadata into the {@link Metadata} instances
+ * that are provided as context for performance tracing.
+ *
+ * <p>Plugins should use PluginMetadata only for metadata kinds that are not known to Gerrit core
+ * (metadata for which {@link Metadata} doesn't have a dedicated field).
+ */
+@AutoValue
+public abstract class PluginMetadata {
+  public static PluginMetadata create(String key, @Nullable String value) {
+    return new AutoValue_PluginMetadata(key, Optional.ofNullable(value));
+  }
+
+  public abstract String key();
+
+  public abstract Optional<String> value();
+}
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index b018da4..06db7b4 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -166,105 +166,13 @@
    * Opens a new timer that logs the time for an operation if request tracing is enabled.
    *
    * @param operation the name of operation the is being performed
-   * @param key meta data key
-   * @param value meta data value
+   * @param metadata metadata
    * @return the trace timer
    */
-  public static TraceTimer newTimer(String operation, String key, @Nullable Object value) {
+  public static TraceTimer newTimer(String operation, Metadata metadata) {
     return new TraceTimer(
         requireNonNull(operation, "operation is required"),
-        requireNonNull(key, "key is required"),
-        value);
-  }
-
-  /**
-   * Opens a new timer that logs the time for an operation if request tracing is enabled.
-   *
-   * @param operation the name of operation the is being performed
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @return the trace timer
-   */
-  public static TraceTimer newTimer(
-      String operation,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2) {
-    return new TraceTimer(
-        requireNonNull(operation, "operation is required"),
-        requireNonNull(key1, "key1 is required"),
-        value1,
-        requireNonNull(key2, "key2 is required"),
-        value2);
-  }
-
-  /**
-   * Opens a new timer that logs the time for an operation if request tracing is enabled.
-   *
-   * @param operation the name of operation the is being performed
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @param key3 third meta data key
-   * @param value3 third meta data value
-   * @return the trace timer
-   */
-  public static TraceTimer newTimer(
-      String operation,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2,
-      String key3,
-      @Nullable Object value3) {
-    return new TraceTimer(
-        requireNonNull(operation, "operation is required"),
-        requireNonNull(key1, "key1 is required"),
-        value1,
-        requireNonNull(key2, "key2 is required"),
-        value2,
-        requireNonNull(key3, "key3 is required"),
-        value3);
-  }
-
-  /**
-   * Opens a new timer that logs the time for an operation if request tracing is enabled.
-   *
-   * @param operation the name of operation the is being performed
-   * @param key1 first meta data key
-   * @param value1 first meta data value
-   * @param key2 second meta data key
-   * @param value2 second meta data value
-   * @param key3 third meta data key
-   * @param value3 third meta data value
-   * @param key4 fourth meta data key
-   * @param value4 fourth meta data value
-   * @return the trace timer
-   */
-  public static TraceTimer newTimer(
-      String operation,
-      String key1,
-      @Nullable Object value1,
-      String key2,
-      @Nullable Object value2,
-      String key3,
-      @Nullable Object value3,
-      String key4,
-      @Nullable Object value4) {
-    return new TraceTimer(
-        requireNonNull(operation, "operation is required"),
-        requireNonNull(key1, "key1 is required"),
-        value1,
-        requireNonNull(key2, "key2 is required"),
-        value2,
-        requireNonNull(key3, "key3 is required"),
-        value3,
-        requireNonNull(key4, "key4 is required"),
-        value4);
+        requireNonNull(metadata, "metadata is required"));
   }
 
   public static class TraceTimer implements AutoCloseable {
@@ -282,76 +190,13 @@
           });
     }
 
-    private TraceTimer(String operation, String key, @Nullable Object value) {
+    private TraceTimer(String operation, Metadata metadata) {
       this(
           elapsedMs -> {
             LoggingContext.getInstance()
                 .addPerformanceLogRecord(
-                    () -> PerformanceLogRecord.create(operation, elapsedMs, key, value));
-            logger.atFine().log("%s (%s=%s) (%d ms)", operation, key, value, elapsedMs);
-          });
-    }
-
-    private TraceTimer(
-        String operation,
-        String key1,
-        @Nullable Object value1,
-        String key2,
-        @Nullable Object value2) {
-      this(
-          elapsedMs -> {
-            LoggingContext.getInstance()
-                .addPerformanceLogRecord(
-                    () ->
-                        PerformanceLogRecord.create(
-                            operation, elapsedMs, key1, value1, key2, value2));
-            logger.atFine().log(
-                "%s (%s=%s, %s=%s) (%d ms)", operation, key1, value1, key2, value2, elapsedMs);
-          });
-    }
-
-    private TraceTimer(
-        String operation,
-        String key1,
-        @Nullable Object value1,
-        String key2,
-        @Nullable Object value2,
-        String key3,
-        @Nullable Object value3) {
-      this(
-          elapsedMs -> {
-            LoggingContext.getInstance()
-                .addPerformanceLogRecord(
-                    () ->
-                        PerformanceLogRecord.create(
-                            operation, elapsedMs, key1, value1, key2, value2, key3, value3));
-            logger.atFine().log(
-                "%s (%s=%s, %s=%s, %s=%s) (%d ms)",
-                operation, key1, value1, key2, value2, key3, value3, elapsedMs);
-          });
-    }
-
-    private TraceTimer(
-        String operation,
-        String key1,
-        @Nullable Object value1,
-        String key2,
-        @Nullable Object value2,
-        String key3,
-        @Nullable Object value3,
-        String key4,
-        @Nullable Object value4) {
-      this(
-          elapsedMs -> {
-            LoggingContext.getInstance()
-                .addPerformanceLogRecord(
-                    () ->
-                        PerformanceLogRecord.create(
-                            operation, elapsedMs, key1, value1, key2, value2, key3, value3, key4,
-                            value4));
-            logger.atFine().log(
-                "%s (%s=%s, %s=%s, %s=%s, %s=%s) (%d ms)",
-                operation, key1, value1, key2, value2, key3, value3, key4, value4, elapsedMs);
+                    () -> PerformanceLogRecord.create(operation, elapsedMs, metadata));
+            logger.atFine().log("%s (%s) (%d ms)", operation, metadata, elapsedMs);
           });
     }
 
@@ -398,6 +243,15 @@
     return this;
   }
 
+  public boolean isTracing() {
+    return LoggingContext.getInstance().isLoggingForced();
+  }
+
+  public Optional<String> getTraceId() {
+    return LoggingContext.getInstance().getTagsAsMap().get(RequestId.Type.TRACE_ID.name()).stream()
+        .findFirst();
+  }
+
   @Override
   public void close() {
     for (Table.Cell<String, String, Boolean> cell : tags.cellSet()) {
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index f8c29b3..4f6a341 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -17,7 +17,6 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.FilenameComparator;
 import com.google.gerrit.exceptions.EmailException;
@@ -55,7 +54,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
@@ -128,14 +126,6 @@
 
   public void setComments(List<Comment> comments) {
     inlineComments = comments;
-
-    Set<String> paths = new HashSet<>();
-    for (Comment c : comments) {
-      if (!Patch.isMagic(c.key.filename)) {
-        paths.add(c.key.filename);
-      }
-    }
-    changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths));
   }
 
   public void setPatchSetComment(String comment) {
@@ -534,7 +524,8 @@
     } catch (IndexOutOfBoundsException err) {
       // Default to the empty string if the given line number does not appear
       // in the file.
-      logger.atFine().withCause(err).log("Failed to get line number of file on side %d", side);
+      logger.atFine().withCause(err).log(
+          "Failed to get line number %d of file on side %d", lineNbr, side);
       return "";
     } catch (NoSuchEntityException err) {
       // Default to the empty string if the side cannot be found.
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 4313473..e56a38f 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -34,7 +34,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
-import com.google.template.soy.data.SanitizedContent;
+import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -54,6 +54,7 @@
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
+  private static final String SOY_TEMPLATE_NAMESPACE = "com.google.gerrit.server.mail.template.";
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   protected String messageClass;
@@ -536,21 +537,19 @@
     return args.instanceNameProvider.get();
   }
 
-  private String soyTemplate(String name, SanitizedContent.ContentKind kind) {
-    return args.soySauce
-        .renderTemplate("com.google.gerrit.server.mail.template." + name)
-        .setExpectedContentKind(kind)
-        .setData(soyContext)
-        .render()
-        .get();
-  }
-
+  /** Renders a soy template of kind="text". */
   protected String textTemplate(String name) {
-    return soyTemplate(name, SanitizedContent.ContentKind.TEXT);
+    return configureRenderer(name).renderText().get();
   }
 
+  /** Renders a soy template of kind="html". */
   protected String soyHtmlTemplate(String name) {
-    return soyTemplate(name, SanitizedContent.ContentKind.HTML);
+    return configureRenderer(name).renderHtml().get().toString();
+  }
+
+  /** Configures a soy renderer for the given template name and rendering data map. */
+  private SoySauce.Renderer configureRenderer(String templateName) {
+    return args.soySauce.renderTemplate(SOY_TEMPLATE_NAMESPACE + templateName).setData(soyContext);
   }
 
   protected void removeUser(Account user) {
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index fd006c2..eb3b831 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -97,9 +97,9 @@
       for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
         if (nc.isNotify(type)) {
           try {
-            add(matching, nc);
+            add(matching, state.getNameKey(), nc);
           } catch (QueryParseException e) {
-            logger.atWarning().log(
+            logger.atInfo().log(
                 "Project %s has invalid notify %s filter \"%s\": %s",
                 state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
           }
@@ -146,17 +146,27 @@
     }
   }
 
-  private void add(Watchers matching, NotifyConfig nc) throws QueryParseException {
-    for (GroupReference ref : nc.getGroups()) {
-      CurrentUser user = new SingleGroupUser(ref.getUUID());
+  private void add(Watchers matching, Project.NameKey projectName, NotifyConfig nc)
+      throws QueryParseException {
+    logger.atFine().log("Checking watchers for notify config %s from project %s", nc, projectName);
+    for (GroupReference groupRef : nc.getGroups()) {
+      CurrentUser user = new SingleGroupUser(groupRef.getUUID());
       if (filterMatch(user, nc.getFilter())) {
-        deliverToMembers(matching.list(nc.getHeader()), ref.getUUID());
+        deliverToMembers(matching.list(nc.getHeader()), groupRef.getUUID());
+        logger.atFine().log("Added watchers for group %s", groupRef);
+      } else {
+        logger.atFine().log("The filter did not match for group %s; skip notification", groupRef);
       }
     }
 
     if (!nc.getAddresses().isEmpty()) {
       if (filterMatch(null, nc.getFilter())) {
         matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
+        logger.atFine().log("Added watchers for these addresses: %s", nc.getAddresses());
+      } else {
+        logger.atFine().log(
+            "The filter did not match; skip notification for these addresses: %s",
+            nc.getAddresses());
       }
     }
   }
@@ -172,19 +182,24 @@
       AccountGroup.UUID uuid = q.remove(q.size() - 1);
       GroupDescription.Basic group = args.groupBackend.get(uuid);
       if (group == null) {
+        logger.atFine().log("group %s not found, skip notification", uuid);
         continue;
       }
       if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
         // If the group has an email address, do not expand membership.
         matching.emails.add(new Address(group.getEmailAddress()));
+        logger.atFine().log(
+            "notify group email address %s; skip expanding to members", group.getEmailAddress());
         continue;
       }
 
       if (!(group instanceof GroupDescription.Internal)) {
         // Non-internal groups cannot be expanded by the server.
+        logger.atFine().log("group %s is not an internal group, skip notification", uuid);
         continue;
       }
 
+      logger.atFine().log("adding the members of group %s as watchers", uuid);
       GroupDescription.Internal ig = (GroupDescription.Internal) group;
       matching.accounts.addAll(ig.getMembers());
       for (AccountGroup.UUID m : ig.getSubgroups()) {
@@ -201,8 +216,9 @@
       ProjectWatchKey key,
       Set<NotifyType> watchedTypes,
       NotifyType type) {
-    IdentifiedUser user = args.identifiedUserFactory.create(accountId);
+    logger.atFine().log("Checking project watch %s of account %s", key, accountId);
 
+    IdentifiedUser user = args.identifiedUserFactory.create(accountId);
     try {
       if (filterMatch(user, key.filter())) {
         // If we are set to notify on this type, add the user.
@@ -210,10 +226,14 @@
         if (watchedTypes.contains(type)) {
           matching.bcc.accounts.add(accountId);
         }
+        logger.atFine().log("Added account %s as watcher", accountId);
         return true;
       }
+      logger.atFine().log("The filter did not match for account %s; skip notification", accountId);
     } catch (QueryParseException e) {
       // Ignore broken filter expressions.
+      logger.atInfo().log(
+          "Account %s has invalid filter in project watch %s: %s", accountId, key, e.getMessage());
     }
     return false;
   }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbMetrics.java b/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
index 7df4c3d..18ffd17 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -45,7 +46,8 @@
 
   @Inject
   NoteDbMetrics(MetricMaker metrics) {
-    Field<NoteDbTable> tableField = Field.ofEnum(NoteDbTable.class, "table").build();
+    Field<NoteDbTable> tableField =
+        Field.ofEnum(NoteDbTable.class, "table", Metadata.Builder::noteDbTable).build();
 
     updateLatency =
         metrics.newTimer(
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
index c989af4..73cc600 100644
--- a/java/com/google/gerrit/server/notedb/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.eclipse.jgit.lib.Config;
@@ -95,8 +96,9 @@
             new Description("Latency of requesting IDs from repo sequences")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            Field.ofEnum(SequenceType.class, "sequence").build(),
-            Field.ofBoolean("multiple").build());
+            Field.ofEnum(SequenceType.class, "sequence", Metadata.Builder::noteDbSequenceType)
+                .build(),
+            Field.ofBoolean("multiple", Metadata.Builder::multiple).build());
   }
 
   public int nextAccountId() {
diff --git a/java/com/google/gerrit/server/patch/PatchFile.java b/java/com/google/gerrit/server/patch/PatchFile.java
index ec02485..80f9ba0 100644
--- a/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/java/com/google/gerrit/server/patch/PatchFile.java
@@ -96,6 +96,14 @@
     }
   }
 
+  private String getOldName() {
+    String name = entry.getOldName();
+    if (name != null) {
+      return name;
+    }
+    return entry.getNewName();
+  }
+
   /**
    * Extract a line from the file, as a string.
    *
@@ -109,7 +117,7 @@
     switch (file) {
       case 0:
         if (a == null) {
-          a = load(aTree, entry.getOldName());
+          a = load(aTree, getOldName());
         }
         return a.getString(line - 1);
 
@@ -124,33 +132,6 @@
     }
   }
 
-  /**
-   * Return number of lines in file.
-   *
-   * @param file the file index to extract.
-   * @return number of lines in file.
-   * @throws IOException the patch or complete file content cannot be read.
-   * @throws NoSuchEntityException the file is not exist.
-   */
-  public int getLineCount(int file) throws IOException, NoSuchEntityException {
-    switch (file) {
-      case 0:
-        if (a == null) {
-          a = load(aTree, entry.getOldName());
-        }
-        return a.size();
-
-      case 1:
-        if (b == null) {
-          b = load(bTree, entry.getNewName());
-        }
-        return b.size();
-
-      default:
-        throw new NoSuchEntityException();
-    }
-  }
-
   private Text load(ObjectId tree, String path)
       throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
           IOException {
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 5c17be8..858edf2 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -208,7 +208,7 @@
     logger.atFinest().log("Filter refs (refs = %s)", refs);
 
     if (projectState.isAllUsers()) {
-      refs = addUsersSelfSymref(refs);
+      refs = addUsersSelfSymref(repo, refs);
     }
 
     // TODO(hiesel): Remove when optimization is done.
@@ -397,19 +397,24 @@
     return refs;
   }
 
-  private Map<String, Ref> addUsersSelfSymref(Map<String, Ref> refs) {
+  private Map<String, Ref> addUsersSelfSymref(Repository repo, Map<String, Ref> refs)
+      throws PermissionBackendException {
     if (user.isIdentifiedUser()) {
       String refName = RefNames.refsUsers(user.getAccountId());
-      Ref r = refs.get(refName);
-      if (r == null) {
-        logger.atWarning().log("User ref %s not found", refName);
-        return refs;
-      }
+      try {
+        Ref r = repo.exactRef(refName);
+        if (r == null) {
+          logger.atWarning().log("User ref %s not found", refName);
+          return refs;
+        }
 
-      SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r);
-      refs = new HashMap<>(refs);
-      refs.put(s.getName(), s);
-      logger.atFinest().log("Added %s as alias for user ref %s", REFS_USERS_SELF, refName);
+        SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r);
+        refs = new HashMap<>(refs);
+        refs.put(s.getName(), s);
+        logger.atFinest().log("Added %s as alias for user ref %s", REFS_USERS_SELF, refName);
+      } catch (IOException e) {
+        throw new PermissionBackendException(e);
+      }
     }
     return refs;
   }
diff --git a/java/com/google/gerrit/server/plugincontext/PluginContext.java b/java/com/google/gerrit/server/plugincontext/PluginContext.java
index bb51a03..266eb92 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginContext.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -118,9 +119,12 @@
 
     @Inject
     PluginMetrics(MetricMaker metricMaker) {
-      Field<String> pluginNameField = Field.ofString("plugin_name").build();
-      Field<String> classNameField = Field.ofString("class_name").build();
-      Field<String> exportNameField = Field.ofString("export_name").build();
+      Field<String> pluginNameField =
+          Field.ofString("plugin_name", Metadata.Builder::pluginName).build();
+      Field<String> classNameField =
+          Field.ofString("class_name", Metadata.Builder::className).build();
+      Field<String> exportValueField =
+          Field.ofString("export_value", Metadata.Builder::exportValue).build();
 
       this.latency =
           metricMaker.newTimer(
@@ -130,14 +134,14 @@
                   .setUnit(Units.MILLISECONDS),
               pluginNameField,
               classNameField,
-              exportNameField);
+              exportValueField);
       this.errorCount =
           metricMaker.newCounter(
               "plugin/error_count",
               new Description("Number of plugin errors").setCumulative().setUnit("errors"),
               pluginNameField,
               classNameField,
-              exportNameField);
+              exportValueField);
     }
 
     Timer3.Context<String, String, String> startLatency(Extension<?> extension) {
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index 79eccbb..1328f6b 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -39,9 +39,6 @@
               BooleanProjectConfig.USE_CONTENT_MERGE,
               new Mapper(i -> i.useContentMerge, (i, v) -> i.useContentMerge = v))
           .put(
-              BooleanProjectConfig.REQUIRE_CHANGE_ID,
-              new Mapper(i -> i.requireChangeId, (i, v) -> i.requireChangeId = v))
-          .put(
               BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
               new Mapper(
                   i -> i.createNewChangeForAllNotInTarget,
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 7405df1..9ae3b2c 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -33,7 +33,6 @@
   public List<String> branch;
   public InheritableBoolean contentMerge;
   public InheritableBoolean newChangeForAllNotInTarget;
-  public InheritableBoolean changeIdRequired;
   public InheritableBoolean rejectEmptyCommit;
   public InheritableBoolean enableSignedPush;
   public InheritableBoolean requireSignedPush;
@@ -44,7 +43,6 @@
     contributorAgreements = InheritableBoolean.INHERIT;
     signedOffBy = InheritableBoolean.INHERIT;
     contentMerge = InheritableBoolean.INHERIT;
-    changeIdRequired = InheritableBoolean.INHERIT;
     newChangeForAllNotInTarget = InheritableBoolean.INHERIT;
     enableSignedPush = InheritableBoolean.INHERIT;
     requireSignedPush = InheritableBoolean.INHERIT;
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 3fc634d..0bfd36d 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.Inject;
@@ -294,7 +295,8 @@
     @Override
     public ProjectState load(String projectName) throws Exception {
       try (TraceTimer timer =
-          TraceContext.newTimer("Loading project", "projectName", projectName)) {
+          TraceContext.newTimer(
+              "Loading project", Metadata.builder().projectName(projectName).build())) {
         long now = clock.read();
         Project.NameKey key = Project.nameKey(projectName);
         try (Repository git = mgr.openRepository(key)) {
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index b50b046..d468b44 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -152,7 +152,6 @@
       newProject.setBooleanConfig(
           BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
           args.newChangeForAllNotInTarget);
-      newProject.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, args.changeIdRequired);
       newProject.setBooleanConfig(BooleanProjectConfig.REJECT_EMPTY_COMMIT, args.rejectEmptyCommit);
       newProject.setMaxObjectSizeLimit(args.maxObjectSizeLimit);
       newProject.setBooleanConfig(BooleanProjectConfig.ENABLE_SIGNED_PUSH, args.enableSignedPush);
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 782ae84..a43047f 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -135,7 +136,7 @@
             new Description("Latency for access computations in ProjectState")
                 .setCumulative()
                 .setUnit(Units.NANOSECONDS),
-            Field.ofString("method").build());
+            Field.ofString("method", Metadata.Builder::methodName).build());
 
     if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
       localOwners = Collections.emptySet();
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 85d91e7..1b1869c 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.OnlineReindexMode;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.PrologRule;
@@ -102,7 +103,7 @@
       return ruleError("Error looking up change " + cd.getId(), e);
     }
 
-    if (!opts.allowClosed() && change.isClosed()) {
+    if ((!opts.allowClosed() || OnlineReindexMode.isActive()) && change.isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 0352b83..59cbf32 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -19,6 +19,7 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -364,6 +365,7 @@
     return allUsersName;
   }
 
+  @VisibleForTesting
   public void setCurrentFilePaths(List<String> filePaths) {
     PatchSet ps = currentPatchSet();
     if (ps != null) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 3c43cb8..db91bb2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -24,8 +24,6 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Enums;
 import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.GroupDescription;
@@ -1332,7 +1330,7 @@
     int maxTerms = args.indexConfig.maxTerms();
     if (allMembers.size() > maxTerms) {
       // limit the number of query terms otherwise Gerrit will barf
-      accounts = ImmutableSet.copyOf(Iterables.limit(allMembers, maxTerms));
+      accounts = allMembers.stream().limit(maxTerms).collect(toSet());
     } else {
       accounts = allMembers;
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 77f1668..9e2f1cf 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.account.AccountLimits;
@@ -40,7 +39,6 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -79,7 +77,7 @@
   }
 
   @Override
-  public Object apply(AccountResource resource)
+  public Map<String, Object> apply(AccountResource resource)
       throws RestApiException, PermissionBackendException {
     permissionBackend.checkUsesDefaultCapabilities();
     PermissionBackend.WithUser perm = permissionBackend.currentUser();
@@ -97,9 +95,7 @@
     addRanges(have, limits);
     addPriority(have, limits);
 
-    return OutputFormat.JSON
-        .newGson()
-        .toJsonTree(have, new TypeToken<Map<String, Object>>() {}.getType());
+    return have;
   }
 
   private Set<GlobalOrPluginPermission> permissionsToTest() {
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index bc1ffc8..76a641d 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -20,9 +20,10 @@
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.api.accounts.UsernameInput;
 import com.google.gerrit.extensions.client.AccountFieldName;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Account;
@@ -71,9 +72,7 @@
 
   @Override
   public String apply(AccountResource rsrc, UsernameInput input)
-      throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
-          ResourceConflictException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
     }
@@ -82,17 +81,13 @@
       throw new MethodNotAllowedException("realm does not allow editing username");
     }
 
-    if (input == null) {
-      input = new UsernameInput();
-    }
-
     Account.Id accountId = rsrc.getUser().getAccountId();
     if (!externalIds.byAccount(accountId, SCHEME_USERNAME).isEmpty()) {
       throw new MethodNotAllowedException("Username cannot be changed.");
     }
 
-    if (Strings.isNullOrEmpty(input.username)) {
-      return input.username;
+    if (input == null || Strings.isNullOrEmpty(input.username)) {
+      throw new BadRequestException("input required");
     }
 
     if (!ExternalId.isValidUsername(input.username)) {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index b8ea310..86d8ed6 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -65,7 +65,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.extensions.validators.CommentForValidation;
-import com.google.gerrit.extensions.validators.CommentForValidation.CommentType;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.json.OutputFormat;
@@ -992,7 +991,7 @@
                       CommentForValidation.create(
                           comment.lineNbr > 0
                               ? CommentForValidation.CommentType.INLINE_COMMENT
-                              : CommentType.FILE_COMMENT,
+                              : CommentForValidation.CommentType.FILE_COMMENT,
                           comment.message))
               .collect(toImmutableList());
       ImmutableList<CommentValidationFailure> draftValidationFailures =
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index db02418..5c36e60 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
@@ -111,10 +110,7 @@
     String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
 
     ensureCanEditCommitMessage(resource.getNotes());
-    ensureChangeIdIsCorrect(
-        projectCache.checkedGet(resource.getProject()).is(BooleanProjectConfig.REQUIRE_CHANGE_ID),
-        resource.getChange().getKey().get(),
-        sanitizedCommitMessage);
+    ensureChangeIdIsCorrect(resource.getChange().getKey().get(), sanitizedCommitMessage);
 
     try (Repository repository = repositoryManager.openRepository(resource.getProject());
         RevWalk revWalk = new RevWalk(repository);
@@ -193,8 +189,7 @@
     }
   }
 
-  private static void ensureChangeIdIsCorrect(
-      boolean requireChangeId, String currentChangeId, String newCommitMessage)
+  private static void ensureChangeIdIsCorrect(String currentChangeId, String newCommitMessage)
       throws ResourceConflictException, BadRequestException {
     RevCommit revCommit =
         RevCommit.parse(
@@ -204,7 +199,7 @@
     CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
 
     List<String> changeIdFooters = revCommit.getFooterLines(FooterConstants.CHANGE_ID);
-    if (requireChangeId && changeIdFooters.isEmpty()) {
+    if (changeIdFooters.isEmpty()) {
       throw new ResourceConflictException("missing Change-Id footer");
     }
     if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) {
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 248dc6e..d8a6fe8 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -20,8 +20,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -357,10 +355,9 @@
 
   private List<GroupReference> suggestAccountGroups(
       SuggestReviewers suggestReviewers, ProjectState projectState) {
-    return Lists.newArrayList(
-        Iterables.limit(
-            groupBackend.suggest(suggestReviewers.getQuery(), projectState),
-            suggestReviewers.getLimit()));
+    return groupBackend.suggest(suggestReviewers.getQuery(), projectState).stream()
+        .limit(suggestReviewers.getLimit())
+        .collect(toList());
   }
 
   private static class GroupAsReviewer {
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 43bfa81..2493cd9 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -293,7 +293,6 @@
     info.allProjects = allProjectsName.get();
     info.allUsers = allUsersName.get();
     info.reportBugUrl = config.getString("gerrit", null, "reportBugUrl");
-    info.reportBugText = config.getString("gerrit", null, "reportBugText");
     info.docUrl = getDocUrl();
     info.docSearch = docSearcher.isAvailable();
     info.editGpgKeys =
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 9f2a7b7..38b525b 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.restapi.group;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
 import com.google.gerrit.common.data.GroupDescription;
@@ -320,10 +320,9 @@
           "You should only have no more than one --project and -n with --suggest");
     }
     List<GroupReference> groupRefs =
-        Lists.newArrayList(
-            Iterables.limit(
-                groupBackend.suggest(suggest, projects.stream().findFirst().orElse(null)),
-                limit <= 0 ? 10 : Math.min(limit, 10)));
+        groupBackend.suggest(suggest, projects.stream().findFirst().orElse(null)).stream()
+            .limit(limit <= 0 ? 10 : Math.min(limit, 10))
+            .collect(toList());
 
     List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
     for (GroupReference ref : groupRefs) {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index 6844cac..6428435 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -147,8 +147,6 @@
     args.newChangeForAllNotInTarget =
         MoreObjects.firstNonNull(
             input.createNewChangeForAllNotInTarget, InheritableBoolean.INHERIT);
-    args.changeIdRequired =
-        MoreObjects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
     args.rejectEmptyCommit =
         MoreObjects.firstNonNull(input.rejectEmptyCommit, InheritableBoolean.INHERIT);
     args.enableSignedPush =
diff --git a/java/com/google/gerrit/server/restapi/project/IndexChanges.java b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
index b84f86c..b6b3d6b 100644
--- a/java/com/google/gerrit/server/restapi/project/IndexChanges.java
+++ b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
@@ -20,7 +20,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Project;
@@ -38,7 +38,7 @@
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
-public class IndexChanges implements RestModifyView<ProjectResource, ProjectInput> {
+public class IndexChanges implements RestModifyView<ProjectResource, Input> {
 
   private final Provider<AllChangesIndexer> allChangesIndexerProvider;
   private final ChangeIndexer indexer;
@@ -55,7 +55,7 @@
   }
 
   @Override
-  public Response.Accepted apply(ProjectResource resource, ProjectInput input) {
+  public Response.Accepted apply(ProjectResource resource, Input input) {
     Project.NameKey project = resource.getNameKey();
     Task mpt =
         new MultiProgressMonitor(ByteStreams.nullOutputStream(), "Reindexing project")
diff --git a/java/com/google/gerrit/server/schema/AllProjectsInput.java b/java/com/google/gerrit/server/schema/AllProjectsInput.java
index 7231b18..f72bf4d 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsInput.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsInput.java
@@ -33,8 +33,6 @@
   public static final ImmutableMap<BooleanProjectConfig, InheritableBoolean>
       DEFAULT_BOOLEAN_PROJECT_CONFIGS =
           ImmutableMap.of(
-              BooleanProjectConfig.REQUIRE_CHANGE_ID,
-              InheritableBoolean.TRUE,
               BooleanProjectConfig.USE_CONTENT_MERGE,
               InheritableBoolean.TRUE,
               BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS,
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index a8020b1..aa552ed 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -15,6 +15,7 @@
         "//java/com/google/gerrit/metrics",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index d7dbf58..cb91dea 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -34,6 +34,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -212,7 +215,15 @@
 
   @Override
   public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Mark file as reviewed",
+                Metadata.builder()
+                    .patchSetId(psId.get())
+                    .accountId(accountId.get())
+                    .filePath(path)
+                    .build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "INSERT INTO account_patch_reviews "
@@ -239,7 +250,15 @@
       return;
     }
 
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Mark files as reviewed",
+                Metadata.builder()
+                    .patchSetId(psId.get())
+                    .accountId(accountId.get())
+                    .resourceCount(paths.size())
+                    .build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "INSERT INTO account_patch_reviews "
@@ -264,7 +283,15 @@
 
   @Override
   public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Clear reviewed flag",
+                Metadata.builder()
+                    .patchSetId(psId.get())
+                    .accountId(accountId.get())
+                    .filePath(path)
+                    .build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "DELETE FROM account_patch_reviews "
@@ -282,7 +309,11 @@
 
   @Override
   public void clearReviewed(PatchSet.Id psId) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Clear all reviewed flags of patch set",
+                Metadata.builder().patchSetId(psId.get()).build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "DELETE FROM account_patch_reviews "
@@ -297,7 +328,11 @@
 
   @Override
   public void clearReviewed(Change.Id changeId) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Clear all reviewed flags of change",
+                Metadata.builder().changeId(changeId.get()).build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement("DELETE FROM account_patch_reviews WHERE change_id = ?")) {
       stmt.setInt(1, changeId.get());
@@ -309,7 +344,11 @@
 
   @Override
   public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
-    try (Connection con = ds.getConnection();
+    try (TraceTimer ignored =
+            TraceContext.newTimer(
+                "Find reviewed flags",
+                Metadata.builder().patchSetId(psId.get()).accountId(accountId.get()).build());
+        Connection con = ds.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "SELECT patch_set_id, file_name FROM account_patch_reviews APR1 "
diff --git a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
index be56782..6c49b22 100644
--- a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
+++ b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
@@ -40,7 +40,6 @@
           "[receive]",
           "  requireContributorAgreement = false",
           "  requireSignedOffBy = false",
-          "  requireChangeId = true",
           "  enableSignedPush = false");
   private static final ImmutableList<String> DEFAULT_ALL_PROJECTS_SUBMIT_SECTION =
       ImmutableList.of("[submit]", "  mergeContent = true");
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 06c52c7..6c3d48b 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -511,6 +511,8 @@
                     retryHelper
                         .getDefaultTimeout(ActionType.CHANGE_UPDATE)
                         .multipliedBy(cs.projects().size()))
+                .caller(getClass())
+                .retryWithTrace(t -> !(t instanceof RestApiException))
                 .build());
 
         if (projects > 1) {
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 7a4f462..695feba 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -39,11 +39,15 @@
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -93,12 +97,20 @@
     @Nullable
     abstract Duration timeout();
 
+    abstract Optional<Class<?>> caller();
+
+    abstract Optional<Predicate<Throwable>> retryWithTrace();
+
     @AutoValue.Builder
     public abstract static class Builder {
       public abstract Builder listener(RetryListener listener);
 
       public abstract Builder timeout(Duration timeout);
 
+      public abstract Builder caller(Class<?> caller);
+
+      public abstract Builder retryWithTrace(Predicate<Throwable> exceptionPredicate);
+
       public abstract Options build();
     }
   }
@@ -111,7 +123,8 @@
 
     @Inject
     Metrics(MetricMaker metricMaker) {
-      Field<ActionType> actionTypeField = Field.ofEnum(ActionType.class, "action_type").build();
+      Field<ActionType> actionTypeField =
+          Field.ofEnum(ActionType.class, "action_type", Metadata.Builder::actionType).build();
       attemptCounts =
           metricMaker.newCounter(
               "action/retry_attempt_count",
@@ -145,6 +158,7 @@
   private final Map<ActionType, Duration> defaultTimeouts;
   private final WaitStrategy waitStrategy;
   @Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
+  private final boolean retryWithTraceOnFailure;
 
   @Inject
   RetryHelper(@GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory) {
@@ -184,6 +198,7 @@
                 MILLISECONDS),
             WaitStrategies.randomWait(50, MILLISECONDS));
     this.overwriteDefaultRetryerStrategySetup = overwriteDefaultRetryerStrategySetup;
+    this.retryWithTraceOnFailure = cfg.getBoolean("retry", "retryWithTraceOnFailure", false);
   }
 
   public Duration getDefaultTimeout(ActionType actionType) {
@@ -254,8 +269,35 @@
       Predicate<Throwable> exceptionPredicate)
       throws Throwable {
     MetricListener listener = new MetricListener();
-    try {
-      RetryerBuilder<T> retryerBuilder = createRetryerBuilder(actionType, opts, exceptionPredicate);
+    try (TraceContext traceContext = TraceContext.open()) {
+      RetryerBuilder<T> retryerBuilder =
+          createRetryerBuilder(
+              actionType,
+              opts,
+              t -> {
+                // exceptionPredicate checks for temporary errors for which the operation should be
+                // retried (e.g. LockFailure). The retry has good chances to succeed.
+                if (exceptionPredicate.test(t)) {
+                  return true;
+                }
+
+                // A non-recoverable failure occurred. Check if we should retry to capture a trace
+                // of the failure. If a trace was already done there is no need to retry.
+                if (retryWithTraceOnFailure
+                    && opts.retryWithTrace().isPresent()
+                    && opts.retryWithTrace().get().test(t)
+                    && !traceContext.isTracing()) {
+                  traceContext
+                      .addTag(RequestId.Type.TRACE_ID, "retry-on-failure-" + new RequestId())
+                      .forceLogging();
+                  logger.atFine().withCause(t).log(
+                      "%s failed, retry with tracing eanbled",
+                      opts.caller().map(Class::getSimpleName).orElse("N/A"));
+                  return true;
+                }
+
+                return false;
+              });
       retryerBuilder.withRetryListener(listener);
       return executeWithTimeoutCount(actionType, action, retryerBuilder.build());
     } finally {
diff --git a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
index 7620386..9204565 100644
--- a/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
+++ b/java/com/google/gerrit/server/update/RetryingRestCollectionModifyView.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
 import com.google.gerrit.extensions.restapi.RestResource;
 
@@ -32,7 +33,13 @@
   @Override
   public final O apply(P parentResource, I input)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    return retryHelper.execute((updateFactory) -> applyImpl(updateFactory, parentResource, input));
+    RetryHelper.Options retryOptions =
+        RetryHelper.options()
+            .caller(getClass())
+            .retryWithTrace(t -> !(t instanceof RestApiException))
+            .build();
+    return retryHelper.execute(
+        (updateFactory) -> applyImpl(updateFactory, parentResource, input), retryOptions);
   }
 
   protected abstract O applyImpl(BatchUpdate.Factory updateFactory, P parentResource, I input)
diff --git a/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
index e2f4a02..b471d70 100644
--- a/java/com/google/gerrit/server/update/RetryingRestModifyView.java
+++ b/java/com/google/gerrit/server/update/RetryingRestModifyView.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.update;
 
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestResource;
 
@@ -27,7 +28,13 @@
 
   @Override
   public final O apply(R resource, I input) throws Exception {
-    return retryHelper.execute((updateFactory) -> applyImpl(updateFactory, resource, input));
+    RetryHelper.Options retryOptions =
+        RetryHelper.options()
+            .caller(getClass())
+            .retryWithTrace(t -> !(t instanceof RestApiException))
+            .build();
+    return retryHelper.execute(
+        (updateFactory) -> applyImpl(updateFactory, resource, input), retryOptions);
   }
 
   protected abstract O applyImpl(BatchUpdate.Factory updateFactory, R resource, I input)
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 2bbdc49..2590188 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -16,10 +16,13 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.PerformanceLogContext;
 import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -29,6 +32,7 @@
 
 public abstract class SshCommand extends BaseCommand {
   @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
+  @Inject private PluginSetContext<RequestListener> requestListeners;
   @Inject @GerritServerConfig private Config config;
 
   @Option(name = "--trace", usage = "enable request tracing")
@@ -50,6 +54,9 @@
           try (TraceContext traceContext = enableTracing();
               PerformanceLogContext performanceLogContext =
                   new PerformanceLogContext(config, performanceLoggers)) {
+            RequestInfo requestInfo =
+                RequestInfo.builder(RequestInfo.RequestType.SSH, user, traceContext).build();
+            requestListeners.runEach(l -> l.onRequest(requestInfo));
             SshCommand.this.run();
           } finally {
             stdout.flush();
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 94e7f1b..773c25b 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.ssh.SshKeyCache;
@@ -107,7 +108,8 @@
     public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
       try (TraceTimer timer =
           TraceContext.newTimer(
-              "Loading SSH keys for account with username", "username", username)) {
+              "Loading SSH keys for account with username",
+              Metadata.builder().username(username).build())) {
         Optional<ExternalId> user =
             externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
         if (!user.isPresent()) {
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index df86d63..56e72e6 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -91,9 +91,6 @@
   @Option(name = "--content-merge", usage = "allow automatic conflict resolving within files")
   private InheritableBoolean contentMerge = InheritableBoolean.INHERIT;
 
-  @Option(name = "--change-id", usage = "if change-id is required")
-  private InheritableBoolean requireChangeID = InheritableBoolean.INHERIT;
-
   @Option(name = "--reject-empty-commit", usage = "if empty commits should be rejected on submit")
   private InheritableBoolean rejectEmptyCommit = InheritableBoolean.INHERIT;
 
@@ -124,14 +121,6 @@
   }
 
   @Option(
-      name = "--require-change-id",
-      aliases = {"--id"},
-      usage = "if change-id is required")
-  void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
-    requireChangeID = InheritableBoolean.TRUE;
-  }
-
-  @Option(
       name = "--create-new-change-for-all-not-in-target",
       aliases = {"--ncfa"},
       usage = "if a new change will be created for every commit not in target branch")
@@ -186,7 +175,6 @@
         input.useContributorAgreements = contributorAgreements;
         input.useSignedOffBy = signedOffBy;
         input.useContentMerge = contentMerge;
-        input.requireChangeId = requireChangeID;
         input.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
         input.branches = branch;
         input.createEmptyCommit = createEmptyCommit;
diff --git a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 8c9fc9f..f145b9e 100644
--- a/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -56,9 +56,6 @@
   @Option(name = "--content-merge", usage = "allow automatic conflict resolving within files")
   private InheritableBoolean contentMerge;
 
-  @Option(name = "--change-id", usage = "if change-id is required")
-  private InheritableBoolean requireChangeID;
-
   @Option(
       name = "--use-contributor-agreements",
       aliases = {"--ca"},
@@ -104,22 +101,6 @@
   }
 
   @Option(
-      name = "--require-change-id",
-      aliases = {"--id"},
-      usage = "if change-id is required")
-  void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
-    requireChangeID = InheritableBoolean.TRUE;
-  }
-
-  @Option(
-      name = "--no-change-id",
-      aliases = {"--nid"},
-      usage = "if change-id is not required")
-  void setNoChangeId(@SuppressWarnings("unused") boolean on) {
-    requireChangeID = InheritableBoolean.FALSE;
-  }
-
-  @Option(
       name = "--project-state",
       aliases = {"--ps"},
       usage = "project's visibility state")
@@ -133,7 +114,6 @@
   @Override
   protected void run() throws Failure {
     ConfigInput configInput = new ConfigInput();
-    configInput.requireChangeId = requireChangeID;
     configInput.submitType = submitType;
     configInput.useContentMerge = contentMerge;
     configInput.useContributorAgreements = contributorAgreements;
diff --git a/java/com/google/gerrit/sshd/commands/Upload.java b/java/com/google/gerrit/sshd/commands/Upload.java
index 24a6975..a22cdaf 100644
--- a/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/java/com/google/gerrit/sshd/commands/Upload.java
@@ -17,15 +17,19 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.RequestInfo;
+import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.UploadPackInitializer;
 import com.google.gerrit.server.git.validators.UploadValidationException;
 import com.google.gerrit.server.git.validators.UploadValidators;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.sshd.AbstractGitCommand;
 import com.google.gerrit.sshd.SshSession;
 import com.google.inject.Inject;
@@ -43,6 +47,7 @@
   @Inject private DynamicSet<PreUploadHook> preUploadHooks;
   @Inject private DynamicSet<PostUploadHook> postUploadHooks;
   @Inject private DynamicSet<UploadPackInitializer> uploadPackInitializers;
+  @Inject private PluginSetContext<RequestListener> requestListeners;
   @Inject private UploadValidators.Factory uploadValidatorsFactory;
   @Inject private SshSession session;
   @Inject private PermissionBackend permissionBackend;
@@ -73,7 +78,13 @@
     for (UploadPackInitializer initializer : uploadPackInitializers) {
       initializer.init(projectState.getNameKey(), up);
     }
-    try {
+    try (TraceContext traceContext = TraceContext.open()) {
+      RequestInfo requestInfo =
+          RequestInfo.builder(RequestInfo.RequestType.GIT_UPLOAD, user, traceContext)
+              .project(projectState.getNameKey())
+              .build();
+      requestListeners.runEach(l -> l.onRequest(requestInfo));
+
       up.upload(in, out, err);
       session.setPeerAgent(up.getPeerUserAgent());
     } catch (UploadValidationException e) {
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index b99a32d..c86bcf4 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -20,8 +20,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.util.HashMap;
 import java.util.Map;
@@ -78,6 +76,6 @@
   }
 
   private static AccountState newState(Account account) {
-    return AccountState.forAccount(new AllUsersName(AllUsersNameProvider.DEFAULT), account);
+    return AccountState.forAccount(account);
   }
 }
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 1c16133..1c430fc 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -41,7 +41,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -313,23 +312,13 @@
     parser.parseArgument(tmp.toArray(new String[tmp.size()]));
   }
 
-  public void parseOptionMap(Map<String, String[]> parameters) throws CmdLineException {
-    ListMultimap<String, String> map = MultimapBuilder.hashKeys().arrayListValues().build();
-    for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
-      for (String val : ent.getValue()) {
-        map.put(ent.getKey(), val);
-      }
-    }
-    parseOptionMap(map);
-  }
-
   public void parseOptionMap(ListMultimap<String, String> params) throws CmdLineException {
     logger.atFinest().log("Command-line parameters: %s", params.keySet());
     List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size());
     for (String key : params.keySet()) {
       String name = makeOption(key);
 
-      if (isBoolean(name)) {
+      if (isBooleanOption(name)) {
         boolean on = false;
         for (String value : params.get(key)) {
           on = toBoolean(key, value);
@@ -347,10 +336,6 @@
     parser.parseArgument(tmp.toArray(new String[tmp.size()]));
   }
 
-  public boolean isBoolean(String name) {
-    return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
-  }
-
   public void parseWithPrefix(String prefix, Object bean) {
     parser.parseWithPrefix(prefix, bean);
   }
@@ -359,6 +344,10 @@
     parser.addOptionsWithMetRequirements();
   }
 
+  private boolean isBooleanOption(String name) {
+    return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
+  }
+
   private String makeOption(String name) {
     if (!name.startsWith("-")) {
       if (name.length() == 1) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 228f233..5b6acfe 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -131,6 +131,8 @@
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.account.StalenessChecker;
 import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -217,6 +219,7 @@
   @Inject private Sequences seq;
   @Inject private StalenessChecker stalenessChecker;
   @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
+  @Inject private PermissionBackend permissionBackend;
 
   @Inject protected Emails emails;
 
@@ -1371,6 +1374,19 @@
   }
 
   @Test
+  public void refsUsersSelfIsAdvertised() throws Exception {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+      assertThat(
+              permissionBackend
+                  .currentUser()
+                  .project(allUsers)
+                  .filter(ImmutableList.of(), allUsersRepo, RefFilterOptions.defaults())
+                  .keySet())
+          .containsExactly(RefNames.REFS_USERS_SELF);
+    }
+  }
+
+  @Test
   public void pushToUserBranch() throws Exception {
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
     fetch(allUsersRepo, RefNames.refsUsers(admin.id()) + ":userRef");
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 627fc09..1c1ef22 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3468,6 +3468,42 @@
   }
 
   @Test
+  public void notifyConfigForDirectoryTriggersEmail() throws Exception {
+    // Configure notifications on project level.
+    RevCommit oldHead = projectOperations.project(project).getHead("master");
+    GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
+    testRepo.reset("config");
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            "Configure Notifications",
+            "project.config",
+            "[notify \"my=notify-config\"]\n"
+                + "  email = foo@test.com\n"
+                + "  filter = dir:\\\"foo/bar/baz\\\"");
+    push.to(RefNames.REFS_CONFIG);
+    testRepo.reset(oldHead);
+
+    // Push a change that matches the filter.
+    sender.clear();
+    push =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "Test change", "foo/bar/baz/test.txt", "some content");
+    PushOneCommit.Result r = push.to("refs/for/master");
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(Address.parse("foo@test.com"));
+
+    // Comment on the change.
+    sender.clear();
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.message = "some message";
+    gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(sender.getMessages().get(0).rcpt()).containsExactly(Address.parse("foo@test.com"));
+  }
+
+  @Test
   public void checkLabelsForMergedChangeWithNonAuthorCodeReview() throws Exception {
     // Configure Non-Author-Code-Review
     RevCommit oldHead = projectOperations.project(project).getHead("master");
@@ -3777,24 +3813,6 @@
   }
 
   @Test
-  public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception {
-    ConfigInput configInput = new ConfigInput();
-    configInput.requireChangeId = InheritableBoolean.FALSE;
-    gApi.projects().name(project.get()).config(configInput);
-
-    PushOneCommit.Result r = createChange();
-    r.assertOkStatus();
-    assertThat(getCommitMessage(r.getChangeId()))
-        .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
-
-    String newMessage = "modified commit\n";
-    gApi.changes().id(r.getChangeId()).setMessage(newMessage);
-    RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
-    assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
-    assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
-  }
-
-  @Test
   public void changeCommitMessageWithNoChangeIdFails() throws Exception {
     PushOneCommit.Result r = createChange();
     assertThat(getCommitMessage(r.getChangeId()))
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 99935b5..20be0a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE;
 import static com.google.gerrit.extensions.client.ChangeKind.NO_CHANGE;
@@ -414,7 +413,7 @@
         noChange(changeId);
         return;
       default:
-        assert_().fail("unexpected change kind: " + changeKind);
+        assertWithMessage("unexpected change kind: " + changeKind).fail();
     }
   }
 
@@ -523,7 +522,7 @@
       case NO_CHANGE:
       case MERGE_FIRST_PARENT_UPDATE:
       default:
-        assert_().fail("unexpected change kind: " + changeKind);
+        assertWithMessage("unexpected change kind: " + changeKind).fail();
     }
 
     testRepo.reset(projectOperations.project(project).getHead("master"));
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index ab1dbd3..966c056 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.api.group;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
@@ -252,7 +252,8 @@
       }
     }
 
-    assert_().fail(String.format("could not find %s substring '%s' in %s", want, msg, problems));
+    assertWithMessage(String.format("could not find %s substring '%s' in %s", want, msg, problems))
+        .fail();
   }
 
   private void updateGroupFile(String refName, String fileName, String content) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index dd12382..a120eac 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.api.plugin;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
@@ -104,13 +103,7 @@
 
     // Disable mandatory
     mandatoryPluginsCollection.add("plugin_e");
-    api = gApi.plugins().name("plugin_e");
-    try {
-      api.disable();
-      assert_().fail("Disabling mandatory plugin should have failed");
-    } catch (MethodNotAllowedException e) {
-      // expected
-    }
+    assertThrows(MethodNotAllowedException.class, () -> gApi.plugins().name("plugin_e").disable());
     api = gApi.plugins().name("plugin_e");
     assertThat(api.get().disabled).isNull();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 3fcc595..b7d6627 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
@@ -247,15 +247,16 @@
       try {
         info = gApi.projects().name(tc.project).checkAccess(tc.input);
       } catch (RestApiException e) {
-        assert_().fail(String.format("check.access(%s, %s): exception %s", tc.project, in, e));
+        assertWithMessage(String.format("check.access(%s, %s): exception %s", tc.project, in, e))
+            .fail();
       }
 
       int want = tc.want;
       if (want != info.status) {
-        assert_()
-            .fail(
+        assertWithMessage(
                 String.format(
-                    "check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want));
+                    "check.access(%s, %s) = %d, want %d", tc.project, in, info.status, want))
+            .fail();
       }
 
       switch (want) {
@@ -271,7 +272,7 @@
           assertThat(info.message).isNull();
           break;
         default:
-          assert_().fail(String.format("unknown code %d", want));
+          assertWithMessage(String.format("unknown code %d", want)).fail();
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
index 96ba722..05295ae 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.extensions.api.projects.CheckProjectInput.AutoCloseableChangesCheckInput;
 import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo;
 import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -66,7 +65,7 @@
 
   @Test
   public void detectAutoCloseableChangeByCommit() throws Exception {
-    RevCommit commit = pushCommitWithoutChangeIdForReview();
+    RevCommit commit = pushCommitForReview();
     ChangeInfo change =
         Iterables.getOnlyElement(gApi.changes().query("commit:" + commit.name()).get());
 
@@ -90,7 +89,7 @@
 
   @Test
   public void fixAutoCloseableChangeByCommit() throws Exception {
-    RevCommit commit = pushCommitWithoutChangeIdForReview();
+    RevCommit commit = pushCommitForReview();
     ChangeInfo change =
         Iterables.getOnlyElement(gApi.changes().query("commit:" + commit.name()).get());
 
@@ -280,17 +279,18 @@
                 + ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT);
   }
 
-  private RevCommit pushCommitWithoutChangeIdForReview() throws Exception {
-    setRequireChangeId(InheritableBoolean.FALSE);
+  private RevCommit pushCommitForReview() throws Exception {
     RevCommit commit =
         testRepo
             .branch("HEAD")
             .commit()
             .message("A change")
+            .insertChangeId()
             .author(admin.newIdent())
             .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
             .create();
     pushHead(testRepo, "refs/for/master");
+
     return commit;
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 91c8d22..da0cd43 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -316,7 +316,6 @@
     assertThat(info.useSignedOffBy.configuredValue).isEqualTo(input.useSignedOffBy);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
         .isEqualTo(input.createNewChangeForAllNotInTarget);
-    assertThat(info.requireChangeId.configuredValue).isEqualTo(input.requireChangeId);
     assertThat(info.rejectImplicitMerges.configuredValue).isEqualTo(input.rejectImplicitMerges);
     assertThat(info.enableReviewerByEmail.configuredValue).isEqualTo(input.enableReviewerByEmail);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
@@ -346,7 +345,6 @@
     assertThat(info.useSignedOffBy.configuredValue).isEqualTo(input.useSignedOffBy);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
         .isEqualTo(input.createNewChangeForAllNotInTarget);
-    assertThat(info.requireChangeId.configuredValue).isEqualTo(input.requireChangeId);
     assertThat(info.rejectImplicitMerges.configuredValue).isEqualTo(input.rejectImplicitMerges);
     assertThat(info.enableReviewerByEmail.configuredValue).isEqualTo(input.enableReviewerByEmail);
     assertThat(info.createNewChangeForAllNotInTarget.configuredValue)
@@ -683,7 +681,6 @@
     input.useContentMerge = InheritableBoolean.TRUE;
     input.useSignedOffBy = InheritableBoolean.TRUE;
     input.createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
-    input.requireChangeId = InheritableBoolean.TRUE;
     input.rejectImplicitMerges = InheritableBoolean.TRUE;
     input.enableReviewerByEmail = InheritableBoolean.TRUE;
     input.createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
@@ -715,10 +712,6 @@
       countsByProject.clear();
     }
 
-    long getCount(String projectName) {
-      return countsByProject.get(projectName);
-    }
-
     void assertReindexOf(String projectName) {
       assertReindexOf(projectName, 1);
     }
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 553f225..3d9b88d 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -89,7 +89,6 @@
 import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -101,7 +100,6 @@
 import com.google.gerrit.server.git.receive.ReceiveConstants;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -125,7 +123,6 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
@@ -460,20 +457,6 @@
   }
 
   @Test
-  public void pushWithoutChangeIdDeprecated() throws Exception {
-    setRequireChangeId(InheritableBoolean.FALSE);
-    testRepo
-        .branch("HEAD")
-        .commit()
-        .message("A change")
-        .author(admin.newIdent())
-        .committer(new PersonIdent(admin.newIdent(), testRepo.getDate()))
-        .create();
-    PushResult result = pushHead(testRepo, "refs/for/master");
-    assertThat(result.getMessages()).contains("warning: pushing without Change-Id is deprecated");
-  }
-
-  @Test
   public void autocloseByChangeId() throws Exception {
     // Create a change
     PushOneCommit.Result r = pushTo("refs/for/master");
@@ -1256,40 +1239,6 @@
   }
 
   @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true")
-  public void pushToRefsChangesAllowed() throws Exception {
-    PushOneCommit.Result r = pushOneCommitToRefsChanges();
-    r.assertOkStatus();
-  }
-
-  @Test
-  public void pushNewPatchsetToRefsChanges() throws Exception {
-    PushOneCommit.Result r = pushOneCommitToRefsChanges();
-    r.assertErrorStatus("upload to refs/changes not allowed");
-  }
-
-  @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "false")
-  public void pushToRefsChangesNotAllowed() throws Exception {
-    PushOneCommit.Result r = pushOneCommitToRefsChanges();
-    r.assertErrorStatus("upload to refs/changes not allowed");
-  }
-
-  private PushOneCommit.Result pushOneCommitToRefsChanges() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master");
-    r.assertOkStatus();
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            PushOneCommit.SUBJECT,
-            "b.txt",
-            "anotherContent",
-            r.getChangeId());
-    return push.to("refs/changes/" + r.getChange().change().getId().get());
-  }
-
-  @Test
   public void pushNewPatchsetToPatchSetLockedChange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     r.assertOkStatus();
@@ -1607,9 +1556,6 @@
     RevCommit c = createCommit(testRepo, "Message without Change-Id");
     assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
     pushForReviewRejected(testRepo, "missing Change-Id in message footer");
-
-    setRequireChangeId(InheritableBoolean.FALSE);
-    pushForReviewOk(testRepo);
   }
 
   @Test
@@ -1633,9 +1579,6 @@
                 + "More text, uh oh.\n");
     assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty();
     pushForReviewRejected(testRepo, "Change-Id must be in message footer");
-
-    setRequireChangeId(InheritableBoolean.FALSE);
-    pushForReviewRejected(testRepo, "Change-Id must be in message footer");
   }
 
   @Test
@@ -1654,28 +1597,6 @@
   }
 
   @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true")
-  public void testPushWithChangedChangeId() throws Exception {
-    PushOneCommit.Result r = pushTo("refs/for/master");
-    r.assertOkStatus();
-    PushOneCommit push =
-        pushFactory.create(
-            admin.newIdent(),
-            testRepo,
-            PushOneCommit.SUBJECT
-                + "\n\n"
-                + "Change-Id: I55eab7c7a76e95005fa9cc469aa8f9fc16da9eba\n",
-            "b.txt",
-            "anotherContent",
-            r.getChangeId());
-    r = push.to("refs/changes/" + r.getChange().change().getId().get());
-    r.assertErrorStatus(
-        String.format(
-            "commit %s: %s",
-            abbreviateName(r.getCommit()), ChangeIdValidator.CHANGE_ID_MISMATCH_MSG));
-  }
-
-  @Test
   public void pushWithMultipleChangeIds() throws Exception {
     testPushWithMultipleChangeIds();
   }
@@ -1694,9 +1615,6 @@
             + "Change-Id: I10f98c2ef76e52e23aa23be5afeb71e40b350e86\n"
             + "Change-Id: Ie9a132e107def33bdd513b7854b50de911edba0a\n");
     pushForReviewRejected(testRepo, "multiple Change-Id lines in message footer");
-
-    setRequireChangeId(InheritableBoolean.FALSE);
-    pushForReviewRejected(testRepo, "multiple Change-Id lines in message footer");
   }
 
   @Test
@@ -1713,9 +1631,6 @@
   private void testpushWithInvalidChangeId() throws Exception {
     createCommit(testRepo, "Message with invalid Change-Id\n\nChange-Id: X\n");
     pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
-
-    setRequireChangeId(InheritableBoolean.FALSE);
-    pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
   }
 
   @Test
@@ -1737,18 +1652,12 @@
             + "\n"
             + "Change-Id: I0000000000000000000000000000000000000000\n");
     pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
-
-    setRequireChangeId(InheritableBoolean.FALSE);
-    pushForReviewRejected(testRepo, "invalid Change-Id line format in message footer");
   }
 
   @Test
   public void pushWithChangeIdInSubjectLine() throws Exception {
     createCommit(testRepo, "Change-Id: I1234000000000000000000000000000000000000");
     pushForReviewRejected(testRepo, "missing subject; Change-Id must be in message footer");
-
-    setRequireChangeId(InheritableBoolean.FALSE);
-    pushForReviewRejected(testRepo, "missing subject; Change-Id must be in message footer");
   }
 
   @Test
@@ -1766,19 +1675,6 @@
         "same Change-Id in multiple changes.\n"
             + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
             + " commit");
-
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
-      u.save();
-    }
-
-    pushForReviewRejected(
-        testRepo,
-        "same Change-Id in multiple changes.\n"
-            + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
-            + " commit");
   }
 
   @Test
@@ -1792,19 +1688,6 @@
         "same Change-Id in multiple changes.\n"
             + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
             + " commit");
-
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig()
-          .getProject()
-          .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
-      u.save();
-    }
-
-    pushForReviewRejected(
-        testRepo,
-        "same Change-Id in multiple changes.\n"
-            + "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
-            + " commit");
   }
 
   private static RevCommit createCommit(TestRepository<?> testRepo, String message)
@@ -1851,25 +1734,6 @@
   }
 
   @Test
-  @GerritConfig(name = "receive.allowPushToRefsChanges", value = "true")
-  public void accidentallyPushNewPatchSetDirectlyToBranchAndRecoverByPushingToRefsChanges()
-      throws Exception {
-    Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
-    ChangeData cd = byChangeId(id);
-    String ps1Rev = Iterables.getOnlyElement(cd.patchSets()).commitId().name();
-
-    String r = "refs/changes/" + id;
-    assertPushOk(pushHead(testRepo, r, false), r);
-
-    // Added a new patch set and auto-closed the change.
-    cd = byChangeId(id);
-    assertThat(cd.change().isMerged()).isTrue();
-    assertThat(getPatchSetRevisions(cd))
-        .containsExactlyEntriesIn(
-            ImmutableMap.of(1, ps1Rev, 2, testRepo.getRepository().resolve("HEAD").name()));
-  }
-
-  @Test
   public void accidentallyPushNewPatchSetDirectlyToBranchAndCantRecoverByPushingToRefsFor()
       throws Exception {
     Change.Id id = accidentallyPushNewPatchSetDirectlyToBranch();
@@ -2799,10 +2663,6 @@
     return cds.get(0);
   }
 
-  private static void pushForReviewOk(TestRepository<?> testRepo) throws GitAPIException {
-    pushForReview(testRepo, RemoteRefUpdate.Status.OK, null);
-  }
-
   private static void pushForReviewRejected(TestRepository<?> testRepo, String expectedMessage)
       throws GitAPIException {
     pushForReview(testRepo, RemoteRefUpdate.Status.REJECTED_OTHER_REASON, expectedMessage);
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 46012f4..66af8a4 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.git.testing.PushResultSubject.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -31,10 +30,8 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -62,8 +59,6 @@
   public void setUp() throws Exception {
     try (ProjectConfigUpdate u = updateProject(allProjects)) {
       ProjectConfig cfg = u.getConfig();
-      cfg.getProject()
-          .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
 
       // Remove push-related permissions, so they can be added back individually by test methods.
       removeAllBranchPermissions(
@@ -98,17 +93,6 @@
   }
 
   @Test
-  public void mixingDirectChangesAndRegularPush() throws Exception {
-    testRepo.branch("HEAD").commit().create();
-    PushResult r = push("HEAD:refs/heads/master", "HEAD:refs/changes/01/101");
-
-    String msg = "cannot combine normal pushes and magic pushes";
-    assertThat(r.getRemoteUpdate("refs/heads/master")).isNotEqualTo(Status.OK);
-    assertThat(r.getRemoteUpdate("refs/changes/01/101")).isNotEqualTo(Status.OK);
-    assertThat(r.getRemoteUpdate("refs/heads/master").getMessage()).isEqualTo(msg);
-  }
-
-  @Test
   public void fastForwardUpdateDenied() throws Exception {
     testRepo.branch("HEAD").commit().create();
     PushResult r = push("HEAD:refs/heads/master");
@@ -267,7 +251,8 @@
         .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS))
         .update();
 
-    ObjectId commit = testRepo.branch("HEAD").commit().create();
+    ObjectId commit =
+        testRepo.branch("HEAD").commit().message("test commit").insertChangeId().create();
     assertThat(push("HEAD:refs/for/master")).onlyRef("refs/for/master").isOk();
     gApi.changes().id(commit.name()).current().review(ReviewInput.approve());
 
@@ -430,7 +415,7 @@
       case REJECTED_OTHER_REASON:
       case RENAMED:
       default:
-        assert_().fail("fetch failed to update local %s: %s", ref, u.getResult());
+        assertWithMessage("fetch failed to update local %s: %s", ref, u.getResult()).fail();
         break;
     }
     return u.getNewObjectId();
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index ed8c081..88fc557 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -630,11 +630,10 @@
     try (Repository repo = repoManager.openRepository(project)) {
       // c2 <- newcommit1 (branch)
       PushOneCommit.Result r =
-          r =
-              pushFactory
-                  .create(admin.newIdent(), testRepo)
-                  .setParent(rcBranch)
-                  .to("refs/heads/branch");
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/heads/branch");
       r.assertOkStatus();
       RevCommit tagRc = r.getCommit();
 
@@ -687,11 +686,10 @@
     try (Repository repo = repoManager.openRepository(project)) {
       // rcBranch (c2) <- newcommit1 (branch)
       PushOneCommit.Result r =
-          r =
-              pushFactory
-                  .create(admin.newIdent(), testRepo)
-                  .setParent(rcBranch)
-                  .to("refs/heads/branch");
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/heads/branch");
       r.assertOkStatus();
       RevCommit tagRc = r.getCommit();
 
@@ -743,11 +741,10 @@
     try (Repository repo = repoManager.openRepository(project)) {
       // rcBranch (c2) <- newcommit1 (branch)
       PushOneCommit.Result r =
-          r =
-              pushFactory
-                  .create(admin.newIdent(), testRepo)
-                  .setParent(rcBranch)
-                  .to("refs/heads/branch");
+          pushFactory
+              .create(admin.newIdent(), testRepo)
+              .setParent(rcBranch)
+              .to("refs/heads/branch");
       r.assertOkStatus();
       RevCommit tagRc = r.getCommit();
 
@@ -921,7 +918,6 @@
               .setParent(rcBranch)
               .to("refs/tags/updated-tag");
       r.assertOkStatus();
-      RevCommit tagRc = r.getCommit();
     }
 
     assertUploadPackRefs(
diff --git a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
index b41c5bb..640f65e 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefOperationValidationIT.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
@@ -83,9 +83,10 @@
   @Test
   public void rejectRefCreation() throws Exception {
     try (TestRefValidator validator = new TestRefValidator(CREATE)) {
-      gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
-      assert_().fail("expected exception");
-    } catch (RestApiException expected) {
+      RestApiException expected =
+          assertThrows(
+              RestApiException.class,
+              () -> gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput()));
       assertThat(expected).hasMessageThat().contains(CREATE.name());
     }
   }
@@ -115,9 +116,10 @@
   public void rejectRefDeletion() throws Exception {
     gApi.projects().name(project.get()).branch(TEST_REF).create(new BranchInput());
     try (TestRefValidator validator = new TestRefValidator(DELETE)) {
-      gApi.projects().name(project.get()).branch(TEST_REF).delete();
-      assert_().fail("expected exception");
-    } catch (RestApiException expected) {
+      RestApiException expected =
+          assertThrows(
+              RestApiException.class,
+              () -> gApi.projects().name(project.get()).branch(TEST_REF).delete());
       assertThat(expected).hasMessageThat().contains(DELETE.name());
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 02f7b0a..3924992 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -24,8 +24,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -34,6 +40,7 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
@@ -358,6 +365,75 @@
     assertThat(cd1.patchSet(psId1_2).commitId()).isEqualTo(c1_2);
   }
 
+  @Test
+  public void pushForSubmitWithNotifyOption() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    TestAccount user = accountCreator.user();
+    String pushSpec = "refs/for/master%reviewer=" + user.email();
+    sender.clear();
+
+    PushOneCommit.Result result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE);
+    result.assertOkStatus();
+    assertThat(sender.getMessages()).isEmpty();
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.OWNER);
+    result.assertOkStatus();
+    assertThat(sender.getMessages()).isEmpty();
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.OWNER_REVIEWERS);
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user, null);
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit,notify=" + NotifyHandling.ALL);
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user, null);
+
+    sender.clear();
+    result = pushTo(pushSpec + ",submit"); // default is notify = ALL
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user, null);
+  }
+
+  @Test
+  public void pushForSubmitWithNotifyingUsersExplicitly() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(adminGroupUuid()))
+        .update();
+
+    TestAccount user = accountCreator.user();
+    String pushSpec = "refs/for/master%reviewer=" + user.email() + ",cc=" + user.email();
+
+    TestAccount user2 = accountCreator.user2();
+
+    sender.clear();
+    PushOneCommit.Result result =
+        pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE + ",notify-to=" + user2.email());
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user2, RecipientType.TO);
+
+    sender.clear();
+    result =
+        pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE + ",notify-cc=" + user2.email());
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user2, RecipientType.CC);
+
+    sender.clear();
+    result =
+        pushTo(pushSpec + ",submit,notify=" + NotifyHandling.NONE + ",notify-bcc=" + user2.email());
+    result.assertOkStatus();
+    assertThatEmailsForChangeCreationAndSubmitWereSent(user2, RecipientType.BCC);
+  }
+
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId) throws Exception {
     ChangeNotes notes = notesFactory.createChecked(project, patchSetId.changeId()).load();
     return approvalsUtil.getSubmitter(notes, patchSetId);
@@ -405,4 +481,45 @@
         pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content, changeId);
     return push.to(ref);
   }
+
+  /**
+   * Makes sure that two emails are sent: one for the change creation, and one for the submit.
+   *
+   * @param expected The account expected to receive message.
+   * @param expectedRecipientType The notification's type: To/Cc/Bcc. if {@code null} then it is not
+   *     needed to check the recipientType. It is meant for -notify without other flags like
+   *     notify-cc, notify-to, and notify-bcc. With the -notify flag, the message can sometimes be
+   *     sent as "To" and sometimes can be sent as "Cc".
+   */
+  private void assertThatEmailsForChangeCreationAndSubmitWereSent(
+      TestAccount expected, @Nullable RecipientType expectedRecipientType) {
+    String expectedEmail = expected.email();
+    String expectedFullName = expected.fullName();
+    Address expectedAddress = new Address(expectedFullName, expectedEmail);
+    assertThat(sender.getMessages()).hasSize(2);
+    Message message = sender.getMessages().get(0);
+    assertThat(message.body().contains("review")).isTrue();
+    assertAddress(message, expectedAddress, expectedRecipientType);
+    message = sender.getMessages().get(1);
+    assertThat(message.rcpt()).containsExactly(expectedAddress);
+    assertAddress(message, expectedAddress, expectedRecipientType);
+    assertThat(message.body().contains("submitted")).isTrue();
+  }
+
+  private void assertAddress(
+      Message message, Address expectedAddress, @Nullable RecipientType expectedRecipientType) {
+    assertThat(message.rcpt()).containsExactly(expectedAddress);
+    if (expectedRecipientType != null
+        && expectedRecipientType
+            != RecipientType.BCC) { // When Bcc, it does not appear in the header.
+      String expectedRecipientTypeString = "To";
+      if (expectedRecipientType == RecipientType.CC) {
+        expectedRecipientTypeString = "Cc";
+      }
+      assertThat(
+              ((EmailHeader.AddressList) message.headers().get(expectedRecipientTypeString))
+                  .getAddressList())
+          .containsExactly(expectedAddress);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 6991a250..3fca298 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -37,7 +37,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_1);
+    return getConfig(ElasticVersion.V7_2);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index d40bcdf..56a9b69 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -16,17 +16,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_OK;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.truth.Expect;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.restapi.ParameterParser;
@@ -37,16 +42,19 @@
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
-import java.util.Map;
-import java.util.Optional;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import org.apache.http.message.BasicHeader;
@@ -55,12 +63,29 @@
 import org.junit.Rule;
 import org.junit.Test;
 
+/**
+ * This test tests the tracing of requests.
+ *
+ * <p>To verify that tracing is working we do:
+ *
+ * <ul>
+ *   <li>Register a plugin extension that we know is invoked when the request is done. Within the
+ *       implementation of this plugin extension we access the status of the thread local state in
+ *       the {@link LoggingContext} and store it locally in the plugin extension class.
+ *   <li>Do a request (e.g. REST) that triggers the plugin extension.
+ *   <li>When the plugin extension is invoked it records the current logging context.
+ *   <li>After the request is done the test verifies that logging context that was recorded by the
+ *       plugin extension has the expected state.
+ * </ul>
+ */
 public class TraceIT extends AbstractDaemonTest {
   @Rule public final Expect expect = Expect.create();
 
   @Inject private DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
   @Inject private DynamicSet<CommitValidationListener> commitValidationListeners;
+  @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
   @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
+  @Inject private DynamicSet<SubmitRule> submitRules;
   @Inject private WorkQueue workQueue;
 
   private TraceValidatingProjectCreationValidationListener projectCreationListener;
@@ -96,6 +121,29 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
     assertThat(projectCreationListener.traceId).isNull();
     assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new1");
+  }
+
+  @Test
+  public void restCallForChangeSetsProjectTag() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    TraceChangeIndexedListener changeIndexedListener = new TraceChangeIndexedListener();
+    RegistrationHandle registrationHandle =
+        changeIndexedListeners.add("gerrit", changeIndexedListener);
+    try {
+      RestResponse response =
+          adminRestSession.post(
+              "/changes/" + changeId + "/revisions/current/review", ReviewInput.approve());
+      assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(changeIndexedListener.tags.get("project")).containsExactly(project.get());
+    } finally {
+      registrationHandle.remove();
+    }
   }
 
   @Test
@@ -106,6 +154,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
     assertThat(projectCreationListener.traceId).isNotNull();
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new2");
   }
 
   @Test
@@ -116,6 +165,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
     assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new3");
   }
 
   @Test
@@ -127,6 +177,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
     assertThat(projectCreationListener.traceId).isNotNull();
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new4");
   }
 
   @Test
@@ -138,6 +189,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
     assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new5");
   }
 
   @Test
@@ -150,6 +202,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
     assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new6");
 
     // trace ID only specified by trace request parameter
     response =
@@ -159,6 +212,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
     assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new7");
 
     // same trace ID specified by trace header and trace request parameter
     response =
@@ -169,6 +223,7 @@
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
     assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new8");
 
     // different trace IDs specified by trace header and trace request parameter
     response =
@@ -180,6 +235,7 @@
         .containsExactly("issue/123", "issue/456");
     assertThat(projectCreationListener.traceIds).containsExactly("issue/123", "issue/456");
     assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new9");
   }
 
   @Test
@@ -189,6 +245,9 @@
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
     assertThat(commitValidationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
   }
 
   @Test
@@ -199,6 +258,7 @@
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNotNull();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
+    assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
   }
 
   @Test
@@ -209,6 +269,7 @@
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
     assertThat(commitValidationListener.isLoggingForced).isTrue();
+    assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
   }
 
   @Test
@@ -218,6 +279,9 @@
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
     assertThat(commitValidationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
   }
 
   @Test
@@ -228,6 +292,7 @@
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNotNull();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
+    assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
   }
 
   @Test
@@ -238,6 +303,7 @@
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
     assertThat(commitValidationListener.isLoggingForced).isTrue();
+    assertThat(commitValidationListener.tags.get("project")).containsExactly(project.get());
   }
 
   @Test
@@ -308,6 +374,256 @@
     assertThat(testPerformanceLogger.logEntries()).isEmpty();
   }
 
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new12")
+  public void traceProject() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new12");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new12");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectMatchRegEx() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new13");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+  public void traceProjectNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new13");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new13");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "][")
+  public void traceProjectInvalidRegEx() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new14");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new14");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  public void traceAccount() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new15");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new15");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+  public void traceAccountNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new16");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new16");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "999")
+  public void traceAccountNotFound() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new17");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new17");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "invalid")
+  public void traceAccountInvalidId() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new18");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new18");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "REST")
+  public void traceRequestType() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new19");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new19");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "SSH")
+  public void traceRequestTypeNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new20");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new20");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestType", value = "FOO")
+  public void traceProjectInvalidRequestType() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new21");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new21");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectForAccount() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new22");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new22");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000000")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "foo.*")
+  public void traceProjectForAccountNoProjectMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new23");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.account", value = "1000001")
+  @GerritConfig(name = "tracing.issue123.projectPattern", value = "new.*")
+  public void traceProjectForAccountNoAccountMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new24");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  public void traceRequestUri() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new23");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*/foo")
+  public void traceRequestUriNoMatch() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new23");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new23");
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "][")
+  public void traceRequestUriInvalidRegEx() throws Exception {
+    RestResponse response = adminRestSession.put("/projects/new24");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+    assertThat(projectCreationListener.traceId).isNull();
+    assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+    // The logging tag with the project name is also set if tracing is off.
+    assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
+  }
+
+  @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+  public void autoRetryWithTrace() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+
+    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+    traceSubmitRule.failOnce = true;
+    RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
+    try {
+      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+      assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
+      assertThat(traceSubmitRule.isLoggingForced).isTrue();
+    } finally {
+      submitRuleRegistrationHandle.remove();
+    }
+  }
+
+  @Test
+  public void noAutoRetryWithTraceIfDisabled() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+
+    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+    traceSubmitRule.failOnce = true;
+    RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
+    try {
+      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(traceSubmitRule.traceId).isNull();
+      assertThat(traceSubmitRule.isLoggingForced).isNull();
+    } finally {
+      submitRuleRegistrationHandle.remove();
+    }
+  }
+
   private void assertForceLogging(boolean expected) {
     assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
         .isEqualTo(expected);
@@ -318,6 +634,7 @@
     String traceId;
     ImmutableSet<String> traceIds;
     Boolean isLoggingForced;
+    ImmutableSetMultimap<String, String> tags;
 
     @Override
     public void validateNewProject(CreateProjectArgs args) throws ValidationException {
@@ -325,12 +642,14 @@
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
       this.traceIds = LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID");
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+      this.tags = LoggingContext.getInstance().getTagsAsMap();
     }
   }
 
   private static class TraceValidatingCommitValidationListener implements CommitValidationListener {
     String traceId;
     Boolean isLoggingForced;
+    ImmutableSetMultimap<String, String> tags;
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
@@ -338,16 +657,51 @@
       this.traceId =
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+      this.tags = LoggingContext.getInstance().getTagsAsMap();
       return ImmutableList.of();
     }
   }
 
+  private static class TraceChangeIndexedListener implements ChangeIndexedListener {
+    ImmutableSetMultimap<String, String> tags;
+
+    @Override
+    public void onChangeIndexed(String projectName, int id) {
+      this.tags = LoggingContext.getInstance().getTagsAsMap();
+    }
+
+    @Override
+    public void onChangeDeleted(int id) {}
+  }
+
+  private static class TraceSubmitRule implements SubmitRule {
+    String traceId;
+    Boolean isLoggingForced;
+    boolean failOnce;
+
+    @Override
+    public Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options) {
+      if (failOnce) {
+        failOnce = false;
+        throw new IllegalStateException("forced failure from test");
+      }
+
+      this.traceId =
+          Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
+      this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
+
+      SubmitRecord submitRecord = new SubmitRecord();
+      submitRecord.status = SubmitRecord.Status.OK;
+      return ImmutableList.of(submitRecord);
+    }
+  }
+
   private static class TestPerformanceLogger implements PerformanceLogger {
     private List<PerformanceLogEntry> logEntries = new ArrayList<>();
 
     @Override
-    public void log(String operation, long durationMs, Map<String, Optional<Object>> metaData) {
-      logEntries.add(PerformanceLogEntry.create(operation, metaData));
+    public void log(String operation, long durationMs, Metadata metadata) {
+      logEntries.add(PerformanceLogEntry.create(operation, metadata));
     }
 
     ImmutableList<PerformanceLogEntry> logEntries() {
@@ -357,12 +711,12 @@
 
   @AutoValue
   abstract static class PerformanceLogEntry {
-    static PerformanceLogEntry create(String operation, Map<String, Optional<Object>> metaData) {
-      return new AutoValue_TraceIT_PerformanceLogEntry(operation, ImmutableMap.copyOf(metaData));
+    static PerformanceLogEntry create(String operation, Metadata metadata) {
+      return new AutoValue_TraceIT_PerformanceLogEntry(operation, metadata);
     }
 
     abstract String operation();
 
-    abstract ImmutableMap<String, Object> metaData();
+    abstract Metadata metadata();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index be3f2a0..c2df9ca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -502,6 +502,81 @@
   }
 
   @Test
+  public void submitWholeTopicWithMultipleTopics() throws Throwable {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    String topic1 = "test-topic-1";
+    String topic2 = "test-topic-2";
+    PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic1);
+    PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "content", topic1);
+    PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic2);
+    PushOneCommit.Result change4 = createChange("Change 4", "d.txt", "content", topic2);
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    approve(change3.getChangeId());
+    approve(change4.getChangeId());
+    submit(change4.getChangeId());
+    String expectedTopic1 = name(topic1);
+    String expectedTopic2 = name(topic2);
+    if (getSubmitType() == SubmitType.CHERRY_PICK) {
+      change1.assertChange(Change.Status.NEW, expectedTopic1, admin);
+      change2.assertChange(Change.Status.NEW, expectedTopic1, admin);
+
+    } else {
+      change1.assertChange(Change.Status.MERGED, expectedTopic1, admin);
+      change2.assertChange(Change.Status.MERGED, expectedTopic1, admin);
+    }
+
+    // Check for the exact change to have the correct submitter.
+    assertSubmitter(change4);
+    // Also check submitters for changes submitted via the topic relationship.
+    assertSubmitter(change3);
+    if (getSubmitType() != SubmitType.CHERRY_PICK) {
+      assertSubmitter(change1);
+      assertSubmitter(change2);
+    }
+
+    // Check that the repo has the expected commits
+    List<RevCommit> log = getRemoteLog();
+    List<String> commitsInRepo = log.stream().map(RevCommit::getShortMessage).collect(toList());
+    int expectedCommitCount;
+    switch (getSubmitType()) {
+      case MERGE_ALWAYS:
+        // initial commit + 4 commits + merge commit
+        expectedCommitCount = 6;
+        break;
+      case CHERRY_PICK:
+        // initial commit + 2 commits
+        expectedCommitCount = 3;
+        break;
+      case FAST_FORWARD_ONLY:
+      case INHERIT:
+      case MERGE_IF_NECESSARY:
+      case REBASE_ALWAYS:
+      case REBASE_IF_NECESSARY:
+      default:
+        // initial commit + 4 commits
+        expectedCommitCount = 5;
+        break;
+    }
+    assertThat(log).hasSize(expectedCommitCount);
+
+    if (getSubmitType() == SubmitType.CHERRY_PICK) {
+      assertThat(commitsInRepo).containsAtLeast("Initial empty repository", "Change 3", "Change 4");
+      assertThat(commitsInRepo).doesNotContain("Change 1");
+      assertThat(commitsInRepo).doesNotContain("Change 2");
+    } else if (getSubmitType() == SubmitType.MERGE_ALWAYS) {
+      assertThat(commitsInRepo)
+          .contains(
+              String.format(
+                  "Merge changes from topics \"%s\", \"%s\"", expectedTopic1, expectedTopic2));
+    } else {
+      assertThat(commitsInRepo)
+          .containsAtLeast(
+              "Initial empty repository", "Change 1", "Change 2", "Change 3", "Change 4");
+    }
+  }
+
+  @Test
   public void submitReusingOldTopic() throws Throwable {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
index 49692dd..ccf1c0d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -29,113 +31,165 @@
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
-import org.junit.After;
-import org.junit.Before;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
 public class WorkInProgressByDefaultIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
-  @Inject private RequestScopeOperations requestScopeOperations;
-
-  private Project.NameKey project1;
-  private Project.NameKey project2;
-
-  @Before
-  public void setUp() throws Exception {
-    project1 = projectOperations.newProject().create();
-    project2 = projectOperations.newProject().parent(project1).create();
-  }
-
-  @After
-  public void tearDown() throws Exception {
-    requestScopeOperations.setApiUser(admin.id());
-    GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id().get()).getPreferences();
-    prefs.workInProgressByDefault = false;
-    gApi.accounts().id(admin.id().get()).setPreferences(prefs);
-  }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     ChangeInfo info =
-        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+        gApi.changes().create(new ChangeInput(project.get(), "master", "empty change")).get();
     assertThat(info.workInProgress).isNull();
   }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
   }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
   }
 
   @Test
   public void createChangeBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     input.workInProgress = false;
     assertThat(gApi.changes().create(input).get().workInProgress).isNull();
   }
 
   @Test
   public void createChangeBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
-    ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
     input.workInProgress = false;
     assertThat(gApi.changes().create(input).get().workInProgress).isNull();
   }
 
   @Test
   public void createChangeWithWorkInProgressByDefaultForProjectInherited() throws Exception {
-    setWorkInProgressByDefaultForProject(project1);
+    Project.NameKey parentProject = projectOperations.newProject().create();
+    Project.NameKey childProject = projectOperations.newProject().parent(parentProject).create();
+    setWorkInProgressByDefaultForProject(parentProject);
     ChangeInfo info =
-        gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+        gApi.changes().create(new ChangeInput(childProject.get(), "master", "empty change")).get();
     assertThat(info.workInProgress).isTrue();
   }
 
   @Test
   public void pushWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
+    assertThat(createChange(project).getChange().change().isWorkInProgress()).isTrue();
   }
 
   @Test
   public void pushWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+    assertThat(createChange(project).getChange().change().isWorkInProgress()).isTrue();
   }
 
   @Test
   public void pushBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
-    setWorkInProgressByDefaultForProject(project2);
+    Project.NameKey project = projectOperations.newProject().create();
+    setWorkInProgressByDefaultForProject(project);
     assertThat(
-            createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+            createChange(project, "refs/for/master%ready").getChange().change().isWorkInProgress())
         .isFalse();
   }
 
   @Test
   public void pushBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
     setWorkInProgressByDefaultForUser();
     assertThat(
-            createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+            createChange(project, "refs/for/master%ready").getChange().change().isWorkInProgress())
         .isFalse();
   }
 
   @Test
   public void pushWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isFalse();
+    Project.NameKey project = projectOperations.newProject().create();
+    assertThat(createChange(project).getChange().change().isWorkInProgress()).isFalse();
   }
 
   @Test
   public void pushWorkInProgressByDefaultForProjectInherited() throws Exception {
-    setWorkInProgressByDefaultForProject(project1);
-    assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+    Project.NameKey parentProject = projectOperations.newProject().create();
+    Project.NameKey childProject = projectOperations.newProject().parent(parentProject).create();
+    setWorkInProgressByDefaultForProject(parentProject);
+    assertThat(createChange(childProject).getChange().change().isWorkInProgress()).isTrue();
+  }
+
+  @Test
+  public void pushNewPatchSetWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+
+    // Create change.
+    TestRepository<InMemoryRepository> testRepo = cloneProject(project);
+    PushOneCommit.Result result =
+        pushFactory.create(admin.newIdent(), testRepo).to("refs/for/master");
+    result.assertOkStatus();
+
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+
+    setWorkInProgressByDefaultForUser();
+
+    // Create new patch set on existing change, this shouldn't mark the change as WIP.
+    result = pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master");
+    result.assertOkStatus();
+    assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
+  }
+
+  @Test
+  public void pushNewPatchSetAndNewChangeAtOnceWithWorkInProgressByDefaultForUserEnabled()
+      throws Exception {
+    Project.NameKey project = projectOperations.newProject().create();
+
+    // Create change.
+    TestRepository<InMemoryRepository> testRepo = cloneProject(project);
+    RevCommit initialHead = getHead(testRepo.getRepository(), "HEAD");
+    RevCommit commit1a =
+        testRepo.commit().parent(initialHead).message("Change 1").insertChangeId().create();
+    String changeId1 = GitUtil.getChangeId(testRepo, commit1a).get();
+    testRepo.reset(commit1a);
+    PushResult result = pushHead(testRepo, "refs/for/master", false);
+    assertPushOk(result, "refs/for/master");
+    assertThat(gApi.changes().id(changeId1).get().workInProgress).isNull();
+
+    setWorkInProgressByDefaultForUser();
+
+    // Create a new patch set on the existing change and in the same push create a new successor
+    // change.
+    RevCommit commit1b = testRepo.amend(commit1a).create();
+    testRepo.reset(commit1b);
+    RevCommit commit2 =
+        testRepo.commit().parent(commit1b).message("Change 2").insertChangeId().create();
+    String changeId2 = GitUtil.getChangeId(testRepo, commit2).get();
+    testRepo.reset(commit2);
+    result = pushHead(testRepo, "refs/for/master", false);
+    assertPushOk(result, "refs/for/master");
+
+    // Check that the existing change (changeId1) is not marked as WIP, but only the newly created
+    // change (changeId2).
+    assertThat(gApi.changes().id(changeId1).get().workInProgress).isNull();
+    assertThat(gApi.changes().id(changeId2).get().workInProgress).isTrue();
   }
 
   private void setWorkInProgressByDefaultForProject(Project.NameKey p) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 14521cc..4a74018 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -70,7 +70,6 @@
   // gerrit
   @GerritConfig(name = "gerrit.allProjects", value = "Root")
   @GerritConfig(name = "gerrit.allUsers", value = "Users")
-  @GerritConfig(name = "gerrit.reportBugText", value = "REPORT BUG")
   @GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report")
 
   // suggest
@@ -112,7 +111,6 @@
     assertThat(i.gerrit.allProjects).isEqualTo("Root");
     assertThat(i.gerrit.allUsers).isEqualTo("Users");
     assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
-    assertThat(i.gerrit.reportBugText).isEqualTo("REPORT BUG");
 
     // plugin
     assertThat(i.plugin.jsResourcePaths).isEmpty();
@@ -179,7 +177,6 @@
     assertThat(i.gerrit.allProjects).isEqualTo(AllProjectsNameProvider.DEFAULT);
     assertThat(i.gerrit.allUsers).isEqualTo(AllUsersNameProvider.DEFAULT);
     assertThat(i.gerrit.reportBugUrl).isNull();
-    assertThat(i.gerrit.reportBugText).isNull();
 
     // plugin
     assertThat(i.plugin.jsResourcePaths).isEmpty();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 043bde7..b102619 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -218,7 +218,6 @@
     in.useContributorAgreements = InheritableBoolean.TRUE;
     in.useSignedOffBy = InheritableBoolean.TRUE;
     in.useContentMerge = InheritableBoolean.TRUE;
-    in.requireChangeId = InheritableBoolean.TRUE;
     ProjectInfo p = gApi.projects().create(in).get();
     assertThat(p.name).isEqualTo(newProjectName);
     Project project = projectCache.get(Project.nameKey(newProjectName)).getProject();
@@ -231,8 +230,6 @@
         .isEqualTo(in.useSignedOffBy);
     assertThat(project.getBooleanConfig(BooleanProjectConfig.USE_CONTENT_MERGE))
         .isEqualTo(in.useContentMerge);
-    assertThat(project.getBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID))
-        .isEqualTo(in.requireChangeId);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
index 52e72fe..f98fb45 100644
--- a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.util;
 
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth.assert_;
 import static org.apache.http.HttpStatus.SC_FORBIDDEN;
 import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
 import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
@@ -76,7 +75,8 @@
         response = restSession.delete(uri);
         break;
       default:
-        assert_().fail(String.format("unsupported method: %s", restCall.httpMethod().name()));
+        assertWithMessage(String.format("unsupported method: %s", restCall.httpMethod().name()))
+            .fail();
         throw new IllegalStateException();
     }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 085cfea..6842926 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -557,6 +557,13 @@
 
   @Test
   public void publishCommentsAllRevisions() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+
+    pushFactory
+        .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "initial content\n", changeId)
+        .to("refs/heads/master");
+
     PushOneCommit.Result r1 =
         pushFactory
             .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "old boring content\n")
@@ -580,7 +587,7 @@
     addDraft(
         r1.getChangeId(),
         r1.getCommit().getName(),
-        newDraft(FILE_NAME, Side.PARENT, 2, "what happened to this?"));
+        newDraft(FILE_NAME, Side.PARENT, createLineRange(1, 0, 7), "what happened to this?"));
     addDraft(
         r2.getChangeId(),
         r2.getCommit().getName(),
@@ -665,8 +672,8 @@
                 + project.get()
                 + "/+/"
                 + c
-                + "/1/a.txt@a2 \n"
-                + "PS1, Line 2: \n"
+                + "/1/a.txt@a1 \n"
+                + "PS1, Line 1: initial\n"
                 + "what happened to this?\n"
                 + "\n"
                 + "\n"
@@ -694,7 +701,7 @@
                 + "/+/"
                 + c
                 + "/2/a.txt@a1 \n"
-                + "PS2, Line 1: \n"
+                + "PS2, Line 1: initial content\n"
                 + "comment 1 on base\n"
                 + "\n"
                 + "\n"
@@ -724,7 +731,7 @@
                 + "/+/"
                 + c
                 + "/2/a.txt@2 \n"
-                + "PS2, Line 2: nten\n"
+                + "PS2, Line 2: cntent\n"
                 + "typo: content\n"
                 + "\n"
                 + "\n");
@@ -1150,7 +1157,7 @@
 
   private DraftInput newDraft(String path, Side side, Comment.Range range, String message) {
     DraftInput d = new DraftInput();
-    return populate(d, path, side, null, range, message, false);
+    return populate(d, path, side, null, range.startLine, range, message, false);
   }
 
   private DraftInput newDraftOnParent(String path, int parent, int line, String message) {
@@ -1163,23 +1170,25 @@
       String path,
       Side side,
       Integer parent,
+      int line,
       Comment.Range range,
       String message,
       Boolean unresolved) {
-    int line = range.startLine;
     c.path = path;
     c.side = side;
     c.parent = parent;
     c.line = line != 0 ? line : null;
     c.message = message;
     c.unresolved = unresolved;
-    if (line != 0) c.range = range;
+    if (range != null) {
+      c.range = range;
+    }
     return c;
   }
 
   private static <C extends Comment> C populate(
       C c, String path, Side side, Integer parent, int line, String message, Boolean unresolved) {
-    return populate(c, path, side, parent, createLineRange(line, 1, 5), message, unresolved);
+    return populate(c, path, side, parent, line, null, message, unresolved);
   }
 
   private static Comment.Range createLineRange(int line, int startChar, int endChar) {
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index eaf65ae..f81ca4c 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -36,7 +36,7 @@
 
   @ConfigSuite.Config
   public static Config elasticsearchV7() {
-    return getConfig(ElasticVersion.V7_1);
+    return getConfig(ElasticVersion.V7_2);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index 5dd07c0..09e97b2 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -18,13 +18,13 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogger;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.project.CreateProjectArgs;
@@ -33,8 +33,6 @@
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.Optional;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -129,8 +127,8 @@
     private List<PerformanceLogEntry> logEntries = new ArrayList<>();
 
     @Override
-    public void log(String operation, long durationMs, Map<String, Optional<Object>> metaData) {
-      logEntries.add(PerformanceLogEntry.create(operation, metaData));
+    public void log(String operation, long durationMs, Metadata metadata) {
+      logEntries.add(PerformanceLogEntry.create(operation, metadata));
     }
 
     ImmutableList<PerformanceLogEntry> logEntries() {
@@ -140,12 +138,12 @@
 
   @AutoValue
   abstract static class PerformanceLogEntry {
-    static PerformanceLogEntry create(String operation, Map<String, Optional<Object>> metaData) {
-      return new AutoValue_SshTraceIT_PerformanceLogEntry(operation, ImmutableMap.copyOf(metaData));
+    static PerformanceLogEntry create(String operation, Metadata metadata) {
+      return new AutoValue_SshTraceIT_PerformanceLogEntry(operation, metadata);
     }
 
     abstract String operation();
 
-    abstract ImmutableMap<String, Object> metaData();
+    abstract Metadata metadata();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index de13552..34406e0 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -118,7 +118,7 @@
       // that is currently not public.
       char channel = packet.charAt(0);
       if (channel != 1) {
-        assert_().fail("got packet on channel " + (int) channel, packet);
+        assertWithMessage("got packet on channel " + (int) channel, packet).fail();
       }
     }
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index ba64b2e..a0c40c7 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -54,6 +54,8 @@
         return "blacktop/elasticsearch:7.0.1";
       case V7_1:
         return "blacktop/elasticsearch:7.1.1";
+      case V7_2:
+        return "blacktop/elasticsearch:7.2.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 78c3684..c6faa7b 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_2);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index ae00e0d..780c8ab 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -51,7 +51,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_2);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
     client = HttpAsyncClients.createDefault();
     client.start();
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 301b5dd..188ed26 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_2);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index e1b7e3f..88617ee 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_1);
+    container = ElasticContainer.createAndStart(ElasticVersion.V7_2);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index d28cbb4..0ad80de 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -48,6 +48,9 @@
 
     assertThat(ElasticVersion.forVersion("7.1.0")).isEqualTo(ElasticVersion.V7_1);
     assertThat(ElasticVersion.forVersion("7.1.1")).isEqualTo(ElasticVersion.V7_1);
+
+    assertThat(ElasticVersion.forVersion("7.2.0")).isEqualTo(ElasticVersion.V7_2);
+    assertThat(ElasticVersion.forVersion("7.2.1")).isEqualTo(ElasticVersion.V7_2);
   }
 
   @Test
@@ -73,6 +76,7 @@
     assertThat(ElasticVersion.V6_7.isAtLeastMinorVersion(ElasticVersion.V6_7)).isTrue();
     assertThat(ElasticVersion.V7_0.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
     assertThat(ElasticVersion.V7_1.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
+    assertThat(ElasticVersion.V7_2.isAtLeastMinorVersion(ElasticVersion.V6_7)).isFalse();
   }
 
   @Test
@@ -86,6 +90,7 @@
     assertThat(ElasticVersion.V6_7.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_0.isV6OrLater()).isTrue();
     assertThat(ElasticVersion.V7_1.isV6OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_2.isV6OrLater()).isTrue();
   }
 
   @Test
@@ -99,5 +104,6 @@
     assertThat(ElasticVersion.V6_7.isV7OrLater()).isFalse();
     assertThat(ElasticVersion.V7_0.isV7OrLater()).isTrue();
     assertThat(ElasticVersion.V7_1.isV7OrLater()).isTrue();
+    assertThat(ElasticVersion.V7_2.isV7OrLater()).isTrue();
   }
 }
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index 0fbd922..6849d66 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -25,5 +25,6 @@
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
         "//lib/truth",
+        "//lib/truth:truth-java8-extension",
     ],
 )
diff --git a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
index 13d04ab..33919e7 100644
--- a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
+++ b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -82,7 +82,7 @@
         metrics.newCounter(
             "test/count",
             new Description("simple test").setCumulative(),
-            Field.ofString("action").build());
+            Field.ofString("action", Field.ignoreMetadata()).build());
 
     Counter total = get("test/count_total", Counter.class);
     assertThat(total.getCount()).isEqualTo(0);
@@ -107,7 +107,7 @@
             new Description("simple test")
                 .setCumulative()
                 .setFieldOrdering(FieldOrdering.PREFIX_FIELDS_BASENAME),
-            Field.ofString("action").build());
+            Field.ofString("action", Field.ignoreMetadata()).build());
 
     Counter total = get("test/count_total", Counter.class);
     assertThat(total.getCount()).isEqualTo(0);
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index d4ad7d7..5470e3c 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.account.AccountResolver.Searcher;
 import com.google.gerrit.server.account.AccountResolver.StringSearcher;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.util.Arrays;
 import java.util.function.Predicate;
@@ -329,14 +328,13 @@
   }
 
   private AccountState newAccount(int id) {
-    return AccountState.forAccount(
-        new AllUsersName("All-Users"), new Account(Account.id(id), TimeUtil.nowTs()));
+    return AccountState.forAccount(new Account(Account.id(id), TimeUtil.nowTs()));
   }
 
   private AccountState newInactiveAccount(int id) {
     Account a = new Account(Account.id(id), TimeUtil.nowTs());
     a.setActive(false);
-    return AccountState.forAccount(new AllUsersName("All-Users"), a);
+    return AccountState.forAccount(a);
   }
 
   private static ImmutableSet<Account.Id> ids(int... ids) {
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index 5573be7..0f73cc5 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -39,8 +39,7 @@
     Account account = new Account(Account.id(1), TimeUtil.nowTs());
     String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
     account.setMetaId(metaId);
-    List<String> values =
-        toStrings(AccountField.REF_STATE.get(AccountState.forAccount(allUsersName, account)));
+    List<String> values = toStrings(AccountField.REF_STATE.get(AccountState.forAccount(account)));
     assertThat(values).hasSize(1);
     String expectedValue =
         allUsersName.get() + ":" + RefNames.refsUsers(account.getId()) + ":" + metaId;
@@ -68,7 +67,7 @@
     List<String> values =
         toStrings(
             AccountField.EXTERNAL_ID_STATE.get(
-                AccountState.forAccount(null, account, ImmutableSet.of(extId1, extId2))));
+                AccountState.forAccount(account, ImmutableSet.of(extId1, extId2))));
     String expectedValue1 = extId1.key().sha1().name() + ":" + extId1.blobId().name();
     String expectedValue2 = extId2.key().sha1().name() + ":" + extId2.blobId().name();
     assertThat(values).containsExactly(expectedValue1, expectedValue2);
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
index 8e2ca09c..733d784 100644
--- a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
@@ -24,8 +24,6 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import java.util.Map;
-import java.util.Optional;
 import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.concurrent.ExecutorService;
@@ -53,8 +51,7 @@
     testPerformanceLogger =
         new PerformanceLogger() {
           @Override
-          public void log(
-              String operation, long durationMs, Map<String, Optional<Object>> metaData) {
+          public void log(String operation, long durationMs, Metadata metadata) {
             // do nothing
           }
         };
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index 6b7df5e..ed4325d 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -18,7 +18,6 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.metrics.Description;
@@ -35,8 +34,6 @@
 import com.google.inject.Injector;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.junit.After;
 import org.junit.Before;
@@ -80,87 +77,17 @@
       assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
 
       TraceContext.newTimer("test1").close();
-      TraceContext.newTimer("test2", "foo", "bar").close();
-      TraceContext.newTimer("test3", "foo1", "bar1", "foo2", "bar2").close();
-      TraceContext.newTimer("test4", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3").close();
-      TraceContext.newTimer("test5", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3", "foo4", "bar4")
+      TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
           .close();
 
-      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(5);
+      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(2);
     }
 
     assertThat(testPerformanceLogger.logEntries())
         .containsExactly(
-            PerformanceLogEntry.create("test1", ImmutableMap.of()),
-            PerformanceLogEntry.create("test2", ImmutableMap.of("foo", Optional.of("bar"))),
+            PerformanceLogEntry.create("test1", Metadata.empty()),
             PerformanceLogEntry.create(
-                "test3", ImmutableMap.of("foo1", Optional.of("bar1"), "foo2", Optional.of("bar2"))),
-            PerformanceLogEntry.create(
-                "test4",
-                ImmutableMap.of(
-                    "foo1",
-                    Optional.of("bar1"),
-                    "foo2",
-                    Optional.of("bar2"),
-                    "foo3",
-                    Optional.of("bar3"))),
-            PerformanceLogEntry.create(
-                "test5",
-                ImmutableMap.of(
-                    "foo1",
-                    Optional.of("bar1"),
-                    "foo2",
-                    Optional.of("bar2"),
-                    "foo3",
-                    Optional.of("bar3"),
-                    "foo4",
-                    Optional.of("bar4"))))
-        .inOrder();
-
-    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-  }
-
-  @Test
-  public void traceTimersInsidePerformanceLogContextCreatePerformanceLogNullValuesAllowed() {
-    assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
-    assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
-
-    try (PerformanceLogContext traceContext =
-        new PerformanceLogContext(config, performanceLoggers)) {
-      assertThat(LoggingContext.getInstance().isPerformanceLogging()).isTrue();
-
-      TraceContext.newTimer("test1").close();
-      TraceContext.newTimer("test2", "foo", null).close();
-      TraceContext.newTimer("test3", "foo1", null, "foo2", null).close();
-      TraceContext.newTimer("test4", "foo1", null, "foo2", null, "foo3", null).close();
-      TraceContext.newTimer("test5", "foo1", null, "foo2", null, "foo3", null, "foo4", null)
-          .close();
-
-      assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(5);
-    }
-
-    assertThat(testPerformanceLogger.logEntries())
-        .containsExactly(
-            PerformanceLogEntry.create("test1", ImmutableMap.of()),
-            PerformanceLogEntry.create("test2", ImmutableMap.of("foo", Optional.empty())),
-            PerformanceLogEntry.create(
-                "test3", ImmutableMap.of("foo1", Optional.empty(), "foo2", Optional.empty())),
-            PerformanceLogEntry.create(
-                "test4",
-                ImmutableMap.of(
-                    "foo1", Optional.empty(), "foo2", Optional.empty(), "foo3", Optional.empty())),
-            PerformanceLogEntry.create(
-                "test5",
-                ImmutableMap.of(
-                    "foo1",
-                    Optional.empty(),
-                    "foo2",
-                    Optional.empty(),
-                    "foo3",
-                    Optional.empty(),
-                    "foo4",
-                    Optional.empty())))
+                "test2", Metadata.builder().accountId(1000000).changeId(123).build()))
         .inOrder();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
@@ -173,10 +100,7 @@
     assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
 
     TraceContext.newTimer("test1").close();
-    TraceContext.newTimer("test2", "foo", "bar").close();
-    TraceContext.newTimer("test3", "foo1", "bar1", "foo2", "bar2").close();
-    TraceContext.newTimer("test4", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3").close();
-    TraceContext.newTimer("test5", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3", "foo4", "bar4")
+    TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
         .close();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
@@ -198,10 +122,7 @@
       assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
 
       TraceContext.newTimer("test1").close();
-      TraceContext.newTimer("test2", "foo", "bar").close();
-      TraceContext.newTimer("test3", "foo1", "bar1", "foo2", "bar2").close();
-      TraceContext.newTimer("test4", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3").close();
-      TraceContext.newTimer("test5", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3", "foo4", "bar4")
+      TraceContext.newTimer("test2", Metadata.builder().accountId(1000000).changeId(123).build())
           .close();
 
       assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
@@ -226,50 +147,43 @@
           metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
       timer0.start().close();
 
-      Timer1<String> timer1 =
+      Timer1<Integer> timer1 =
           metricMaker.newTimer(
               "test2/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build());
-      timer1.start("value1").close();
+              Field.ofInteger("account", Metadata.Builder::accountId).build());
+      timer1.start(1000000).close();
 
-      Timer2<String, String> timer2 =
+      Timer2<Integer, Integer> timer2 =
           metricMaker.newTimer(
               "test3/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build(),
-              Field.ofString("bar").build());
-      timer2.start("value1", "value2").close();
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build());
+      timer2.start(1000000, 123).close();
 
-      Timer3<String, String, String> timer3 =
+      Timer3<Integer, Integer, String> timer3 =
           metricMaker.newTimer(
               "test4/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build(),
-              Field.ofString("bar").build(),
-              Field.ofString("baz").build());
-      timer3.start("value1", "value2", "value3").close();
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build(),
+              Field.ofString("project", Metadata.Builder::projectName).build());
+      timer3.start(1000000, 123, "foo/bar").close();
 
       assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(4);
     }
 
     assertThat(testPerformanceLogger.logEntries())
         .containsExactly(
-            PerformanceLogEntry.create("test1/latency", ImmutableMap.of()),
+            PerformanceLogEntry.create("test1/latency", Metadata.empty()),
             PerformanceLogEntry.create(
-                "test2/latency", ImmutableMap.of("foo", Optional.of("value1"))),
+                "test2/latency", Metadata.builder().accountId(1000000).build()),
             PerformanceLogEntry.create(
-                "test3/latency",
-                ImmutableMap.of("foo", Optional.of("value1"), "bar", Optional.of("value2"))),
+                "test3/latency", Metadata.builder().accountId(1000000).changeId(123).build()),
             PerformanceLogEntry.create(
                 "test4/latency",
-                ImmutableMap.of(
-                    "foo",
-                    Optional.of("value1"),
-                    "bar",
-                    Optional.of("value2"),
-                    "baz",
-                    Optional.of("value3"))))
+                Metadata.builder().accountId(1000000).changeId(123).projectName("foo/bar").build()))
         .inOrder();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
@@ -289,24 +203,24 @@
           metricMaker.newTimer(
               "test1/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build());
+              Field.ofString("project", Metadata.Builder::projectName).build());
       timer1.start(null).close();
 
       Timer2<String, String> timer2 =
           metricMaker.newTimer(
               "test2/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build(),
-              Field.ofString("bar").build());
+              Field.ofString("project", Metadata.Builder::projectName).build(),
+              Field.ofString("branch", Metadata.Builder::branchName).build());
       timer2.start(null, null).close();
 
       Timer3<String, String, String> timer3 =
           metricMaker.newTimer(
               "test3/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build(),
-              Field.ofString("bar").build(),
-              Field.ofString("baz").build());
+              Field.ofString("project", Metadata.Builder::projectName).build(),
+              Field.ofString("branch", Metadata.Builder::branchName).build(),
+              Field.ofString("revision", Metadata.Builder::revision).build());
       timer3.start(null, null, null).close();
 
       assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(3);
@@ -314,13 +228,9 @@
 
     assertThat(testPerformanceLogger.logEntries())
         .containsExactly(
-            PerformanceLogEntry.create("test1/latency", ImmutableMap.of("foo", Optional.empty())),
-            PerformanceLogEntry.create(
-                "test2/latency", ImmutableMap.of("foo", Optional.empty(), "bar", Optional.empty())),
-            PerformanceLogEntry.create(
-                "test3/latency",
-                ImmutableMap.of(
-                    "foo", Optional.empty(), "bar", Optional.empty(), "baz", Optional.empty())))
+            PerformanceLogEntry.create("test1/latency", Metadata.empty()),
+            PerformanceLogEntry.create("test2/latency", Metadata.empty()),
+            PerformanceLogEntry.create("test3/latency", Metadata.empty()))
         .inOrder();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
@@ -336,29 +246,29 @@
         metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
     timer0.start().close();
 
-    Timer1<String> timer1 =
+    Timer1<Integer> timer1 =
         metricMaker.newTimer(
             "test2/latency",
             new Description("Latency metric for testing"),
-            Field.ofString("foo").build());
-    timer1.start("value1").close();
+            Field.ofInteger("account", Metadata.Builder::accountId).build());
+    timer1.start(1000000).close();
 
-    Timer2<String, String> timer2 =
+    Timer2<Integer, Integer> timer2 =
         metricMaker.newTimer(
             "test3/latency",
             new Description("Latency metric for testing"),
-            Field.ofString("foo").build(),
-            Field.ofString("bar").build());
-    timer2.start("value1", "value2").close();
+            Field.ofInteger("account", Metadata.Builder::accountId).build(),
+            Field.ofInteger("change", Metadata.Builder::changeId).build());
+    timer2.start(1000000, 123).close();
 
-    Timer3<String, String, String> timer3 =
+    Timer3<Integer, Integer, String> timer3 =
         metricMaker.newTimer(
             "test4/latency",
             new Description("Latency metric for testing"),
-            Field.ofString("foo").build(),
-            Field.ofString("bar").build(),
-            Field.ofString("baz").build());
-    timer3.start("value1", "value2", "value3").close();
+            Field.ofInteger("account", Metadata.Builder::accountId).build(),
+            Field.ofInteger("change", Metadata.Builder::changeId).build(),
+            Field.ofString("project", Metadata.Builder::projectName).build());
+    timer3.start(1000000, 123, "value3").close();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
     assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
@@ -382,29 +292,29 @@
           metricMaker.newTimer("test1/latency", new Description("Latency metric for testing"));
       timer0.start().close();
 
-      Timer1<String> timer1 =
+      Timer1<Integer> timer1 =
           metricMaker.newTimer(
               "test2/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build());
-      timer1.start("value1").close();
+              Field.ofInteger("accoutn", Metadata.Builder::accountId).build());
+      timer1.start(1000000).close();
 
-      Timer2<String, String> timer2 =
+      Timer2<Integer, Integer> timer2 =
           metricMaker.newTimer(
               "test3/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build(),
-              Field.ofString("bar").build());
-      timer2.start("value1", "value2").close();
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build());
+      timer2.start(1000000, 123).close();
 
-      Timer3<String, String, String> timer3 =
+      Timer3<Integer, Integer, String> timer3 =
           metricMaker.newTimer(
               "test4/latency",
               new Description("Latency metric for testing"),
-              Field.ofString("foo").build(),
-              Field.ofString("bar").build(),
-              Field.ofString("baz").build());
-      timer3.start("value1", "value2", "value3").close();
+              Field.ofInteger("account", Metadata.Builder::accountId).build(),
+              Field.ofInteger("change", Metadata.Builder::changeId).build(),
+              Field.ofString("project", Metadata.Builder::projectName).build());
+      timer3.start(1000000, 123, "foo/bar").close();
 
       assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
     }
@@ -450,8 +360,8 @@
     private List<PerformanceLogEntry> logEntries = new ArrayList<>();
 
     @Override
-    public void log(String operation, long durationMs, Map<String, Optional<Object>> metaData) {
-      logEntries.add(PerformanceLogEntry.create(operation, metaData));
+    public void log(String operation, long durationMs, Metadata metadata) {
+      logEntries.add(PerformanceLogEntry.create(operation, metadata));
     }
 
     ImmutableList<PerformanceLogEntry> logEntries() {
@@ -461,13 +371,12 @@
 
   @AutoValue
   abstract static class PerformanceLogEntry {
-    static PerformanceLogEntry create(String operation, Map<String, Optional<Object>> metaData) {
-      return new AutoValue_PerformanceLogContextTest_PerformanceLogEntry(
-          operation, ImmutableMap.copyOf(metaData));
+    static PerformanceLogEntry create(String operation, Metadata metadata) {
+      return new AutoValue_PerformanceLogContextTest_PerformanceLogEntry(operation, metadata);
     }
 
     abstract String operation();
 
-    abstract ImmutableMap<String, Object> metaData();
+    abstract Metadata metadata();
   }
 }
diff --git a/javatests/com/google/gerrit/server/logging/TraceContextTest.java b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
index fedbe8b..13f2035 100644
--- a/javatests/com/google/gerrit/server/logging/TraceContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
@@ -240,58 +240,17 @@
   @Test
   public void operationForTraceTimerCannotBeNull() throws Exception {
     assertThrows(NullPointerException.class, () -> TraceContext.newTimer(null));
-    assertThrows(NullPointerException.class, () -> TraceContext.newTimer(null, "foo", "bar"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer(null, "foo1", "bar1", "foo2", "bar2"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer(null, "foo1", "bar1", "foo2", "bar2", "foo3", "bar3"));
+    assertThrows(NullPointerException.class, () -> TraceContext.newTimer(null, Metadata.empty()));
     assertThrows(
         NullPointerException.class,
         () ->
             TraceContext.newTimer(
-                null, "foo1", "bar1", "foo2", "bar2", "foo3", "bar3", "foo4", "bar4"));
+                null, Metadata.builder().accountId(1000000).changeId(123).build()));
   }
 
   @Test
-  public void keysForTraceTimerCannotBeNull() throws Exception {
-    assertThrows(NullPointerException.class, () -> TraceContext.newTimer("test", null, "bar"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer("test", null, "bar1", "foo2", "bar2"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer("test", "foo1", "bar1", null, "bar2"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer("test", null, "bar1", "foo2", "bar2", "foo3", "bar3"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer("test", "foo1", "bar1", null, "bar2", "foo3", "bar3"));
-    assertThrows(
-        NullPointerException.class,
-        () -> TraceContext.newTimer("test", "foo1", "bar1", "foo2", "bar2", null, "bar3"));
-    assertThrows(
-        NullPointerException.class,
-        () ->
-            TraceContext.newTimer(
-                "test", null, "bar1", "foo2", "bar2", "foo3", "bar3", "foo4", "bar4"));
-    assertThrows(
-        NullPointerException.class,
-        () ->
-            TraceContext.newTimer(
-                "test", "foo1", "bar1", null, "bar2", "foo3", "bar3", "foo4", "bar4"));
-    assertThrows(
-        NullPointerException.class,
-        () ->
-            TraceContext.newTimer(
-                "test", "foo1", "bar1", "foo2", "bar2", null, "bar3", "foo4", "bar4"));
-    assertThrows(
-        NullPointerException.class,
-        () ->
-            TraceContext.newTimer(
-                "test", "foo1", "bar1", "foo2", "bar2", "foo3", "bar3", null, "bar4"));
+  public void metadataForTraceTimerCannotBeNull() throws Exception {
+    assertThrows(NullPointerException.class, () -> TraceContext.newTimer("test", null));
   }
 
   private void assertTags(ImmutableMap<String, ImmutableSet<String>> expectedTagMap) {
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 128279f..0e04739 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -25,8 +25,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.util.Arrays;
 import java.util.List;
@@ -383,6 +381,6 @@
     final Account account = new Account(userId, TimeUtil.nowTs());
     account.setFullName(name);
     account.setPreferredEmail(email);
-    return AccountState.forAccount(new AllUsersName(AllUsersNameProvider.DEFAULT), account);
+    return AccountState.forAccount(account);
   }
 }
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 75e1cd7..aacd41b 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.gerrit.reviewdb.client.BooleanProjectConfig.REQUIRE_CHANGE_ID;
+import static com.google.gerrit.reviewdb.client.BooleanProjectConfig.USE_CONTRIBUTOR_AGREEMENTS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -612,13 +612,13 @@
   public void readAllProjectsBaseConfigFromSitePaths() throws Exception {
     ProjectConfig cfg = factory.create(ALL_PROJECTS);
     cfg.load(db);
-    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
         .isEqualTo(InheritableBoolean.INHERIT);
 
-    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
+    writeDefaultAllProjectsConfig("[receive]", "requireContributorAgreement = false");
 
     cfg.load(db);
-    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
         .isEqualTo(InheritableBoolean.FALSE);
   }
 
@@ -626,17 +626,17 @@
   public void readOtherProjectIgnoresAllProjectsBaseConfig() throws Exception {
     ProjectConfig cfg = factory.create(Project.nameKey("test"));
     cfg.load(db);
-    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
         .isEqualTo(InheritableBoolean.INHERIT);
 
-    writeDefaultAllProjectsConfig("[receive]", "requireChangeId = false");
+    writeDefaultAllProjectsConfig("[receive]", "requireContributorAgreement = false");
 
     cfg.load(db);
     // If we went through ProjectState, then this would return FALSE, since project.config for
     // All-Projects would inherit from all_projects.config, and this project would inherit from
     // All-Projects. But in ProjectConfig itself, there is no inheritance from All-Projects, so this
     // continues to return the default.
-    assertThat(cfg.getProject().getBooleanConfig(REQUIRE_CHANGE_ID))
+    assertThat(cfg.getProject().getBooleanConfig(USE_CONTRIBUTOR_AGREEMENTS))
         .isEqualTo(InheritableBoolean.INHERIT);
   }
 
diff --git a/javatests/com/google/gerrit/testing/GerritJUnitTest.java b/javatests/com/google/gerrit/testing/GerritJUnitTest.java
index 430f48f..56dda08 100644
--- a/javatests/com/google/gerrit/testing/GerritJUnitTest.java
+++ b/javatests/com/google/gerrit/testing/GerritJUnitTest.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.testing;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import org.junit.Test;
@@ -68,7 +68,7 @@
           () -> {
             throw new MyException("foo");
           });
-      assert_().fail("expected AssertionError");
+      assertWithMessage("expected AssertionError").fail();
     } catch (AssertionError e) {
       assertThat(e).hasMessageThat().contains(IllegalStateException.class.getSimpleName());
       assertThat(e).hasMessageThat().contains(MyException.class.getSimpleName());
@@ -81,7 +81,7 @@
   public void assertThrowsThrowsAssertionErrorWhenNothingThrown() {
     try {
       assertThrows(MyException.class, () -> {});
-      assert_().fail("expected AssertionError");
+      assertWithMessage("expected AssertionError").fail();
     } catch (AssertionError e) {
       assertThat(e).hasMessageThat().contains(MyException.class.getSimpleName());
       assertThat(e).hasCauseThat().isNull();
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 2d3f265..56ebd4f 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 2d3f265ab1797d4179cbd6855c937989175d5ce5
+Subproject commit 56ebd4f7a2bf27f89aa11245ff77f7eefcf4a7d6
diff --git a/plugins/hooks b/plugins/hooks
index d285496..cfc7675 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit d285496d44d8e49378910ea28fc8a77d4a86bf9f
+Subproject commit cfc7675ef9c4d0f2bd1da47957835306bb1fd36a
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index a51055a..f681e7e 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit a51055a8f3b71f2ccf634016c42eb5b8086a373b
+Subproject commit f681e7ecb6ddbc52fe9e07cf3672ccdcad7d7d0b
diff --git a/plugins/replication b/plugins/replication
index c449ba1..a5a5e0c 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit c449ba153358150927cb8d88e77f2b3d60d63ecc
+Subproject commit a5a5e0cd13f1ff2614d77e9bf1bacbbc1d61b696
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
index 0e5be3f..4252e6e 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -129,8 +129,8 @@
       return this.getBaseUrl() + '/c/' + changeNum;
     },
 
-    changeIsOpen(status) {
-      return status === this.ChangeStatus.NEW;
+    changeIsOpen(change) {
+      return change && change.status === this.ChangeStatus.NEW;
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
index 071beda..08131ab 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.html
@@ -43,33 +43,34 @@
       <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
         <td>Loading...</td>
       </tr>
-      <template is="dom-repeat" items="[[_auditLog]]"
-          class$="[[computeLoadingClass(_loading)]]">
-        <tr class="table">
-          <td class="date">
-            <gr-date-formatter
-                has-tooltip
-                date-str="[[item.date]]">
-            </gr-date-formatter>
-          </td>
-          <td class="type">[[itemType(item.type)]]</td>
-          <td class="member">
-            <template is="dom-if" if="[[_isGroupEvent(item.type)]]">
-              <a href$="[[_computeGroupUrl(item.member)]]">
-                [[_getNameForGroup(item.member)]]
-              </a>
-            </template>
-            <template is="dom-if" if="[[!_isGroupEvent(item.type)]]">
-              <gr-account-link account="[[item.member]]"></gr-account-link>
-              [[_getIdForUser(item.member)]]
-            </template>
-          </td>
-          <td class="by-user">
-            <gr-account-link account="[[item.user]]"></gr-account-link>
-            [[_getIdForUser(item.user)]]
-          </td>
-        </tr>
-      </template>
+      <tbody class$="[[computeLoadingClass(_loading)]]">
+        <template is="dom-repeat" items="[[_auditLog]]">
+          <tr class="table">
+            <td class="date">
+              <gr-date-formatter
+                  has-tooltip
+                  date-str="[[item.date]]">
+              </gr-date-formatter>
+            </td>
+            <td class="type">[[itemType(item.type)]]</td>
+            <td class="member">
+              <template is="dom-if" if="[[_isGroupEvent(item.type)]]">
+                <a href$="[[_computeGroupUrl(item.member)]]">
+                  [[_getNameForGroup(item.member)]]
+                </a>
+              </template>
+              <template is="dom-if" if="[[!_isGroupEvent(item.type)]]">
+                <gr-account-link account="[[item.member]]"></gr-account-link>
+                [[_getIdForUser(item.member)]]
+              </template>
+            </td>
+            <td class="by-user">
+              <gr-account-link account="[[item.user]]"></gr-account-link>
+              [[_getIdForUser(item.user)]]
+            </td>
+          </tr>
+        </template>
+      </tbody>
     </table>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
index 1c02b3c..e284201 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.html
@@ -128,9 +128,8 @@
                       bind-value="{{_revisedRef}}"
                       class="editItem">
                     <input
-                        is=iron-input
-                        bind-value="{{_revisedRef}}"
-                        class="editItem">
+                        is="iron-input"
+                        bind-value="{{_revisedRef}}">
                   </iron-input>
                   <gr-button
                       link
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
index 872d282..e17409e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.html
@@ -88,7 +88,7 @@
               </template>
               <template is="dom-if" if="[[_isString(option.info.type)]]">
                 <iron-input
-                    value="[[option.info.value]]"
+                    bind-value="[[option.info.value]]"
                     on-input="_handleStringChange"
                     data-option-key$="[[option._key]]"
                     disabled$="[[_computeDisabled(option.info.editable)]]">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 05fb221..044e1fe 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -154,21 +154,6 @@
                   </gr-select>
                 </span>
               </section>
-              <section>
-                <span class="title">Require Change-Id in commit message</span>
-                <span class="value">
-                  <gr-select
-                      id="requireChangeIdSelect"
-                      bind-value="{{_repoConfig.require_change_id.configured_value}}">
-                    <select disabled$="[[_readOnly]]">
-                      <template is="dom-repeat"
-                          items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
-                        <option value="[[item.value]]">[[item.label]]</option>
-                      </template>
-                    </select>
-                  </gr-select>
-                </span>
-              </section>
               <section
                    id="enableSignedPushSettings"
                    class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
@@ -271,6 +256,7 @@
                 <span class="title">Maximum Git object size limit</span>
                 <span class="value">
                   <iron-input
+                      id="maxGitObjSizeIronInput"
                       bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
                       type="text"
                       disabled$="[[_readOnly]]">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index 0ac1834..744ef42 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -57,10 +57,6 @@
         value: false,
         configured_value: 'FALSE',
       },
-      require_change_id: {
-        value: false,
-        configured_value: 'FALSE',
-      },
       enable_signed_push: {
         value: false,
         configured_value: 'FALSE',
@@ -322,7 +318,6 @@
           use_content_merge: 'TRUE',
           use_signed_off_by: 'TRUE',
           create_new_change_for_all_not_in_target: 'TRUE',
-          require_change_id: 'TRUE',
           enable_signed_push: 'TRUE',
           require_signed_push: 'TRUE',
           reject_implicit_merges: 'TRUE',
@@ -352,8 +347,6 @@
               configInputObj.use_content_merge;
           element.$.newChangeSelect.bindValue =
               configInputObj.create_new_change_for_all_not_in_target;
-          element.$.requireChangeIdSelect.bindValue =
-              configInputObj.require_change_id;
           element.$.enableSignedPush.bindValue =
               configInputObj.enable_signed_push;
           element.$.requireSignedPush.bindValue =
@@ -364,8 +357,9 @@
               configInputObj.private_by_default;
           element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
               configInputObj.match_author_to_committer_date;
-          element.$.maxGitObjSizeInput.bindValue =
-              configInputObj.max_object_size_limit;
+          const inputElement = Polymer.Element ?
+              element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
+          inputElement.bindValue = configInputObj.max_object_size_limit;
           element.$.contributorAgreementSelect.bindValue =
               configInputObj.use_contributor_agreements;
           element.$.useSignedOffBySelect.bindValue =
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 7d4dc5a..1f0da2b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -40,11 +40,6 @@
         type: String,
         computed: '_computeChangeURL(change)',
       },
-      needsReview: {
-        type: Boolean,
-        reflectToAttribute: true,
-        computed: '_computeItemNeedsReview(change.reviewed)',
-      },
       statuses: {
         type: Array,
         computed: 'changeStatuses(change)',
@@ -78,10 +73,6 @@
       });
     },
 
-    _computeItemNeedsReview(reviewed) {
-      return reviewed === false;
-    },
-
     _computeChangeURL(change) {
       return Gerrit.Nav.getUrlForChange(change);
     },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 1d76df2..df4a442 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -274,11 +274,5 @@
       assert.equal(element._computeRepoDisplay(change, true),
           '…/test/repo');
     });
-
-    test('_computeItemNeedsReview', () => {
-      assert.isFalse(element._computeItemNeedsReview(undefined));
-      assert.isFalse(element._computeItemNeedsReview(true));
-      assert.isTrue(element._computeItemNeedsReview(false));
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index d2b17a3..57d6f8a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -236,9 +236,9 @@
     },
 
     _computeItemNeedsReview(account, change, showReviewedState) {
-      return showReviewedState && change.reviewed === false &&
+      return showReviewedState && !change.reviewed &&
           !change.work_in_progress &&
-          this.changeIsOpen(change.status) &&
+          this.changeIsOpen(change) &&
           (!account || account._account_id != change.owner._account_id);
     },
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 650ebeb..dce2a55 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -221,38 +221,29 @@
         {
           _number: 1,
           status: 'NEW',
-          reviewed: false,
           owner: {_account_id: 0},
         },
         {
           _number: 2,
           status: 'MERGED',
-          reviewed: false,
           owner: {_account_id: 0},
         },
         {
           _number: 3,
           status: 'ABANDONED',
-          reviewed: false,
           owner: {_account_id: 0},
         },
         {
           _number: 4,
           status: 'NEW',
-          reviewed: false,
           work_in_progress: true,
           owner: {_account_id: 0},
         },
-        {
-          _number: 5,
-          status: 'NEW',
-          owner: {_account_id: 0},
-        },
       ];
       flushAsynchronousOperations();
       let elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 6);
+      assert.equal(elementItems.length, 5);
       for (let i = 0; i < elementItems.length; i++) {
         assert.isFalse(elementItems[i].hasAttribute('needs-review'));
       }
@@ -260,24 +251,22 @@
       element.showReviewedState = true;
       elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 6);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
       assert.isFalse(elementItems[4].hasAttribute('needs-review'));
-      assert.isFalse(elementItems[5].hasAttribute('needs-review'));
 
       element.account = {_account_id: 42};
       elementItems = Polymer.dom(element.root).querySelectorAll(
           'gr-change-list-item');
-      assert.equal(elementItems.length, 6);
+      assert.equal(elementItems.length, 5);
       assert.isFalse(elementItems[0].hasAttribute('needs-review'));
       assert.isTrue(elementItems[1].hasAttribute('needs-review'));
       assert.isFalse(elementItems[2].hasAttribute('needs-review'));
       assert.isFalse(elementItems[3].hasAttribute('needs-review'));
       assert.isFalse(elementItems[4].hasAttribute('needs-review'));
-      assert.isFalse(elementItems[5].hasAttribute('needs-review'));
     });
 
     test('no changes', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index d7ad0f4..608aaf7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -249,7 +249,7 @@
       if (!draftSection || !draftSection.results.length) { return; }
 
       const closedChanges = draftSection.results
-          .filter(change => !this.changeIsOpen(change.status));
+          .filter(change => !this.changeIsOpen(change));
       if (!closedChanges.length) { return; }
 
       this._showDraftsBanner = true;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index ca0aa16..83a454b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -592,7 +592,7 @@
       if (editPatchsetLoaded) {
         // Only show actions that mutate an edit if an actual edit patch set
         // is loaded.
-        if (this.changeIsOpen(this.change.status)) {
+        if (this.changeIsOpen(this.change)) {
           if (editBasedOnCurrentPatchSet) {
             if (!this.actions.publishEdit) {
               this.set('actions.publishEdit', PUBLISH_EDIT);
@@ -614,7 +614,7 @@
         this._deleteAndNotify('deleteEdit');
       }
 
-      if (this.changeIsOpen(this.change.status)) {
+      if (this.changeIsOpen(this.change)) {
         // Only show edit button if there is no edit patchset loaded and the
         // file list is not in edit mode.
         if (editPatchsetLoaded || editMode) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 34e21b3..154fc36 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -177,7 +177,7 @@
     },
 
     _computeHideStrategy(change) {
-      return !this.changeIsOpen(change.status);
+      return !this.changeIsOpen(change);
     },
 
     /**
@@ -222,12 +222,14 @@
     },
 
     _showAddTopic(changeRecord, settingTopic) {
-      const hasTopic = !!changeRecord && !!changeRecord.base.topic;
+      const hasTopic = !!changeRecord &&
+          !!changeRecord.base && !!changeRecord.base.topic;
       return !hasTopic && !settingTopic;
     },
 
     _showTopicChip(changeRecord, settingTopic) {
-      const hasTopic = !!changeRecord && !!changeRecord.base.topic;
+      const hasTopic = !!changeRecord &&
+          !!changeRecord.base && !!changeRecord.base.topic;
       return hasTopic && !settingTopic;
     },
 
@@ -406,7 +408,7 @@
      * @return {Object|null} either an accound or null.
      */
     _getNonOwnerRole(change, role) {
-      if (!change.current_revision ||
+      if (!change || !change.current_revision ||
           !change.revisions[change.current_revision]) {
         return null;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 9d2710b2..db41da0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -817,7 +817,7 @@
 
     _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
       // Await all promises resolving from reload. @See Issue 9057
-      if (loading) { return; }
+      if (loading || !changeComments) { return; }
 
       const commentedPaths = changeComments.getPaths(patchRange);
       const files = Object.assign({}, filesByPath);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 8a8b6b3..07bf139 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -121,7 +121,7 @@
       ];
 
       // Get conflicts if change is open and is mergeable.
-      if (this.changeIsOpen(this.change.status) && this.mergeable) {
+      if (this.changeIsOpen(this.change) && this.mergeable) {
         promises.push(this._getConflicts().then(response => {
           // Because the server doesn't always return a response and the
           // template expects an array, always return an array.
@@ -315,6 +315,7 @@
     _computeConnectedRevisions(change, patchNum, relatedChanges) {
       const connected = [];
       let changeRevision;
+      if (!change) { return []; }
       for (const rev in change.revisions) {
         if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
           changeRevision = rev;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 264639d..cd93a2c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -571,31 +571,31 @@
       //
       this.disabled = false;
 
-      if (response.status !== 400) {
-        // This is all restAPI does when there is no custom error handling.
-        this.fire('server-error', {response});
-        return response;
-      }
-
-      // Process the response body, format a better error message, and fire
-      // an event for gr-event-manager to display.
-      const jsonPromise = this.$.restAPI.getResponseObject(response);
+      // Using response.clone() here, because getResponseObject() and
+      // potentially the generic error handler will want to call text() on the
+      // response object, which can only be done once per object.
+      const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
       return jsonPromise.then(result => {
-        const errors = [];
-        for (const state of ['reviewers', 'ccs']) {
-          if (!result.hasOwnProperty(state)) { continue; }
-          for (const reviewer of Object.values(result[state])) {
-            if (reviewer.error) {
-              errors.push(reviewer.error);
+        // Only perform custom error handling for 400s and a parseable
+        // ReviewResult response.
+        if (response.status === 400 && result) {
+          const errors = [];
+          for (const state of ['reviewers', 'ccs']) {
+            if (!result.hasOwnProperty(state)) { continue; }
+            for (const reviewer of Object.values(result[state])) {
+              if (reviewer.error) {
+                errors.push(reviewer.error);
+              }
             }
           }
+          response = {
+            ok: false,
+            status: response.status,
+            text() { return Promise.resolve(errors.join(', ')); },
+          };
         }
-        response = {
-          ok: false,
-          status: response.status,
-          text() { return Promise.resolve(errors.join(', ')); },
-        };
         this.fire('server-error', {response});
+        return null; // Means that the error has been handled.
       });
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index c36b572..e137eef 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -35,6 +35,25 @@
 </test-fixture>
 
 <script>
+  function cloneableResponse(status, text) {
+    return {
+      ok: false,
+      status,
+      text() {
+        return Promise.resolve(text);
+      },
+      clone() {
+        return {
+          ok: false,
+          status,
+          text() {
+            return Promise.resolve(text);
+          },
+        };
+      },
+    };
+  }
+
   suite('gr-reply-dialog tests', () => {
     let element;
     let changeNum;
@@ -473,11 +492,7 @@
       sandbox.stub(window, 'fetch', () => {
         const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
           '"ccs":{"id2":{"error":"second error"}}}';
-        return Promise.resolve({
-          ok: false,
-          status: 400,
-          text() { return Promise.resolve(text); },
-        });
+        return Promise.resolve(cloneableResponse(400, text));
       });
 
       element.addEventListener('server-error', event => {
@@ -495,6 +510,27 @@
       flush(() => { element.send(); });
     });
 
+    test('non-json 400 is treated as a normal server-error', done => {
+      sandbox.stub(window, 'fetch', () => {
+        const text = 'Comment validation error!';
+        return Promise.resolve(cloneableResponse(400, text));
+      });
+
+      element.addEventListener('server-error', event => {
+        if (event.target !== element) {
+          return;
+        }
+        event.detail.response.text().then(body => {
+          assert.equal(body, 'Comment validation error!');
+          done();
+        });
+      });
+
+      // Async tick is needed because iron-selector content is distributed and
+      // distributed content requires an observer to be set up.
+      flush(() => { element.send(); });
+    });
+
     test('filterReviewerSuggestion', () => {
       const owner = makeAccount();
       const reviewer1 = makeAccount();
@@ -803,60 +839,50 @@
       const error1 = 'error 1';
       const error2 = 'error 2';
       const error3 = 'error 3';
-      const response = {
-        status: 400,
-        text() {
-          return Promise.resolve(')]}\'' + JSON.stringify({
-            reviewers: {
-              username1: {
-                input: 'user 1',
-                error: error1,
-              },
-              username2: {
-                input: 'user 2',
-                error: error2,
-              },
-            },
-            ccs: {
-              username3: {
-                input: 'user 3',
-                error: error3,
-              },
-            },
-          }));
+      const text = ')]}\'' + JSON.stringify({
+        reviewers: {
+          username1: {
+            input: 'user 1',
+            error: error1,
+          },
+          username2: {
+            input: 'user 2',
+            error: error2,
+          },
         },
-      };
+        ccs: {
+          username3: {
+            input: 'user 3',
+            error: error3,
+          },
+        },
+      });
       element.addEventListener('server-error', e => {
         e.detail.response.text().then(text => {
           assert.equal(text, [error1, error2, error3].join(', '));
           done();
         });
       });
-      element._handle400Error(response);
+      element._handle400Error(cloneableResponse(400, text));
     });
 
     test('_handle400Error CCs only', done => {
       const error1 = 'error 1';
-      const response = {
-        status: 400,
-        text() {
-          return Promise.resolve(')]}\'' + JSON.stringify({
-            ccs: {
-              username1: {
-                input: 'user 1',
-                error: error1,
-              },
-            },
-          }));
+      const text = ')]}\'' + JSON.stringify({
+        ccs: {
+          username1: {
+            input: 'user 1',
+            error: error1,
+          },
         },
-      };
+      });
       element.addEventListener('server-error', e => {
         e.detail.response.text().then(text => {
           assert.equal(text, error1);
           done();
         });
       });
-      element._handle400Error(response);
+      element._handle400Error(cloneableResponse(400, text));
     });
 
     test('fires height change when the drafts load', done => {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index babf886..37cb317 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -195,6 +195,9 @@
         </template>
       </ul>
       <div class="rightItems">
+        <gr-endpoint-decorator
+            class="hideOnMobile"
+            name="header-small-banner"></gr-endpoint-decorator>
         <gr-smart-search
             id="search"
             search-query="{{searchQuery}}"></gr-smart-search>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 83f68a1..f247156 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -201,8 +201,11 @@
       if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
         console.error(eventValue.error || eventName);
       } else {
-        console.log(eventName + (eventValue !== undefined ?
-            (': ' + eventValue) : ''));
+        if (eventValue !== undefined) {
+          console.log(`Reporting: ${eventName}: ${eventValue}`);
+        } else {
+          console.log(`Reporting: ${eventName}`);
+        }
       }
     },
 
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index b1c7fd8..4d70bdc 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -197,7 +197,9 @@
     const reporting = document.createElement('gr-reporting');
 
     window.addEventListener('load', () => {
-      reporting.pageLoaded();
+      setTimeout(() => {
+        reporting.pageLoaded();
+      }, 0);
     });
 
     window.addEventListener('WebComponentsReady', () => {
@@ -1116,11 +1118,16 @@
     },
 
     _handleProjectsOldRoute(data) {
+      let params = '';
       if (data.params[1]) {
-        this._redirect('/admin/repos/' + encodeURIComponent(data.params[1]));
-      } else {
-        this._redirect('/admin/repos');
+        params = encodeURIComponent(data.params[1]);
+        if (data.params[1].includes(',')) {
+          params =
+              encodeURIComponent(data.params[1]).replace('%2C', ',');
+        }
       }
+
+      this._redirect(`/admin/repos/${params}`);
     },
 
     _handleRepoCommandsRoute(data) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 90d65137..27016e8 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -1108,6 +1108,28 @@
       });
 
       suite('repo routes', () => {
+        test('_handleProjectsOldRoute', () => {
+          const data = {params: {}};
+          element._handleProjectsOldRoute(data);
+          assert.isTrue(redirectStub.calledOnce);
+          assert.equal(redirectStub.lastCall.args[0], '/admin/repos/');
+        });
+
+        test('_handleProjectsOldRoute test', () => {
+          const data = {params: {1: 'test'}};
+          element._handleProjectsOldRoute(data);
+          assert.isTrue(redirectStub.calledOnce);
+          assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test');
+        });
+
+        test('_handleProjectsOldRoute test,branches', () => {
+          const data = {params: {1: 'test,branches'}};
+          element._handleProjectsOldRoute(data);
+          assert.isTrue(redirectStub.calledOnce);
+          assert.equal(
+              redirectStub.lastCall.args[0], '/admin/repos/test,branches');
+        });
+
         test('_handleRepoRoute', () => {
           const data = {params: {0: 4321}};
           assertDataToParams(data, '_handleRepoRoute', {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 4ec460f..6ac6714 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -35,7 +35,12 @@
       :host(.no-left) .sideBySide ::content .right:not([data-value]) + td {
         display: none;
       }
-      .thread-group, ::slotted(*) .thread-group {
+      ::slotted(*) .thread-group {
+        display: block;
+        max-width: var(--content-width, 80ch);
+        white-space: normal;
+      }
+      .thread-group {
         display: block;
         max-width: var(--content-width, 80ch);
         white-space: normal;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index d2f9214..633e081 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -86,6 +86,9 @@
    * implements the same behavior as the template parsing for imperative slots.
    */
   Gerrit.slotToContent = function(slot) {
+    if (Polymer.Element) {
+      return slot;
+    }
     const content = document.createElement('content');
     content.name = slot.name;
     content.setAttribute('select', `[slot='${slot.name}']`);
diff --git a/polygerrit-ui/app/elements/gr-app-element.html b/polygerrit-ui/app/elements/gr-app-element.html
index af4d799..aff71f7 100644
--- a/polygerrit-ui/app/elements/gr-app-element.html
+++ b/polygerrit-ui/app/elements/gr-app-element.html
@@ -72,10 +72,6 @@
         padding: 0 var(--default-horizontal-margin);
         border-bottom: 1px solid var(--border-color);
       }
-      gr-main-header.shadow {
-        /* Make it obvious for shadow dom testing */
-        border-bottom: 1px solid pink;
-      }
       footer {
         background-color: var(--footer-background-color);
         border-top: 1px solid var(--border-color);
@@ -125,7 +121,6 @@
       <gr-main-header
           id="mainHeader"
           search-query="{{params.query}}"
-          class$="[[_computeShadowClass(_isShadowDom)]]"
           on-mobile-search="_mobileSearchToggle">
       </gr-main-header>
     </gr-fixed-panel>
@@ -191,7 +186,7 @@
         <div class="errorMoreInfo">[[_lastError.moreInfo]]</div>
       </div>
     </main>
-    <footer r="contentinfo" class$="[[_computeShadowClass(_isShadowDom)]]">
+    <footer r="contentinfo">
       <div>
         Powered by <a href="https://www.gerritcodereview.com/" rel="noopener"
         target="_blank">Gerrit Code Review</a>
@@ -201,7 +196,7 @@
       <div>
         <a class="feedback"
             href$="[[_feedbackUrl]]"
-            rel="noopener" target="_blank">Send feedback</a>
+            rel="noopener" target="_blank">Report bug</a>
         | Press &ldquo;?&rdquo; for keyboard shortcuts
         <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
       </div>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 75abc3a..4d556ae 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -73,7 +73,6 @@
       _lastError: Object,
       _lastSearchPage: String,
       _path: String,
-      _isShadowDom: Boolean,
       _pluginScreenName: {
         type: String,
         computed: '_computePluginScreenName(params)',
@@ -122,7 +121,7 @@
     },
 
     ready() {
-      this._isShadowDom = Polymer.Settings.useShadow;
+      this.$.reporting.appStarted(document.visibilityState === 'hidden');
       this.$.router.start();
 
       this.$.restAPI.getAccount().then(account => {
@@ -150,8 +149,6 @@
       // router has been initialized. @see Issue 7837
       this._settingsUrl = Gerrit.Nav.getUrlForSettings();
 
-      this.$.reporting.appStarted(document.visibilityState === 'hidden');
-
       this._viewState = {
         changeView: {
           changeNum: null,
@@ -404,10 +401,6 @@
       this.$.registrationOverlay.close();
     },
 
-    _computeShadowClass(isShadowDom) {
-      return isShadowDom ? 'shadow' : '';
-    },
-
     _goToOpenedChanges() {
       Gerrit.Nav.navigateToStatusSearch('open');
     },
@@ -431,8 +424,6 @@
       if (window.VERSION_INFO) {
         console.log(`UI Version Info: ${window.VERSION_INFO}`);
       }
-      const renderTime = new Date(window.performance.timing.loadEventStart);
-      console.log(`Document loaded at: ${renderTime}`);
       console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
       console.groupEnd();
     },
diff --git a/polygerrit-ui/app/elements/gr-app-p2.html b/polygerrit-ui/app/elements/gr-app-p2.html
index 5742350..dbac7fe 100644
--- a/polygerrit-ui/app/elements/gr-app-p2.html
+++ b/polygerrit-ui/app/elements/gr-app-p2.html
@@ -19,7 +19,7 @@
 </script>
 
 <link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/polymer-resin/polymer-resin.html">
+<link rel="import" href="/bower_components/polymer-resin/standalone/polymer-resin.html">
 <link rel="import" href="/bower_components/polymer/lib/legacy/legacy-data-mixin.html">
 <link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
 <script>
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index f934cff..73d012a 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -47,12 +47,18 @@
       stub('gr-account-dropdown', {
         _getTopContent: sinon.stub(),
       });
+      stub('gr-router', {
+        start: sandbox.stub(),
+      });
       stub('gr-rest-api-interface', {
         getAccount() { return Promise.resolve({}); },
         getAccountCapabilities() { return Promise.resolve({}); },
         getConfig() {
           return Promise.resolve({
             plugin: {},
+            auth: {
+              auth_type: undefined,
+            },
           });
         },
         getPreferences() { return Promise.resolve({my: []}); },
@@ -78,6 +84,13 @@
       assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
     });
 
+    test('reporting called before router start', () => {
+      const element = appElement();
+      const appStartedStub = element.$.reporting.appStarted;
+      const routerStartStub = element.$.router.start;
+      sinon.assert.callOrder(appStartedStub, routerStartStub);
+    });
+
     test('passes config to gr-plugin-host', () => {
       const config = appElement().$.restAPI.getConfig;
       return config.lastCall.returnValue.then(config => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 16a2a35..e8b7212 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -47,9 +47,16 @@
     _applyStyle(name) {
       if (this._stylesApplied.includes(name)) { return; }
       this._stylesApplied.push(name);
+      // Hybrid custom-style syntax:
+      // https://polymer-library.polymer-project.org/2.0/docs/devguide/style-shadow-dom
       const s = document.createElement('style', 'custom-style');
       s.setAttribute('include', name);
-      Polymer.dom(this.root).appendChild(s);
+      const cs = document.createElement('custom-style');
+      cs.appendChild(s);
+      // When using Shadow DOM <custom-style> must be added to the <body>.
+      // Within <gr-external-style> itself the styles would have no effect.
+      const topEl = document.getElementsByTagName('body')[0];
+      topEl.insertBefore(cs, topEl.firstChild);
     },
 
     _importAndApply() {
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index 8de8db8..3403d0d 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -34,7 +34,7 @@
         margin-right: .15em;
         vertical-align: -.25em;
       }
-      .hide {
+      div section.hide {
         display: none;
       }
     </style>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index 383d129..64758ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -19,6 +19,7 @@
 
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="/bower_components/iron-fit-behavior/iron-fit-behavior.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <script src="../../../scripts/rootElement.js"></script>
 <link rel="import" href="../../../styles/shared-styles.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 11c9864..e422b7e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -528,7 +528,11 @@
     // HTML import polyfill adds __importElement pointing to the import tag.
     const script = document.currentScript &&
         (document.currentScript.__importElement || document.currentScript);
-    const src = opt_src || (script && (script.src || script.baseURI));
+
+    let src = opt_src || (script && script.src);
+    if (!src || src.startsWith('data:')) {
+      src = script && script.baseURI;
+    }
     const name = getPluginNameFromUrl(src);
 
     if (opt_version && opt_version !== API_VERSION) {
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 74bd895..1f9615f 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -96,10 +96,8 @@
   {/if}
 
   {if $polymer2}
-    <link rel="preload" href="{$staticResourcePath}/elements/gr-app-p2.js" as="script" crossorigin="anonymous">{\n}
     <link rel="import" href="{$staticResourcePath}/elements/gr-app-p2.html">{\n}
   {else}
-    <link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n}
     <link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
   {/if}
 
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index 23f86ee..d92ec51 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -63,7 +63,7 @@
 running() {
   test -f $1 || return 1
   PID=`cat $1`
-  ps -p $PID >/dev/null 2>/dev/null || return 1
+  ps ax -o pid | grep -w $PID >/dev/null 2>/dev/null || return 1
   return 0
 }
 
diff --git a/resources/com/google/gerrit/server/mail/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index 04d54c4..899d1c0 100644
--- a/resources/com/google/gerrit/server/mail/Merged.soy
+++ b/resources/com/google/gerrit/server/mail/Merged.soy
@@ -25,7 +25,7 @@
   {@param change: ?}
   {@param email: ?}
   {@param fromName: ?}
-  {$fromName} has submitted this change and it was merged.
+  {$fromName} has submitted this change.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
   Change subject: {$change.subject}{\n}
diff --git a/resources/com/google/gerrit/server/mail/MergedHtml.soy b/resources/com/google/gerrit/server/mail/MergedHtml.soy
index e8c04a5..f0a47c7 100644
--- a/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -21,7 +21,7 @@
   {@param email: ?}
   {@param fromName: ?}
   <p>
-    {$fromName} <strong>merged</strong> this change.
+    {$fromName} <strong>submitted</strong> this change.
   </p>
 
   {if $email.changeUrl}
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 188a2a1..0408b2b 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -81,7 +81,7 @@
 
     out = ctx.execute(cmd)
     if out.return_code:
-        fail("failed %s: %s" % (" ".join(cmd), out.stderr))
+        fail("failed %s: %s" % (cmd, out.stderr))
 
     _bash(ctx, " && ".join([
         "TMP=$(mktemp -d || mktemp -d -t bazel-tmp)",
@@ -117,7 +117,7 @@
     cmd_list = ["bash", "-c", cmd]
     out = ctx.execute(cmd_list)
     if out.return_code:
-        fail("failed %s: %s" % (" ".join(cmd_list), out.stderr))
+        fail("failed %s: %s" % (cmd_list, out.stderr))
 
 bower_archive = repository_rule(
     _bower_archive,