Merge changes If0d4ed46,I37e0362a

* changes:
  gr-change-actions: Fix typo in comment
  gr-change-actions: Remove redundant code
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 1666444..496c205 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -212,7 +212,15 @@
 === Trace
 
 For executing SSH commands tracing can be enabled by setting the
-`--trace` option.
+`--trace` and `--trace-id <trace-id>` options. It is recommended to use
+the ID of the issue that is being investigated as trace ID.
+
+----
+  $ ssh -p 29418 review.example.com gerrit create-project --trace --trace-id issue/123 foo/bar
+----
+
+It is also possible to omit the trace ID and get a unique trace ID
+generated.
 
 ----
   $ ssh -p 29418 review.example.com gerrit create-project --trace foo/bar
@@ -220,8 +228,8 @@
 
 Enabling tracing results in additional logs with debug information that
 are written to the `error_log`. All logs that correspond to the traced
-request are associated with a unique trace ID. This trace ID is printed
-to the stderr command output:
+request are associated with the trace ID. The trace ID is printed to
+the stderr command output:
 
 ----
   TRACE_ID: 1534174322774-7edf2a7b
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7f32488..0108a04 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3748,6 +3748,14 @@
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
 
+[[receive.inheritProjectMaxObjectSizeLimit]]receive.inheritProjectMaxObjectSizeLimit::
++
+Controls whether the project-level link:config-project-config.html[`receive.maxObjectSizeLimit`]
+value is inherited from the parent project. When `true`, the value is
+inherited, otherwise it is not inherited.
++
+Default is false, the value is not inherited.
+
 [[receive.maxTrustDepth]]receive.maxTrustDepth::
 +
 If signed push validation is link:#receive.enableSignedPush[enabled],
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 1aa6cd7c..ff43520 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -374,6 +374,15 @@
 link:access-control.html#reference[here], but must not contain `${username}` or
 `${shardeduserid}`.
 
+[[label_ignoreSelfApproval]]
+=== `label.Label-Name.ignoreSelfApproval`
+
+If true, the label may be voted on by the uploader of the latest patch set,
+but their approval does not make a change submittable. Instead, a
+non-uploader who has the right to vote has to approve the change.
+
+Defaults to false.
+
 [[label_example]]
 === Example
 
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index b652bda9..cc5386f 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -148,8 +148,10 @@
 than the global limit (if configured). In other words, it is only honored when
 it further reduces the global limit.
 +
-The setting is not inherited from the parent project; it must be explicitly
-set per project.
+When link:config-gerrit.html#receive.inheritProjectMaxObjectSizeLimit[
+`receive.inheritProjectmaxObjectSizeLimit`] is enabled in the global config,
+the value is inherited from the parent project. Otherwise, it is not inherited
+and must be explicitly set per project.
 +
 Default is zero.
 +
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index cc0fe60..bc9f782 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -368,6 +368,36 @@
 that they are vetted long enough before they go into a release and we can be sure
 that the update doesn't introduce a regression.
 
+[[deprecating-features]]
+=== Deprecating features
+
+Gerrit should be as stable as possible and we aim to add only features that last.
+However, sometimes we are required to deprecate and remove features to be able
+to move forward with the project and keep the code-base clean. The following process
+should serve as a guideline on how to deprecate functionality in Gerrit. Its purpose
+is that we have a structured process for deprecation that users, administrators and
+developers can agree and rely on.
+
+General process:
+* Make sure that the feature (e.g. a field on the API) is not needed anymore or blocks
+  further development or improvement. If in doubt, consult the mailing list.
+* If you can provide a schema migration that moves users to a comparable feature, do
+  so and stop here.
+* Mark the feature as deprecated in the documentation and release notes.
+* If possible, mark the feature deprecated in any user-visible interface. For example,
+  if you are deprecating a Git push option, add a message to the Git response if
+  the user provided the option informing them about deprecation.
+* Annotate the code with `@Deprecated` and `@RemoveAfter(x.xx)` if applicable.
+  Alternatively, use `// DEPRECATED, remove after x.xx` (where x.xx is the version
+  number that has to be branched off before removing the feature)
+* Gate the feature behind a config that is off by default (forcing admins to turn
+  the deprecated feature on explicitly).
+* After the next release was branched off, remove any code that backed the feature.
+
+You can optionally consult the mailing list to ask if there are users of the feature you
+wish to deprecate. If there are no major users, you can remove the feature without
+following this process and without the grace period of one release.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 6e39502..0f23db5 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -3,7 +3,7 @@
 This document is about configuring Gerrit Code Review into an
 Eclipse workspace for development and debugging with GWT.
 
-Java 6 or later SDK is also required to run GWT's compiler and
+Java 8 or later SDK is also required to run GWT's compiler and
 runtime debugging environment.
 
 
@@ -49,6 +49,19 @@
 link:dev-build-plugins.html#_bundle_custom_plugin_in_release_war[bundling in release.war]
 and run `tools/eclipse/project.py`.
 
+[[Newer Java versions]]
+
+Java 9 and later are supported, but some adjustments must be done, because
+Java 8 is still the default:
+
+* Add JRE, e.g.: directory: /usr/lib64/jvm/java-9-openjdk, name: java-9-openjdk-9
+* Change execution environemnt for gerrit project to: JavaSE-9 (java-9-openjdk-9)
+* Check that compiler compliance level in gerrit project is set to: 9
+* Add this parameter to VM argument for gerrit_daemin launcher:
+----
+  --add-modules java.activation \
+  --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED
+----
 
 [[Formatting]]
 == Code Formatter Settings
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 2afac94..0abd8a1 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -365,6 +365,17 @@
 must reference the new version. Upload a change to bazlets repository with
 api version upgrade.
 
+[[clean-up-on-master]]
+=== Clean up on master
+
+Once you are done with the release, check if there are any code changes in the
+master branch that were gated on the next release. Mostly, these are
+feature-deprecations that we were holding off on to have a stable release where
+the feature is still contained, but marked as deprecated.
+
+See link:dev-contributing.html#deprecating-features[Deprecating features] for
+details.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index 65a15ca..c2a7d21 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -263,6 +263,7 @@
       ],
       "can_upload": true,
       "can_add": true,
+      "can_add_tags": true,
       "config_visible": true,
       "groups": {
          "53a4f647a89ea57992571187d8025f830625192a": {
@@ -313,6 +314,7 @@
       ],
       "can_upload": true,
       "can_add": true,
+      "can_add_tags": true,
       "config_visible": true
     }
   }
@@ -399,6 +401,8 @@
 Whether the calling user can upload to any ref.
 |`can_add`            |not set if `false`|
 Whether the calling user can add any ref.
+|`can_add_tags`       |not set if `false`|
+Whether the calling user can add any tag ref.
 |`config_visible`     |not set if `false`|
 Whether the calling user can see the `refs/meta/config` branch of the
 project.
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index edb642e..e28a9c4 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -418,7 +418,10 @@
 .Response
 ----
   HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
 
+  )]}'
   ok
 ----
 
@@ -1095,7 +1098,10 @@
 .Response
 ----
   HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
 
+  )]}'
   ok
 ----
 
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 310ec7b..b517d3c 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1141,6 +1141,7 @@
     ],
     "can_upload": true,
     "can_add": true,
+    "can_add_tags": true,
     "config_visible": true,
     "groups": {
       "c2ce4749a32ceb82cd6adcce65b8216e12afb41c": {
@@ -1242,6 +1243,7 @@
     ],
     "can_upload": true,
     "can_add": true,
+    "can_add_tags": true,
     "config_visible": true,
     "groups": {
       "global:Anonymous-Users": {
@@ -3442,10 +3444,10 @@
 formatted string. +
 Not set if there is no limit for the object size configured on project
 level.
-|`inherited_value` |optional|
-The max object size limit that is inherited from the global config as a
-formatted string. +
-Not set if there is no global limit for the object size.
+|`summary`         |optional|
+A string describing whether the value was inherited or overridden from
+the parent project or global config. +
+Not set if not inherited or overridden.
 |===============================
 
 [[project-access-input]]
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index a9c1ca6..8f6a47b 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -193,18 +193,36 @@
 
 [[tracing]]
 === Request Tracing
-For each REST endpoint tracing can be enabled by setting the `trace`
-request parameter.
+For each REST endpoint tracing can be enabled by setting the
+`trace=<trace-id>` request parameter. It is recommended to use the ID
+of the issue that is being investigated as trace ID.
+
+.Example Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?trace=issue/123&q=J
+----
+
+It is also possible to omit the trace ID and get a unique trace ID
+generated.
 
 .Example Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?trace&q=J
 ----
 
+Alternatively request tracing can also be enabled by setting the
+`X-Gerrit-Trace` header:
+
+.Example Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?q=J
+  X-Gerrit-Trace: issue/123
+----
+
 Enabling tracing results in additional logs with debug information that
 are written to the `error_log`. All logs that correspond to the traced
-request are associated with a unique trace ID. This trace ID is
-returned with the REST response in the `X-Gerrit-Trace` header.
+request are associated with the trace ID. The trace ID is returned with
+the REST response in the `X-Gerrit-Trace` header.
 
 .Example Response
 ----
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index bb4c5e4..8430e97 100644
--- a/Documentation/user-request-tracing.txt
+++ b/Documentation/user-request-tracing.txt
@@ -11,10 +11,10 @@
 request type:
 
 * REST API: For REST calls tracing can be enabled by setting the
-  `trace` request parameter, the trace ID is returned as
-  `X-Gerrit-Trace` header. More information about this can be found in
-  the link:rest-api.html#tracing[Request Tracing] section of the
-  link:rest-api.html[REST API documentation].
+  `trace` request parameter or the `X-Gerrit-Trace` header, the trace
+  ID is returned as `X-Gerrit-Trace` header. More information about
+  this can be found in the link:rest-api.html#tracing[Request Tracing]
+  section of the link:rest-api.html[REST API documentation].
 * SSH API: For SSH calls tracing can be enabled by setting the
   `--trace` option. More information about this can be found in
   the link:cmd-index.html#trace[Trace] section of the
@@ -26,6 +26,16 @@
   link:user-upload.html[upload documentation]. Tracing for Git requests
   other than Git push is not supported.
 
+When request tracing is enabled it is possible to provide an ID that
+should be used as trace ID. If a trace ID is not provided a trace ID is
+automatically generated. The trace ID must be provided to the support
+team so that they can find the trace.
+
+When doing traces it is recommended to specify the ID of the issue
+that is being investigated as trace ID so that the traces of the issue
+can be found more easily. When the issue ID is used as trace ID there
+is no need to find the generated trace ID and report it in the issue.
+
 Since tracing consumes additional server resources tracing should only
 be enabled for single requests if there is a concrete need for
 debugging. In particular bots should never enable tracing for all their
@@ -44,3 +54,19 @@
 
 By doing a grep with the trace ID over the error log the log entries
 that correspond to the request can be found.
+
+== Which information is captured in a trace?
+
+* request details
+** REST API: request URL, request parameter names, calling user,
+   response code, response body on errors
+** SSH API: parameter names
+** Git API: push options, magic branch parameter names
+* cache misses, cache evictions
+* reads from NoteDb, writes to NoteDb
+* reads of meta data files, writes of meta data files
+* index queries (with parameters and matches)
+* reindex events
+* permission checks (e.g. which rule is responsible for a deny)
+* timer metrics
+* all other logs
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 78bbe99..751e886 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -424,17 +424,26 @@
 [[trace]]
 ==== Trace
 
-When pushing to Gerrit tracing can be enabled by setting the `trace`
-push option.
+When pushing to Gerrit tracing can be enabled by setting the
+`trace=<trace-id>` push option. It is recommended to use the ID of the
+issue that is being investigated as trace ID.
 
 ----
+  git push -o trace=issue/123 ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master
+----
+
+It is also possible to omit the trace ID and get a unique trace ID
+generated.
+
+.Example Request
+----
   git push -o trace ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master
 ----
 
 Enabling tracing results in additional logs with debug information that
 are written to the `error_log`. All logs that correspond to the traced
-request are associated with a unique trace ID. This trace ID is
-returned in the command output:
+request are associated with the trace ID. This trace ID is returned in
+the command output:
 
 ----
   remote: TRACE_ID: 1534174322774-7edf2a7b
diff --git a/WORKSPACE b/WORKSPACE
index 0a6caa2..dca68d3 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1038,8 +1038,8 @@
 
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.3.2",
-    sha1 = "2077ea5f00fdd2d6af85223b730ba8047303297f",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.4.0",
+    sha1 = "0eaa13decb9796eb671c5841d0770ae68b348da5",
 )
 
 JACKSON_VERSION = "2.8.9"
diff --git a/antlr3/BUILD b/antlr3/BUILD
index 6d7102a..fc96715 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -15,3 +15,17 @@
     ],
     visibility = ["//visibility:public"],
 )
+
+java_library(
+    name = "query_parser",
+    srcs = [":query"],
+    visibility = [
+        "//java/com/google/gerrit/index:__pkg__",
+        "//javatests/com/google/gerrit/index:__pkg__",
+        "//plugins:__pkg__",
+    ],
+    deps = [
+        "//java/com/google/gerrit/index:query_exception",
+        "//lib/antlr:java-runtime",
+    ],
+)
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
deleted file mode 100644
index c1e34dd..0000000
--- a/gerrit-acceptance-tests/tests.bzl
+++ /dev/null
@@ -1,21 +0,0 @@
-load("//tools/bzl:junit.bzl", "junit_tests")
-
-def acceptance_tests(
-        group,
-        deps = [],
-        labels = [],
-        vm_args = ["-Xmx256m"],
-        **kwargs):
-    junit_tests(
-        name = group,
-        deps = deps + [
-            "//gerrit-acceptance-tests:lib",
-        ],
-        tags = labels + [
-            "acceptance",
-            "slow",
-        ],
-        size = "large",
-        jvm_flags = vm_args,
-        **kwargs
-    )
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 866d74f..0f786a6 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.info;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
@@ -30,8 +32,6 @@
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -447,18 +447,8 @@
 
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
       final int editParent = findEditParent(list);
-      Collections.sort(
-          Natives.asList(list),
-          new Comparator<RevisionInfo>() {
-            @Override
-            public int compare(RevisionInfo a, RevisionInfo b) {
-              return num(a) - num(b);
-            }
-
-            private int num(RevisionInfo r) {
-              return !r.isEdit() ? 2 * (r._number() - 1) + 1 : 2 * editParent;
-            }
-          });
+      Natives.asList(list)
+          .sort(comparing(r -> !r.isEdit() ? 2 * (r._number() - 1) + 1 : 2 * editParent));
     }
 
     public static int findEditParent(JsArray<RevisionInfo> list) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
index 345a260..fc3dbf1 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
-import java.util.Collections;
 import java.util.Comparator;
 
 public class FileInfo extends JavaScriptObject {
@@ -55,8 +54,7 @@
   public final native void _row(int r) /*-{ this._row = r }-*/;
 
   public static void sortFileInfoByPath(JsArray<FileInfo> list) {
-    Collections.sort(
-        Natives.asList(list), Comparator.comparing(FileInfo::path, FilenameComparator.INSTANCE));
+    Natives.asList(list).sort(Comparator.comparing(FileInfo::path, FilenameComparator.INSTANCE));
   }
 
   public static String getFileName(String path) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
index 4b17068..41306ff 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.client.rpc;
 
+import static java.util.stream.Collectors.toCollection;
+
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -57,10 +58,7 @@
   }
 
   public final List<String> sortedKeys() {
-    Set<String> keys = keySet();
-    List<String> sorted = new ArrayList<>(keys);
-    Collections.sort(sorted);
-    return sorted;
+    return keySet().stream().sorted().collect(toCollection(ArrayList::new));
   }
 
   public final native JsArray<T> values() /*-{
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
index b115c7d..88635df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
@@ -19,6 +19,8 @@
 public class ProjectAccessInfo extends JavaScriptObject {
   public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/;
 
+  public final native boolean canAddTagRefs() /*-{ return this.can_add_tags ? true : false; }-*/;
+
   public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/;
 
   public final native boolean configVisible() /*-{ return this.config_visible ? true : false; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
index 0dc1dab..1a0090a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.account;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -40,8 +42,6 @@
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 public class MyGpgKeysScreen extends SettingsScreen {
@@ -118,14 +118,7 @@
                     List<GpgKeyInfo> list = Natives.asList(result.values());
                     // TODO(dborowitz): Sort on something more meaningful, like
                     // created date?
-                    Collections.sort(
-                        list,
-                        new Comparator<GpgKeyInfo>() {
-                          @Override
-                          public int compare(GpgKeyInfo a, GpgKeyInfo b) {
-                            return a.id().compareTo(b.id());
-                          }
-                        });
+                    list.sort(comparing(GpgKeyInfo::id));
                     keys.clear();
                     keyText.setText("");
                     errorPanel.setVisible(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index 5c6d40f..730d98e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.account;
 
+import static java.util.Comparator.naturalOrder;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -29,7 +31,6 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 
@@ -169,7 +170,7 @@
 
     void display(JsArray<ExternalIdInfo> results) {
       List<ExternalIdInfo> idList = Natives.asList(results);
-      Collections.sort(idList);
+      idList.sort(naturalOrder());
 
       while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index 80c6d1a..4fdd067 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -41,7 +41,7 @@
   // corresponding regular expressions in the
   // com.google.gerrit.server.account.externalids.ExternalId class.
   private static final String USER_NAME_PATTERN_FIRST_REGEX = "[a-zA-Z0-9]";
-  private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9._@-]";
+  private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9.!#$%&’*+=?^_`\\{|\\}~@-]";
 
   private CopyableLabel userNameLbl;
   private NpTextBox userNameTxt;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 1b946cd..7bd8b82 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.admin;
 
+import static java.util.stream.Collectors.toCollection;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.LabelType;
@@ -45,7 +47,6 @@
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.ValueListBox;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 public class AccessSectionEditor extends Composite
@@ -205,9 +206,8 @@
   }
 
   private void sortPermissions(AccessSection accessSection) {
-    List<Permission> permissionList = new ArrayList<>(accessSection.getPermissions());
-    Collections.sort(permissionList);
-    accessSection.setPermissions(permissionList);
+    accessSection.setPermissions(
+        accessSection.getPermissions().stream().sorted().collect(toCollection(ArrayList::new)));
   }
 
   void setEditing(boolean editing) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 2614224..6eaab5d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.admin;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
@@ -29,7 +31,6 @@
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -295,26 +296,9 @@
 
     void insert(AccountInfo info) {
       Comparator<AccountInfo> c =
-          new Comparator<AccountInfo>() {
-            @Override
-            public int compare(AccountInfo a, AccountInfo b) {
-              int cmp = nullToEmpty(a.name()).compareTo(nullToEmpty(b.name()));
-              if (cmp != 0) {
-                return cmp;
-              }
-
-              cmp = nullToEmpty(a.email()).compareTo(nullToEmpty(b.email()));
-              if (cmp != 0) {
-                return cmp;
-              }
-
-              return a._accountId() - b._accountId();
-            }
-
-            public String nullToEmpty(String str) {
-              return str == null ? "" : str;
-            }
-          };
+          comparing((AccountInfo a) -> nullToEmpty(a.name()))
+              .thenComparing(a -> nullToEmpty(a.email()))
+              .thenComparing(AccountInfo::_accountId);
       int insertPos = getInsertRow(c, info);
       if (insertPos >= 0) {
         table.insertRow(insertPos);
@@ -405,20 +389,7 @@
 
     void insert(GroupInfo info) {
       Comparator<GroupInfo> c =
-          new Comparator<GroupInfo>() {
-            @Override
-            public int compare(GroupInfo a, GroupInfo b) {
-              int cmp = nullToEmpty(a.name()).compareTo(nullToEmpty(b.name()));
-              if (cmp != 0) {
-                return cmp;
-              }
-              return a.getGroupUUID().compareTo(b.getGroupUUID());
-            }
-
-            private String nullToEmpty(@Nullable String str) {
-              return (str == null) ? "" : str;
-            }
-          };
+          comparing((GroupInfo g) -> nullToEmpty(g.name())).thenComparing(GroupInfo::getGroupUUID);
       int insertPos = getInsertRow(c, info);
       if (insertPos >= 0) {
         table.insertRow(insertPos);
@@ -457,4 +428,9 @@
       setRowItem(row, i);
     }
   }
+
+  // Like Guava's Strings#nullToEmpty, which can't be used in GWT UI code.
+  private static String nullToEmpty(String str) {
+    return str == null ? "" : str;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index fe27e9c..7b18a39 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -36,8 +36,6 @@
 
   String effectiveMaxObjectSizeLimit(String effectiveMaxObjectSizeLimit);
 
-  String globalMaxObjectSizeLimit(String globalMaxObjectSizeLimit);
-
   String noMaxObjectSizeLimit();
 
   String pluginProjectOptionsTitle(String pluginName);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index f746365..c9aa987 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -6,7 +6,6 @@
 deletedReference = Reference {0} was deleted
 deletedSection = Section {0} was deleted
 effectiveMaxObjectSizeLimit = effective: {0} bytes
-globalMaxObjectSizeLimit = The global max object size limit is set to {0}. The limit cannot be increased on project level.
 noMaxObjectSizeLimit = No max object size limit is set.
 pluginProjectOptionsTitle = {0} Plugin Options
 pluginProjectOptionsTitle = {0} Plugin
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 259847e..be0db41 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.admin;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.groups.GroupList;
@@ -32,8 +34,6 @@
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.Image;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 public class GroupTable extends NavigationTable<GroupInfo> {
@@ -105,14 +105,7 @@
       table.removeRow(table.getRowCount() - 1);
     }
 
-    Collections.sort(
-        list,
-        new Comparator<GroupInfo>() {
-          @Override
-          public int compare(GroupInfo a, GroupInfo b) {
-            return a.name().compareTo(b.name());
-          }
-        });
+    list.sort(comparing(GroupInfo::name));
     for (GroupInfo group : list.subList(fromIndex, toIndex)) {
       final int row = table.getRowCount();
       table.insertRow(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 05a29ac..d10a031 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -442,9 +442,8 @@
     if (result.maxObjectSizeLimit().value() != null) {
       effectiveMaxObjectSizeLimit.setText(
           AdminMessages.I.effectiveMaxObjectSizeLimit(result.maxObjectSizeLimit().value()));
-      if (result.maxObjectSizeLimit().inheritedValue() != null) {
-        effectiveMaxObjectSizeLimit.setTitle(
-            AdminMessages.I.globalMaxObjectSizeLimit(result.maxObjectSizeLimit().inheritedValue()));
+      if (result.maxObjectSizeLimit().summary() != null) {
+        effectiveMaxObjectSizeLimit.setTitle(result.maxObjectSizeLimit().summary());
       }
     } else {
       effectiveMaxObjectSizeLimit.setText(AdminMessages.I.noMaxObjectSizeLimit());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
index 18e4176..22c331d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -94,7 +94,7 @@
         new GerritCallback<ProjectAccessInfo>() {
           @Override
           public void onSuccess(ProjectAccessInfo result) {
-            addPanel.setVisible(result.canAddRefs());
+            addPanel.setVisible(result.canAddTagRefs());
           }
         });
     query = new Query(match).start(start).run();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 1f4820f..801a927 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.client.change;
 
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toCollection;
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.Util;
@@ -135,9 +139,12 @@
   }
 
   void set(ChangeInfo info) {
-    List<String> names = new ArrayList<>(info.labels());
+    List<String> names =
+        info.labels()
+            .stream()
+            .sorted()
+            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
     Set<Integer> removable = info.removableReviewerIds();
-    Collections.sort(names);
 
     resize(names.size(), 2);
 
@@ -197,8 +204,7 @@
   }
 
   private static List<Integer> sort(Set<Integer> keySet, int a, int b) {
-    List<Integer> r = new ArrayList<>(keySet);
-    Collections.sort(r);
+    List<Integer> r = keySet.stream().sorted().collect(toCollection(ArrayList::new));
     if (keySet.contains(a)) {
       r.remove(Integer.valueOf(a));
       r.add(0, a);
@@ -238,31 +244,32 @@
       Set<Integer> removable,
       String label,
       Map<Integer, VotableInfo> votable) {
-    List<AccountInfo> users = new ArrayList<>(in);
-    Collections.sort(
-        users,
-        new Comparator<AccountInfo>() {
-          @Override
-          public int compare(AccountInfo a, AccountInfo b) {
-            String as = name(a);
-            String bs = name(b);
-            if (as.isEmpty()) {
-              return 1;
-            } else if (bs.isEmpty()) {
-              return -1;
-            }
-            return as.compareTo(bs);
-          }
+    List<AccountInfo> users =
+        in.stream()
+            .sorted(
+                new Comparator<AccountInfo>() {
+                  @Override
+                  public int compare(AccountInfo a, AccountInfo b) {
+                    String as = name(a);
+                    String bs = name(b);
+                    if (as.isEmpty()) {
+                      return 1;
+                    } else if (bs.isEmpty()) {
+                      return -1;
+                    }
+                    return as.compareTo(bs);
+                  }
 
-          private String name(AccountInfo a) {
-            if (a.name() != null) {
-              return a.name();
-            } else if (a.email() != null) {
-              return a.email();
-            }
-            return "";
-          }
-        });
+                  private String name(AccountInfo a) {
+                    if (a.name() != null) {
+                      return a.name();
+                    } else if (a.email() != null) {
+                      return a.email();
+                    }
+                    return "";
+                  }
+                })
+            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 
     SafeHtmlBuilder html = new SafeHtmlBuilder();
     Iterator<? extends AccountInfo> itr = users.iterator();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index 0bbd614..8a1a2d5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -16,6 +16,8 @@
 
 import static com.google.gwt.event.dom.client.KeyCodes.KEY_ENTER;
 import static com.google.gwt.event.dom.client.KeyCodes.KEY_MAC_ENTER;
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
@@ -123,11 +125,15 @@
     this.lc = new LocalComments(project, psId.getParentKey());
     initWidget(uiBinder.createAndBindUi(this));
 
-    List<String> names = new ArrayList<>(permitted.keySet());
+    List<String> names =
+        permitted
+            .keySet()
+            .stream()
+            .sorted()
+            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
     if (names.isEmpty()) {
       UIObject.setVisible(labelsParent, false);
     } else {
-      Collections.sort(names);
       renderLabels(names, all, permitted);
     }
 
@@ -439,8 +445,11 @@
               clp, project, psId, Util.C.commitMessage(), copyPath(Patch.MERGE_LIST, l)));
     }
 
-    List<String> paths = new ArrayList<>(m.keySet());
-    Collections.sort(paths);
+    List<String> paths =
+        m.keySet()
+            .stream()
+            .sorted()
+            .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 
     for (String path : paths) {
       if (!Patch.isMagic(path)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index c6e4e2f..7ec1102 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.changes;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
 import com.google.gerrit.client.info.ChangeInfo;
@@ -176,7 +178,7 @@
       }
     }
 
-    Collections.sort(Natives.asList(out), outComparator());
+    Natives.asList(out).sort(outComparator());
 
     table.updateColumnsForLabels(wip, out, in, done);
     workInProgress.display(wip);
@@ -187,16 +189,7 @@
   }
 
   private Comparator<ChangeInfo> outComparator() {
-    return new Comparator<ChangeInfo>() {
-      @Override
-      public int compare(ChangeInfo a, ChangeInfo b) {
-        int cmp = a.created().compareTo(b.created());
-        if (cmp != 0) {
-          return cmp;
-        }
-        return a._number() - b._number();
-      }
-    };
+    return comparing(ChangeInfo::created).thenComparing(ChangeInfo::_number);
   }
 
   private boolean hasChanges(JsArray<ChangeList> result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index caea87e..4fda78b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -16,11 +16,13 @@
 
 import static com.google.gerrit.client.FormatUtil.relativeFormat;
 import static com.google.gerrit.client.FormatUtil.shortFormat;
+import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.info.ChangeInfo;
 import com.google.gerrit.client.info.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -45,6 +47,7 @@
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
@@ -185,17 +188,13 @@
   }
 
   public void updateColumnsForLabels(ChangeList... lists) {
-    labelNames = new ArrayList<>();
-    for (ChangeList list : lists) {
-      for (int i = 0; i < list.length(); i++) {
-        for (String name : list.get(i).labels()) {
-          if (!labelNames.contains(name)) {
-            labelNames.add(name);
-          }
-        }
-      }
-    }
-    Collections.sort(labelNames);
+    labelNames =
+        Arrays.stream(lists)
+            .flatMap(l -> Natives.asList(l).stream())
+            .flatMap(c -> c.labels().stream())
+            .distinct()
+            .sorted()
+            .collect(toList());
 
     int baseColumns = BASE_COLUMNS;
     if (baseColumns + labelNames.size() < columns) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
index 0e4ef4e..dcb9c01 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.dashboards;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -25,8 +27,6 @@
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -75,14 +75,7 @@
       table.removeRow(table.getRowCount() - 1);
     }
 
-    Collections.sort(
-        list,
-        new Comparator<DashboardInfo>() {
-          @Override
-          public int compare(DashboardInfo a, DashboardInfo b) {
-            return a.id().compareTo(b.id());
-          }
-        });
+    list.sort(comparing(DashboardInfo::id));
 
     String ref = null;
     for (DashboardInfo d : list) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index 533b745..2698584 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.diff;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.DiffObject;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentApi;
@@ -27,8 +29,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
-import java.util.Collections;
-import java.util.Comparator;
 
 /** Collection of published and draft comments loaded from the server. */
 class CommentsCollections {
@@ -158,14 +158,7 @@
       for (CommentInfo c : Natives.asList(in)) {
         c.path(path);
       }
-      Collections.sort(
-          Natives.asList(in),
-          new Comparator<CommentInfo>() {
-            @Override
-            public int compare(CommentInfo a, CommentInfo b) {
-              return a.updated().compareTo(b.updated());
-            }
-          });
+      Natives.asList(in).sort(comparing(CommentInfo::updated));
     }
     return in;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
index 1a662e2..98ad023 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.diff;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.diff.DiffInfo.Span;
 import com.google.gerrit.client.rpc.Natives;
@@ -227,12 +229,7 @@
 
   /** Diff chunks are ordered by their starting lines in CodeMirror */
   private Comparator<UnifiedDiffChunkInfo> getDiffChunkComparatorCmLine() {
-    return new Comparator<UnifiedDiffChunkInfo>() {
-      @Override
-      public int compare(UnifiedDiffChunkInfo o1, UnifiedDiffChunkInfo o2) {
-        return o1.getCmLine() - o2.getCmLine();
-      }
-    };
+    return comparing(UnifiedDiffChunkInfo::getCmLine);
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 4185ef3..684f8e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -175,10 +175,10 @@
   public static class MaxObjectSizeLimitInfo extends JavaScriptObject {
     public final native String value() /*-{ return this.value; }-*/;
 
-    public final native String inheritedValue() /*-{ return this.inherited_value; }-*/;
-
     public final native String configuredValue() /*-{ return this.configured_value }-*/;
 
+    public final native String summary() /*-{ return this.summary; }-*/;
+
     protected MaxObjectSizeLimitInfo() {}
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index ac89180..3576b12 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.client.ui;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.Image;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 public class ProjectsTable extends NavigationTable<ProjectInfo> {
@@ -69,14 +69,7 @@
     }
 
     List<ProjectInfo> list = Natives.asList(projects.values());
-    Collections.sort(
-        list,
-        new Comparator<ProjectInfo>() {
-          @Override
-          public int compare(ProjectInfo a, ProjectInfo b) {
-            return a.name().compareTo(b.name());
-          }
-        });
+    list.sort(comparing(ProjectInfo::name));
     for (ProjectInfo p : list.subList(fromIndex, toIndex)) {
       insert(table.getRowCount(), p);
     }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
index c6f113e..9df066d 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -14,6 +14,8 @@
 
 package net.codemirror.mode;
 
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -21,8 +23,6 @@
 import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.resources.client.DataResource;
 import com.google.gwt.safehtml.shared.SafeUri;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -242,14 +242,7 @@
         byMime.put(m.mode(), m);
       }
     }
-    Collections.sort(
-        Natives.asList(filtered),
-        new Comparator<ModeInfo>() {
-          @Override
-          public int compare(ModeInfo a, ModeInfo b) {
-            return a.name().toLowerCase().compareTo(b.name().toLowerCase());
-          }
-        });
+    Natives.asList(filtered).sort(comparing(m -> m.name().toLowerCase()));
     setAll(filtered);
   }
 
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 1d87880..69d603f 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1487,12 +1487,7 @@
     assertNotifyTo(expected.email, expected.fullName);
   }
 
-  protected void assertNotifyTo(
-      com.google.gerrit.acceptance.testsuite.account.TestAccount expected) {
-    assertNotifyTo(expected.preferredEmail().orElse(null), expected.fullname().orElse(null));
-  }
-
-  private void assertNotifyTo(String expectedEmail, String expectedFullname) {
+  protected void assertNotifyTo(String expectedEmail, String expectedFullname) {
     Address expectedAddress = new Address(expectedFullname, expectedEmail);
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
@@ -1506,11 +1501,6 @@
     assertNotifyCc(expected.emailAddress);
   }
 
-  protected void assertNotifyCc(
-      com.google.gerrit.acceptance.testsuite.account.TestAccount expected) {
-    assertNotifyCc(expected.preferredEmail().orElse(null), expected.fullname().orElse(null));
-  }
-
   protected void assertNotifyCc(String expectedEmail, String expectedFullname) {
     Address expectedAddress = new Address(expectedFullname, expectedEmail);
     assertNotifyCc(expectedAddress);
@@ -1533,13 +1523,10 @@
     assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
 
-  protected void assertNotifyBcc(
-      com.google.gerrit.acceptance.testsuite.account.TestAccount expected) {
+  protected void assertNotifyBcc(String expectedEmail, String expectedFullName) {
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt())
-        .containsExactly(
-            new Address(expected.fullname().orElse(null), expected.preferredEmail().orElse(null)));
+    assertThat(m.rcpt()).containsExactly(new Address(expectedFullName, expectedEmail));
     assertThat(m.headers().get("To").isEmpty()).isTrue();
     assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 218ee18..5654c35 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -63,6 +63,7 @@
 
     eventListenerRegistration =
         eventListeners.add(
+            "gerrit",
             new UserScopedEventListener() {
               @Override
               public void onEvent(Event e) {
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 6e5424c..582c7cb 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -24,6 +24,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
@@ -441,6 +443,7 @@
             bindConstant().annotatedWith(SshEnabled.class).to(daemon.getEnableSshd());
             bind(AccountCreator.class);
             bind(AccountOperations.class).to(AccountOperationsImpl.class);
+            bind(GroupOperations.class).to(GroupOperationsImpl.class);
             factory(PushOneCommit.Factory.class);
             install(InProcessProtocol.module());
             install(new NoSshModule());
diff --git a/java/com/google/gerrit/acceptance/HttpResponse.java b/java/com/google/gerrit/acceptance/HttpResponse.java
index b62e932..3e98d71 100644
--- a/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -14,11 +14,16 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.ByteBuffer;
+import java.util.Arrays;
 import org.apache.http.Header;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -34,7 +39,7 @@
 
   public Reader getReader() throws IllegalStateException, IOException {
     if (reader == null && response.getEntity() != null) {
-      reader = new InputStreamReader(response.getEntity().getContent());
+      reader = new InputStreamReader(response.getEntity().getContent(), UTF_8);
     }
     return reader;
   }
@@ -59,6 +64,13 @@
     return hdr != null ? hdr.getValue() : null;
   }
 
+  public ImmutableList<String> getHeaders(String name) {
+    return Arrays.asList(response.getHeaders(name))
+        .stream()
+        .map(Header::getValue)
+        .collect(toImmutableList());
+  }
+
   public boolean hasContent() {
     Preconditions.checkNotNull(response, "Response is not initialized.");
     return response.getEntity() != null;
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 27dae3b..52d7f28 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.jcraft.jsch.ChannelExec;
@@ -54,10 +55,10 @@
       InputStream err = channel.getErrStream();
       channel.connect();
 
-      Scanner s = new Scanner(err).useDelimiter("\\A");
+      Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
       error = s.hasNext() ? s.next() : null;
 
-      s = new Scanner(in).useDelimiter("\\A");
+      s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
       return s.hasNext() ? s.next() : "";
     } finally {
       channel.disconnect();
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
index 58a00d0..61b7599 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
@@ -42,7 +42,7 @@
    * <p>Example:
    *
    * <pre>
-   * TestAccount createdAccount = accountOperations
+   * Account.Id createdAccountId = accountOperations
    *     .newAccount()
    *     .username("janedoe")
    *     .preferredEmail("janedoe@example.com")
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index 94b511b..ebbcfe4 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -59,12 +59,12 @@
     return TestAccountCreation.builder(this::createAccount);
   }
 
-  private TestAccount createAccount(TestAccountCreation accountCreation) throws Exception {
+  private Account.Id createAccount(TestAccountCreation accountCreation) throws Exception {
     AccountsUpdate.AccountUpdater accountUpdater =
         (account, updateBuilder) ->
             fillBuilder(updateBuilder, accountCreation, account.getAccount().getId());
     AccountState createdAccount = createAccount(accountUpdater);
-    return toTestAccount(createdAccount);
+    return createdAccount.getAccount().getId();
   }
 
   private AccountState createAccount(AccountsUpdate.AccountUpdater accountUpdater)
@@ -85,17 +85,6 @@
     accountCreation.active().ifPresent(builder::setActive);
   }
 
-  private static TestAccount toTestAccount(AccountState accountState) {
-    Account createdAccount = accountState.getAccount();
-    return TestAccount.builder()
-        .accountId(createdAccount.getId())
-        .preferredEmail(Optional.ofNullable(createdAccount.getPreferredEmail()))
-        .fullname(Optional.ofNullable(createdAccount.getFullName()))
-        .username(accountState.getUserName())
-        .active(accountState.getAccount().isActive())
-        .build();
-  }
-
   private static InternalAccountUpdate.Builder setPreferredEmail(
       InternalAccountUpdate.Builder builder, Account.Id accountId, String preferredEmail) {
     return builder
@@ -133,6 +122,17 @@
       return toTestAccount(account);
     }
 
+    private TestAccount toTestAccount(AccountState accountState) {
+      Account account = accountState.getAccount();
+      return TestAccount.builder()
+          .accountId(account.getId())
+          .preferredEmail(Optional.ofNullable(account.getPreferredEmail()))
+          .fullname(Optional.ofNullable(account.getFullName()))
+          .username(accountState.getUserName())
+          .active(accountState.getAccount().isActive())
+          .build();
+    }
+
     @Override
     public TestAccountUpdate.Builder forUpdate() {
       return TestAccountUpdate.builder(this::updateAccount);
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
index a82d180..ab32409 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -16,6 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.reviewdb.client.Account;
 import java.util.Optional;
 
 @AutoValue
@@ -32,9 +33,9 @@
 
   public abstract Optional<Boolean> active();
 
-  abstract ThrowingFunction<TestAccountCreation, TestAccount> accountCreator();
+  abstract ThrowingFunction<TestAccountCreation, Account.Id> accountCreator();
 
-  public static Builder builder(ThrowingFunction<TestAccountCreation, TestAccount> accountCreator) {
+  public static Builder builder(ThrowingFunction<TestAccountCreation, Account.Id> accountCreator) {
     return new AutoValue_TestAccountCreation.Builder()
         .accountCreator(accountCreator)
         .httpPassword("http-pass");
@@ -83,11 +84,11 @@
     }
 
     abstract Builder accountCreator(
-        ThrowingFunction<TestAccountCreation, TestAccount> accountCreator);
+        ThrowingFunction<TestAccountCreation, Account.Id> accountCreator);
 
     abstract TestAccountCreation autoBuild();
 
-    public TestAccount create() throws Exception {
+    public Account.Id create() throws Exception {
       TestAccountCreation accountUpdate = autoBuild();
       return accountUpdate.accountCreator().apply(accountUpdate);
     }
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
new file mode 100644
index 0000000..f75ca2e
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperations.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.group;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/**
+ * An aggregation of operations on groups for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ *
+ * <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
+ * Hence, it cannot be used for testing those APIs.
+ */
+public interface GroupOperations {
+  /**
+   * Starts the fluent chain for querying or modifying a group. Please see the methods of {@link
+   * MoreGroupOperations} for details on possible operations.
+   *
+   * @return an aggregation of operations on a specific group
+   */
+  MoreGroupOperations group(AccountGroup.UUID groupUuid);
+
+  /**
+   * Starts the fluent chain to create a group. The returned builder can be used to specify the
+   * attributes of the new group. To create the group for real, {@link
+   * TestGroupCreation.Builder#create()} must be called.
+   *
+   * <p>Example:
+   *
+   * <pre>
+   * AccountGroup.UUID createdGroupUuid = groupOperations
+   *     .newGroup()
+   *     .name("verifiers")
+   *     .description("All verifiers of this server")
+   *     .create();
+   * </pre>
+   *
+   * <p><strong>Note:</strong> If another group with the provided name already exists, the creation
+   * of the group will fail.
+   *
+   * @return a builder to create the new group
+   */
+  TestGroupCreation.Builder newGroup();
+
+  /** An aggregation of methods on a specific group. */
+  interface MoreGroupOperations {
+
+    /**
+     * Checks whether the group exists.
+     *
+     * @return {@code true} if the group exists
+     */
+    boolean exists() throws Exception;
+
+    /**
+     * Retrieves the group.
+     *
+     * <p><strong>Note:</strong> This call will fail with an exception if the requested group
+     * doesn't exist. If you want to check for the existence of a group, use {@link #exists()}
+     * instead.
+     *
+     * @return the corresponding {@code TestGroup}
+     */
+    TestGroup get() throws Exception;
+
+    /**
+     * Starts the fluent chain to update a group. The returned builder can be used to specify how
+     * the attributes of the group should be modified. To update the group for real, {@link
+     * TestGroupUpdate.Builder#update()} must be called.
+     *
+     * <p>Example:
+     *
+     * <pre>
+     * groupOperations.forUpdate().description("Another description for this group").update();
+     * </pre>
+     *
+     * <p><strong>Note:</strong> The update will fail with an exception if the group to update
+     * doesn't exist. If you want to check for the existence of a group, use {@link #exists()}.
+     *
+     * @return a builder to update the group
+     */
+    TestGroupUpdate.Builder forUpdate();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
new file mode 100644
index 0000000..f9769c5
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImpl.java
@@ -0,0 +1,159 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.group;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.server.group.db.InternalGroupCreation;
+import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+
+/**
+ * The implementation of {@code GroupOperations}.
+ *
+ * <p>There is only one implementation of {@code GroupOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class GroupOperationsImpl implements GroupOperations {
+  private final Groups groups;
+  private final GroupsUpdate groupsUpdate;
+  private final Sequences seq;
+  private final PersonIdent serverIdent;
+
+  @Inject
+  public GroupOperationsImpl(
+      Groups groups,
+      @ServerInitiated GroupsUpdate groupsUpdate,
+      Sequences seq,
+      @GerritPersonIdent PersonIdent serverIdent) {
+    this.groups = groups;
+    this.groupsUpdate = groupsUpdate;
+    this.seq = seq;
+    this.serverIdent = serverIdent;
+  }
+
+  @Override
+  public MoreGroupOperations group(AccountGroup.UUID groupUuid) {
+    return new MoreGroupOperationsImpl(groupUuid);
+  }
+
+  @Override
+  public TestGroupCreation.Builder newGroup() {
+    return TestGroupCreation.builder(this::createNewGroup);
+  }
+
+  private AccountGroup.UUID createNewGroup(TestGroupCreation groupCreation)
+      throws ConfigInvalidException, IOException, OrmException {
+    InternalGroupCreation internalGroupCreation = toInternalGroupCreation(groupCreation);
+    InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupCreation);
+    InternalGroup internalGroup =
+        groupsUpdate.createGroup(internalGroupCreation, internalGroupUpdate);
+    return internalGroup.getGroupUUID();
+  }
+
+  private InternalGroupCreation toInternalGroupCreation(TestGroupCreation groupCreation)
+      throws OrmException {
+    AccountGroup.Id groupId = new AccountGroup.Id(seq.nextGroupId());
+    String groupName = groupCreation.name().orElse("group-with-id-" + groupId.get());
+    AccountGroup.UUID groupUuid = GroupUUID.make(groupName, serverIdent);
+    AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
+    return InternalGroupCreation.builder()
+        .setId(groupId)
+        .setGroupUUID(groupUuid)
+        .setNameKey(nameKey)
+        .build();
+  }
+
+  private static InternalGroupUpdate toInternalGroupUpdate(TestGroupCreation groupCreation) {
+    InternalGroupUpdate.Builder builder = InternalGroupUpdate.builder();
+    groupCreation.description().ifPresent(builder::setDescription);
+    groupCreation.ownerGroupUuid().ifPresent(builder::setOwnerGroupUUID);
+    groupCreation.visibleToAll().ifPresent(builder::setVisibleToAll);
+    builder.setMemberModification(originalMembers -> groupCreation.members());
+    builder.setSubgroupModification(originalSubgroups -> groupCreation.subgroups());
+    return builder.build();
+  }
+
+  private class MoreGroupOperationsImpl implements MoreGroupOperations {
+    private final AccountGroup.UUID groupUuid;
+
+    MoreGroupOperationsImpl(AccountGroup.UUID groupUuid) {
+      this.groupUuid = groupUuid;
+    }
+
+    @Override
+    public boolean exists() throws Exception {
+      return groups.getGroup(groupUuid).isPresent();
+    }
+
+    @Override
+    public TestGroup get() throws Exception {
+      Optional<InternalGroup> group = groups.getGroup(groupUuid);
+      checkState(group.isPresent(), "Tried to get non-existing test group");
+      return toTestGroup(group.get());
+    }
+
+    private TestGroup toTestGroup(InternalGroup internalGroup) {
+      return TestGroup.builder()
+          .groupUuid(internalGroup.getGroupUUID())
+          .groupId(internalGroup.getId())
+          .nameKey(internalGroup.getNameKey())
+          .description(Optional.ofNullable(internalGroup.getDescription()))
+          .ownerGroupUuid(internalGroup.getOwnerGroupUUID())
+          .visibleToAll(internalGroup.isVisibleToAll())
+          .createdOn(internalGroup.getCreatedOn())
+          .members(internalGroup.getMembers())
+          .subgroups(internalGroup.getSubgroups())
+          .build();
+    }
+
+    @Override
+    public TestGroupUpdate.Builder forUpdate() {
+      return TestGroupUpdate.builder(this::updateGroup);
+    }
+
+    private void updateGroup(TestGroupUpdate groupUpdate)
+        throws OrmDuplicateKeyException, NoSuchGroupException, ConfigInvalidException, IOException {
+      InternalGroupUpdate internalGroupUpdate = toInternalGroupUpdate(groupUpdate);
+      groupsUpdate.updateGroup(groupUuid, internalGroupUpdate);
+    }
+
+    private InternalGroupUpdate toInternalGroupUpdate(TestGroupUpdate groupUpdate) {
+      InternalGroupUpdate.Builder builder = InternalGroupUpdate.builder();
+      groupUpdate.name().map(AccountGroup.NameKey::new).ifPresent(builder::setName);
+      groupUpdate.description().ifPresent(builder::setDescription);
+      groupUpdate.ownerGroupUuid().ifPresent(builder::setOwnerGroupUUID);
+      groupUpdate.visibleToAll().ifPresent(builder::setVisibleToAll);
+      builder.setMemberModification(groupUpdate.memberModification()::apply);
+      builder.setSubgroupModification(groupUpdate.subgroupModification()::apply);
+      return builder.build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
new file mode 100644
index 0000000..b450304
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.group;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestGroup {
+
+  public abstract AccountGroup.UUID groupUuid();
+
+  public abstract AccountGroup.Id groupId();
+
+  public String name() {
+    return nameKey().get();
+  }
+
+  public abstract AccountGroup.NameKey nameKey();
+
+  public abstract Optional<String> description();
+
+  public abstract AccountGroup.UUID ownerGroupUuid();
+
+  public abstract boolean visibleToAll();
+
+  public abstract Timestamp createdOn();
+
+  public abstract ImmutableSet<Account.Id> members();
+
+  public abstract ImmutableSet<AccountGroup.UUID> subgroups();
+
+  static Builder builder() {
+    return new AutoValue_TestGroup.Builder();
+  }
+
+  @AutoValue.Builder
+  abstract static class Builder {
+
+    public abstract Builder groupUuid(AccountGroup.UUID groupUuid);
+
+    public abstract Builder groupId(AccountGroup.Id id);
+
+    public abstract Builder nameKey(AccountGroup.NameKey name);
+
+    public abstract Builder description(String description);
+
+    public abstract Builder description(Optional<String> description);
+
+    public abstract Builder ownerGroupUuid(AccountGroup.UUID ownerGroupUuid);
+
+    public abstract Builder visibleToAll(boolean visibleToAll);
+
+    public abstract Builder createdOn(Timestamp createdOn);
+
+    public abstract Builder members(ImmutableSet<Account.Id> members);
+
+    public abstract Builder subgroups(ImmutableSet<AccountGroup.UUID> subgroups);
+
+    abstract TestGroup build();
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
new file mode 100644
index 0000000..efed720
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.group;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.util.Optional;
+import java.util.Set;
+
+@AutoValue
+public abstract class TestGroupCreation {
+
+  public abstract Optional<String> name();
+
+  public abstract Optional<String> description();
+
+  public abstract Optional<AccountGroup.UUID> ownerGroupUuid();
+
+  public abstract Optional<Boolean> visibleToAll();
+
+  public abstract ImmutableSet<Account.Id> members();
+
+  public abstract ImmutableSet<AccountGroup.UUID> subgroups();
+
+  abstract ThrowingFunction<TestGroupCreation, AccountGroup.UUID> groupCreator();
+
+  public static Builder builder(
+      ThrowingFunction<TestGroupCreation, AccountGroup.UUID> groupCreator) {
+    return new AutoValue_TestGroupCreation.Builder().groupCreator(groupCreator);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder name(String name);
+
+    public abstract Builder description(String description);
+
+    public Builder clearDescription() {
+      return description("");
+    }
+
+    public abstract Builder ownerGroupUuid(AccountGroup.UUID ownerGroupUuid);
+
+    public abstract Builder visibleToAll(boolean visibleToAll);
+
+    public Builder clearMembers() {
+      return members(ImmutableSet.of());
+    }
+
+    public Builder members(Account.Id member1, Account.Id... otherMembers) {
+      return members(Sets.union(ImmutableSet.of(member1), ImmutableSet.copyOf(otherMembers)));
+    }
+
+    public abstract Builder members(Set<Account.Id> members);
+
+    abstract ImmutableSet.Builder<Account.Id> membersBuilder();
+
+    public Builder addMember(Account.Id member) {
+      membersBuilder().add(member);
+      return this;
+    }
+
+    public Builder clearSubgroups() {
+      return subgroups(ImmutableSet.of());
+    }
+
+    public Builder subgroups(AccountGroup.UUID subgroup1, AccountGroup.UUID... otherSubgroups) {
+      return subgroups(Sets.union(ImmutableSet.of(subgroup1), ImmutableSet.copyOf(otherSubgroups)));
+    }
+
+    public abstract Builder subgroups(Set<AccountGroup.UUID> subgroups);
+
+    abstract ImmutableSet.Builder<AccountGroup.UUID> subgroupsBuilder();
+
+    public Builder addSubgroup(AccountGroup.UUID subgroup) {
+      subgroupsBuilder().add(subgroup);
+      return this;
+    }
+
+    abstract Builder groupCreator(
+        ThrowingFunction<TestGroupCreation, AccountGroup.UUID> groupCreator);
+
+    abstract TestGroupCreation autoBuild();
+
+    /**
+     * Executes the group creation as specified.
+     *
+     * @return the UUID of the created group
+     */
+    public AccountGroup.UUID create() throws Exception {
+      TestGroupCreation groupCreation = autoBuild();
+      return groupCreation.groupCreator().apply(groupCreation);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
new file mode 100644
index 0000000..095a270
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupUpdate.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.group;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+@AutoValue
+public abstract class TestGroupUpdate {
+
+  public abstract Optional<String> name();
+
+  public abstract Optional<String> description();
+
+  public abstract Optional<AccountGroup.UUID> ownerGroupUuid();
+
+  public abstract Optional<Boolean> visibleToAll();
+
+  public abstract Function<ImmutableSet<Account.Id>, Set<Account.Id>> memberModification();
+
+  public abstract Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>>
+      subgroupModification();
+
+  abstract ThrowingConsumer<TestGroupUpdate> groupUpdater();
+
+  public static Builder builder(ThrowingConsumer<TestGroupUpdate> groupUpdater) {
+    return new AutoValue_TestGroupUpdate.Builder()
+        .groupUpdater(groupUpdater)
+        .memberModification(in -> in)
+        .subgroupModification(in -> in);
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder name(String name);
+
+    public abstract Builder description(String description);
+
+    public Builder clearDescription() {
+      return description("");
+    }
+
+    public abstract Builder ownerGroupUuid(AccountGroup.UUID ownerGroupUUID);
+
+    public abstract Builder visibleToAll(boolean visibleToAll);
+
+    abstract Builder memberModification(
+        Function<ImmutableSet<Account.Id>, Set<Account.Id>> memberModification);
+
+    abstract Function<ImmutableSet<Account.Id>, Set<Account.Id>> memberModification();
+
+    public Builder clearMembers() {
+      return memberModification(originalMembers -> ImmutableSet.of());
+    }
+
+    public Builder addMember(Account.Id member) {
+      Function<ImmutableSet<Account.Id>, Set<Account.Id>> previousModification =
+          memberModification();
+      memberModification(
+          originalMembers ->
+              Sets.union(previousModification.apply(originalMembers), ImmutableSet.of(member)));
+      return this;
+    }
+
+    public Builder removeMember(Account.Id member) {
+      Function<ImmutableSet<Account.Id>, Set<Account.Id>> previousModification =
+          memberModification();
+      memberModification(
+          originalMembers ->
+              Sets.difference(
+                  previousModification.apply(originalMembers), ImmutableSet.of(member)));
+      return this;
+    }
+
+    abstract Builder subgroupModification(
+        Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> subgroupModification);
+
+    abstract Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>>
+        subgroupModification();
+
+    public Builder clearSubgroups() {
+      return subgroupModification(originalMembers -> ImmutableSet.of());
+    }
+
+    public Builder addSubgroup(AccountGroup.UUID subgroup) {
+      Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> previousModification =
+          subgroupModification();
+      subgroupModification(
+          originalSubgroups ->
+              Sets.union(previousModification.apply(originalSubgroups), ImmutableSet.of(subgroup)));
+      return this;
+    }
+
+    public Builder removeSubgroup(AccountGroup.UUID subgroup) {
+      Function<ImmutableSet<AccountGroup.UUID>, Set<AccountGroup.UUID>> previousModification =
+          subgroupModification();
+      subgroupModification(
+          originalSubgroups ->
+              Sets.difference(
+                  previousModification.apply(originalSubgroups), ImmutableSet.of(subgroup)));
+      return this;
+    }
+
+    abstract Builder groupUpdater(ThrowingConsumer<TestGroupUpdate> groupUpdater);
+
+    abstract TestGroupUpdate autoBuild();
+
+    /** Executes the group update as specified. */
+    public void update() throws Exception {
+      TestGroupUpdate groupUpdater = autoBuild();
+      groupUpdater.groupUpdater().accept(groupUpdater);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index 29fc9c7..ff7d25b 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -17,13 +17,13 @@
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.collectingAndThen;
 import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toMap;
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -36,6 +36,7 @@
   public static final boolean DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = false;
   public static final boolean DEF_COPY_MAX_SCORE = false;
   public static final boolean DEF_COPY_MIN_SCORE = false;
+  public static final boolean DEF_IGNORE_SELF_APPROVAL = false;
 
   public static LabelType withDefaultValues(String name) {
     checkName(name);
@@ -103,6 +104,7 @@
   protected boolean copyAllScoresIfNoCodeChange;
   protected boolean copyAllScoresIfNoChange;
   protected boolean allowPostSubmit;
+  protected boolean ignoreSelfApproval;
   protected short defaultValue;
 
   protected List<LabelValue> values;
@@ -141,13 +143,12 @@
     setCopyMaxScore(DEF_COPY_MAX_SCORE);
     setCopyMinScore(DEF_COPY_MIN_SCORE);
     setAllowPostSubmit(DEF_ALLOW_POST_SUBMIT);
+    setIgnoreSelfApproval(DEF_IGNORE_SELF_APPROVAL);
 
-    byValue =
-        values
-            .stream()
-            .collect(
-                collectingAndThen(
-                    toMap(LabelValue::getValue, v -> v), Collections::unmodifiableMap));
+    byValue = new HashMap<>();
+    for (LabelValue v : values) {
+      byValue.put(v.getValue(), v);
+    }
   }
 
   public String getName() {
@@ -190,6 +191,14 @@
     this.allowPostSubmit = allowPostSubmit;
   }
 
+  public boolean ignoreSelfApproval() {
+    return ignoreSelfApproval;
+  }
+
+  public void setIgnoreSelfApproval(boolean ignoreSelfApproval) {
+    this.ignoreSelfApproval = ignoreSelfApproval;
+  }
+
   public void setRefPatterns(List<String> refPatterns) {
     if (refPatterns != null) {
       this.refPatterns =
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 630594f..2c1c93a 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -71,6 +71,7 @@
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.ContentType;
 import org.apache.http.nio.entity.NStringEntity;
+import org.elasticsearch.client.Request;
 import org.elasticsearch.client.Response;
 
 abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
@@ -171,10 +172,10 @@
   public void deleteAll() throws IOException {
     // Delete the index, if it exists.
     String endpoint = indexName + client.adapter().indicesExistParam();
-    Response response = client.get().performRequest("HEAD", endpoint);
+    Response response = client.get().performRequest(new Request("HEAD", endpoint));
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode == HttpStatus.SC_OK) {
-      response = client.get().performRequest("DELETE", indexName);
+      response = client.get().performRequest(new Request("DELETE", indexName));
       statusCode = response.getStatusLine().getStatusCode();
       if (statusCode != HttpStatus.SC_OK) {
         throw new IOException(
@@ -307,9 +308,13 @@
 
   private Response performRequest(
       String method, Object payload, String uri, Map<String, String> params) throws IOException {
+    Request request = new Request(method, uri);
     String payloadStr = payload instanceof String ? (String) payload : payload.toString();
-    HttpEntity entity = new NStringEntity(payloadStr, ContentType.APPLICATION_JSON);
-    return client.get().performRequest(method, uri, params, entity);
+    request.setEntity(new NStringEntity(payloadStr, ContentType.APPLICATION_JSON));
+    for (Map.Entry<String, String> entry : params.entrySet()) {
+      request.addParameter(entry.getKey(), entry.getValue());
+    }
+    return client.get().performRequest(request);
   }
 
   protected class ElasticQuerySource implements DataSource<V> {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
index e36ab2d..a777f47 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
@@ -24,7 +24,7 @@
 import java.util.List;
 import org.apache.http.HttpStatus;
 import org.apache.http.StatusLine;
-import org.apache.http.client.methods.HttpGet;
+import org.elasticsearch.client.Request;
 import org.elasticsearch.client.Response;
 
 @Singleton
@@ -40,10 +40,8 @@
 
   List<String> discover(String prefix, String indexName) throws IOException {
     String name = prefix + indexName + "_";
-    Response response =
-        client
-            .get()
-            .performRequest(HttpGet.METHOD_NAME, client.adapter().getVersionDiscoveryUrl(name));
+    Request request = new Request("GET", client.adapter().getVersionDiscoveryUrl(name));
+    Response response = client.get().performRequest(request);
 
     StatusLine statusLine = response.getStatusLine();
     if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
index 2beb528..8cb69e0 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryAdapter.java
@@ -32,13 +32,14 @@
 
   ElasticQueryAdapter(ElasticVersion version) {
     this.ignoreUnmapped = version == ElasticVersion.V2_4;
-    this.usePostV5Type = isV6(version);
-    this.versionDiscoveryUrl = isV6(version) ? "%s*" : "%s*/_aliases";
+    this.usePostV5Type = version.isV6();
+    this.versionDiscoveryUrl = version.isV6() ? "%s*" : "%s*/_aliases";
 
     switch (version) {
       case V5_6:
       case V6_2:
       case V6_3:
+      case V6_4:
         this.searchFilteringName = "_source";
         this.indicesExistParam = "?allow_no_indices=false";
         this.exactFieldType = "keyword";
@@ -58,10 +59,6 @@
     }
   }
 
-  private boolean isV6(ElasticVersion version) {
-    return version == ElasticVersion.V6_2 || version == ElasticVersion.V6_3;
-  }
-
   void setIgnoreUnmapped(JsonObject properties) {
     if (ignoreUnmapped) {
       properties.addProperty("ignore_unmapped", true);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
index 6ceb897..337f2ca 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticRestClientProvider.java
@@ -29,6 +29,7 @@
 import org.apache.http.client.CredentialsProvider;
 import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
+import org.elasticsearch.client.Request;
 import org.elasticsearch.client.Response;
 import org.elasticsearch.client.RestClient;
 import org.elasticsearch.client.RestClientBuilder;
@@ -105,7 +106,7 @@
 
   private ElasticVersion getVersion() throws ElasticException {
     try {
-      Response response = client.performRequest("GET", "");
+      Response response = client.performRequest(new Request("GET", ""));
       StatusLine statusLine = response.getStatusLine();
       if (statusLine.getStatusCode() != HttpStatus.SC_OK) {
         throw new FailedToGetVersion(statusLine);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 610a212..dfa5d21 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -21,7 +21,8 @@
   V2_4("2.4.*"),
   V5_6("5.6.*"),
   V6_2("6.2.*"),
-  V6_3("6.3.*");
+  V6_3("6.3.*"),
+  V6_4("6.4.*");
 
   private final String version;
   private final Pattern pattern;
@@ -31,29 +32,33 @@
     this.pattern = Pattern.compile(version);
   }
 
-  public static class InvalidVersion extends ElasticException {
+  public static class UnsupportedVersion extends ElasticException {
     private static final long serialVersionUID = 1L;
 
-    InvalidVersion(String version) {
+    UnsupportedVersion(String version) {
       super(
           String.format(
-              "Invalid version: [%s]. Supported versions: %s", version, supportedVersions()));
+              "Unsupported version: [%s]. Supported versions: %s", version, supportedVersions()));
     }
   }
 
-  public static ElasticVersion forVersion(String version) throws InvalidVersion {
+  public static ElasticVersion forVersion(String version) throws UnsupportedVersion {
     for (ElasticVersion value : ElasticVersion.values()) {
       if (value.pattern.matcher(version).matches()) {
         return value;
       }
     }
-    throw new InvalidVersion(version);
+    throw new UnsupportedVersion(version);
   }
 
   public static String supportedVersions() {
     return Joiner.on(", ").join(ElasticVersion.values());
   }
 
+  public boolean isV6() {
+    return version.startsWith("6.");
+  }
+
   @Override
   public String toString() {
     return version;
diff --git a/java/com/google/gerrit/extensions/annotations/RemoveAfter.java b/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
new file mode 100644
index 0000000..aa31dd0
--- /dev/null
+++ b/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for features that are deprecated, but still present to adhere to the one-release-grace
+ * period we promised to users.
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
+@Retention(SOURCE)
+@BindingAnnotation
+public @interface RemoveAfter {
+  /**
+   * Version after which the annotated functionality can be removed. Once the referenced version was
+   * branched off, the annotated code can be removed.
+   */
+  String value();
+}
diff --git a/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java b/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java
index 5d8e950..8273d84 100644
--- a/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java
+++ b/java/com/google/gerrit/extensions/api/access/ProjectAccessInfo.java
@@ -29,6 +29,7 @@
   public Set<String> ownerOf;
   public Boolean canUpload;
   public Boolean canAdd;
+  public Boolean canAddTags;
   public Boolean configVisible;
   public Map<String, GroupInfo> groups;
   public List<WebLinkInfo> configWebLinks;
diff --git a/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java b/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java
index d876034..8fe47bd 100644
--- a/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java
+++ b/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.extensions.api.changes;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -23,9 +24,11 @@
   public Map<String, Collection<String>> external;
 
   public IncludedInInfo(
-      List<String> branches, List<String> tags, Map<String, Collection<String>> external) {
-    this.branches = branches;
-    this.tags = tags;
+      Collection<String> branches,
+      Collection<String> tags,
+      Map<String, Collection<String>> external) {
+    this.branches = new ArrayList<>(branches);
+    this.tags = new ArrayList<>(tags);
     this.external = external;
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index b5aff67..08ba486 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -59,14 +59,17 @@
   }
 
   public static class MaxObjectSizeLimitInfo {
-    /* The effective value. Null if not set. */
+    /** The effective value in bytes. Null if not set. */
     @Nullable public String value;
 
-    /* The value configured on the project. Null if not set. */
+    /** The value configured explicitly on the project as a formatted string. Null if not set. */
     @Nullable public String configuredValue;
 
-    /* The value configured globally. Null if not set. */
-    @Nullable public String inheritedValue;
+    /**
+     * Whether the value was inherited or overridden from the project's parent hierarchy or global
+     * config. Null if not inherited or overridden.
+     */
+    @Nullable public String summary;
   }
 
   public static class ConfigParameterInfo {
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItem.java b/java/com/google/gerrit/extensions/registration/DynamicItem.java
index b5eafe3..4f36ab4 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.registration;
 
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -77,7 +78,8 @@
    * @param item item to store.
    */
   public static <T> DynamicItem<T> itemOf(Class<T> member, T item) {
-    return new DynamicItem<>(keyFor(TypeLiteral.get(member)), Providers.of(item), "gerrit");
+    return new DynamicItem<>(
+        keyFor(TypeLiteral.get(member)), Providers.of(item), PluginName.GERRIT);
   }
 
   @SuppressWarnings("unchecked")
@@ -126,12 +128,26 @@
    * @return the configured item instance; null if no implementation has been bound to the item.
    *     This is common if no plugin registered an implementation for the type.
    */
+  @Nullable
   public T get() {
     NamedProvider<T> item = ref.get();
     return item != null ? item.impl.get() : null;
   }
 
   /**
+   * Get the name of the plugin that has bound the configured item, or null.
+   *
+   * @return the name of the plugin that has bound the configured item; null if no implementation
+   *     has been bound to the item. This is common if no plugin registered an implementation for
+   *     the type.
+   */
+  @Nullable
+  public String getPluginName() {
+    NamedProvider<T> item = ref.get();
+    return item != null ? item.pluginName : null;
+  }
+
+  /**
    * Set the element to provide.
    *
    * @param item the item to use. Must not be null.
@@ -154,7 +170,7 @@
     NamedProvider<T> old = null;
     while (!ref.compareAndSet(old, item)) {
       old = ref.get();
-      if (old != null && !"gerrit".equals(old.pluginName)) {
+      if (old != null && !PluginName.GERRIT.equals(old.pluginName)) {
         throw new ProvisionException(
             String.format(
                 "%s already provided by %s, ignoring plugin %s",
@@ -186,7 +202,9 @@
     NamedProvider<T> old = null;
     while (!ref.compareAndSet(old, item)) {
       old = ref.get();
-      if (old != null && !"gerrit".equals(old.pluginName) && !pluginName.equals(old.pluginName)) {
+      if (old != null
+          && !PluginName.GERRIT.equals(old.pluginName)
+          && !pluginName.equals(old.pluginName)) {
         // We allow to replace:
         // 1. Gerrit core items, e.g. websession cache
         //    can be replaced by plugin implementation
@@ -222,6 +240,7 @@
     }
 
     @Override
+    @Nullable
     public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
       NamedProvider<T> n = new NamedProvider<>(newItem, item.pluginName);
       if (ref.compareAndSet(item, n)) {
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
index 5b76741..d8dd1f9 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
@@ -36,7 +36,7 @@
 
   @Override
   public DynamicItem<T> get() {
-    return new DynamicItem<>(key, find(injector, type), "gerrit");
+    return new DynamicItem<>(key, find(injector, type), PluginName.GERRIT);
   }
 
   private static <T> Provider<T> find(Injector src, TypeLiteral<T> type) {
diff --git a/java/com/google/gerrit/extensions/registration/DynamicMap.java b/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 0bf6edd..96d19b2 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -41,12 +41,26 @@
  * singleton and non-singleton members.
  */
 public abstract class DynamicMap<T> implements Iterable<DynamicMap.Entry<T>> {
-  public interface Entry<T> {
-    String getPluginName();
+  public static class Entry<T> {
+    private final NamePair namePair;
+    private final Provider<T> provider;
 
-    String getExportName();
+    private Entry(NamePair namePair, Provider<T> provider) {
+      this.namePair = namePair;
+      this.provider = provider;
+    }
 
-    Provider<T> getProvider();
+    public String getPluginName() {
+      return namePair.pluginName;
+    }
+
+    public String getExportName() {
+      return namePair.exportName;
+    }
+
+    public Provider<T> getProvider() {
+      return provider;
+    }
   }
 
   /**
@@ -162,23 +176,8 @@
 
       @Override
       public Entry<T> next() {
-        final Map.Entry<NamePair, Provider<T>> e = i.next();
-        return new Entry<T>() {
-          @Override
-          public String getPluginName() {
-            return e.getKey().pluginName;
-          }
-
-          @Override
-          public String getExportName() {
-            return e.getKey().exportName;
-          }
-
-          @Override
-          public Provider<T> getProvider() {
-            return e.getValue();
-          }
-        };
+        Map.Entry<NamePair, Provider<T>> e = i.next();
+        return new Entry<>(e.getKey(), e.getValue());
       }
 
       @Override
diff --git a/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
index 420a356..9d96131 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -37,7 +37,7 @@
     if (bindings != null) {
       for (Binding<T> b : bindings) {
         if (b.getKey().getAnnotation() != null) {
-          m.put("gerrit", b.getKey(), b.getProvider());
+          m.put(PluginName.GERRIT, b.getKey(), b.getProvider());
         }
       }
     }
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 7ffb86d..6b3a49b 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -14,6 +14,12 @@
 
 package com.google.gerrit.extensions.registration;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.util.Comparator.naturalOrder;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -39,6 +45,24 @@
  * singleton and non-singleton members.
  */
 public class DynamicSet<T> implements Iterable<T> {
+  public static class Entry<T> {
+    private final String pluginName;
+    private final Provider<T> provider;
+
+    private Entry(String pluginName, Provider<T> provider) {
+      this.pluginName = pluginName;
+      this.provider = provider;
+    }
+
+    public String getPluginName() {
+      return pluginName;
+    }
+
+    public Provider<T> getProvider() {
+      return provider;
+    }
+  }
+
   /**
    * Declare a singleton {@code DynamicSet<T>} with a binder.
    *
@@ -129,12 +153,12 @@
   }
 
   public static <T> DynamicSet<T> emptySet() {
-    return new DynamicSet<>(Collections.<AtomicReference<Provider<T>>>emptySet());
+    return new DynamicSet<>(Collections.<AtomicReference<NamedProvider<T>>>emptySet());
   }
 
-  private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
+  private final CopyOnWriteArrayList<AtomicReference<NamedProvider<T>>> items;
 
-  DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
+  DynamicSet(Collection<AtomicReference<NamedProvider<T>>> base) {
     items = new CopyOnWriteArrayList<>(base);
   }
 
@@ -144,38 +168,59 @@
 
   @Override
   public Iterator<T> iterator() {
-    final Iterator<AtomicReference<Provider<T>>> itr = items.iterator();
+    Iterator<Entry<T>> entryIterator = entries().iterator();
     return new Iterator<T>() {
-      private T next;
-
       @Override
       public boolean hasNext() {
-        while (next == null && itr.hasNext()) {
-          Provider<T> p = itr.next().get();
-          if (p != null) {
-            try {
-              next = p.get();
-            } catch (RuntimeException e) {
-              // TODO Log failed member of DynamicSet.
-            }
-          }
-        }
-        return next != null;
+        return entryIterator.hasNext();
       }
 
       @Override
       public T next() {
-        if (hasNext()) {
-          T result = next;
-          next = null;
-          return result;
-        }
-        throw new NoSuchElementException();
+        Entry<T> next = entryIterator.next();
+        return next != null ? next.getProvider().get() : null;
       }
+    };
+  }
 
+  public Iterable<Entry<T>> entries() {
+    final Iterator<AtomicReference<NamedProvider<T>>> itr = items.iterator();
+    return new Iterable<Entry<T>>() {
       @Override
-      public void remove() {
-        throw new UnsupportedOperationException();
+      public Iterator<Entry<T>> iterator() {
+        return new Iterator<Entry<T>>() {
+          private Entry<T> next;
+
+          @Override
+          public boolean hasNext() {
+            while (next == null && itr.hasNext()) {
+              NamedProvider<T> p = itr.next().get();
+              if (p != null) {
+                try {
+                  next = new Entry<>(p.pluginName, p.impl);
+                } catch (RuntimeException e) {
+                  // TODO Log failed member of DynamicSet.
+                }
+              }
+            }
+            return next != null;
+          }
+
+          @Override
+          public Entry<T> next() {
+            if (hasNext()) {
+              Entry<T> result = next;
+              next = null;
+              return result;
+            }
+            throw new NoSuchElementException();
+          }
+
+          @Override
+          public void remove() {
+            throw new UnsupportedOperationException();
+          }
+        };
       }
     };
   }
@@ -198,13 +243,29 @@
   }
 
   /**
-   * Add one new element to the set.
+   * Get the names of all running plugins supplying this type.
    *
-   * @param item the item to add to the collection. Must not be null.
-   * @return handle to remove the item at a later point in time.
+   * @return sorted set of active plugins that supply at least one item.
    */
-  public RegistrationHandle add(T item) {
-    return add(Providers.of(item));
+  public ImmutableSortedSet<String> plugins() {
+    return items
+        .stream()
+        .map(i -> i.get().pluginName)
+        .collect(toImmutableSortedSet(naturalOrder()));
+  }
+
+  /**
+   * Get the items exported by a single plugin.
+   *
+   * @param pluginName name of the plugin.
+   * @return items exported by a plugin.
+   */
+  public ImmutableSet<Provider<T>> byPlugin(String pluginName) {
+    return items
+        .stream()
+        .filter(i -> i.get().pluginName.equals(pluginName))
+        .map(i -> i.get().impl)
+        .collect(toImmutableSet());
   }
 
   /**
@@ -213,13 +274,24 @@
    * @param item the item to add to the collection. Must not be null.
    * @return handle to remove the item at a later point in time.
    */
-  public RegistrationHandle add(Provider<T> item) {
-    final AtomicReference<Provider<T>> ref = new AtomicReference<>(item);
+  public RegistrationHandle add(String pluginName, T item) {
+    return add(pluginName, Providers.of(item));
+  }
+
+  /**
+   * Add one new element to the set.
+   *
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle add(String pluginName, Provider<T> item) {
+    final AtomicReference<NamedProvider<T>> ref =
+        new AtomicReference<>(new NamedProvider<>(item, pluginName));
     items.add(ref);
     return new RegistrationHandle() {
       @Override
       public void remove() {
-        if (ref.compareAndSet(item, null)) {
+        if (ref.compareAndSet(ref.get(), null)) {
           items.remove(ref);
         }
       }
@@ -229,6 +301,7 @@
   /**
    * Add one new element that may be hot-replaceable in the future.
    *
+   * @param pluginName unique name of the plugin providing the item.
    * @param key unique description from the item's Guice binding. This can be later obtained from
    *     the registration handle to facilitate matching with the new equivalent instance during a
    *     hot reload.
@@ -236,18 +309,19 @@
    * @return a handle that can remove this item later, or hot-swap the item without it ever leaving
    *     the collection.
    */
-  public ReloadableRegistrationHandle<T> add(Key<T> key, Provider<T> item) {
-    AtomicReference<Provider<T>> ref = new AtomicReference<>(item);
+  public ReloadableRegistrationHandle<T> add(String pluginName, Key<T> key, Provider<T> item) {
+    AtomicReference<NamedProvider<T>> ref =
+        new AtomicReference<>(new NamedProvider<>(item, pluginName));
     items.add(ref);
-    return new ReloadableHandle(ref, key, item);
+    return new ReloadableHandle(ref, key, ref.get());
   }
 
   private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
-    private final AtomicReference<Provider<T>> ref;
+    private final AtomicReference<NamedProvider<T>> ref;
     private final Key<T> key;
-    private final Provider<T> item;
+    private final NamedProvider<T> item;
 
-    ReloadableHandle(AtomicReference<Provider<T>> ref, Key<T> key, Provider<T> item) {
+    ReloadableHandle(AtomicReference<NamedProvider<T>> ref, Key<T> key, NamedProvider<T> item) {
       this.ref = ref;
       this.key = key;
       this.item = item;
@@ -267,8 +341,9 @@
 
     @Override
     public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
-      if (ref.compareAndSet(item, newItem)) {
-        return new ReloadableHandle(ref, newKey, newItem);
+      NamedProvider<T> n = new NamedProvider<>(newItem, item.pluginName);
+      if (ref.compareAndSet(item, n)) {
+        return new ReloadableHandle(ref, newKey, n);
       }
       return null;
     }
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 707c76a..6d36f54 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -38,16 +38,17 @@
     return new DynamicSet<>(find(injector, type));
   }
 
-  private static <T> List<AtomicReference<Provider<T>>> find(Injector src, TypeLiteral<T> type) {
+  private static <T> List<AtomicReference<NamedProvider<T>>> find(
+      Injector src, TypeLiteral<T> type) {
     List<Binding<T>> bindings = src.findBindingsByType(type);
     int cnt = bindings != null ? bindings.size() : 0;
     if (cnt == 0) {
       return Collections.emptyList();
     }
-    List<AtomicReference<Provider<T>>> r = new ArrayList<>(cnt);
+    List<AtomicReference<NamedProvider<T>>> r = new ArrayList<>(cnt);
     for (Binding<T> b : bindings) {
       if (b.getKey().getAnnotation() != null) {
-        r.add(new AtomicReference<>(b.getProvider()));
+        r.add(new AtomicReference<>(new NamedProvider<>(b.getProvider(), PluginName.GERRIT)));
       }
     }
     return r;
diff --git a/java/com/google/gerrit/extensions/registration/PluginName.java b/java/com/google/gerrit/extensions/registration/PluginName.java
new file mode 100644
index 0000000..c110d45
--- /dev/null
+++ b/java/com/google/gerrit/extensions/registration/PluginName.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+public class PluginName {
+  /** Name that is used as plugin name if Gerrit core implements a plugin extension point. */
+  public static final String GERRIT = "gerrit";
+
+  private PluginName() {}
+}
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
index 9342e0f..fd31fcd 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -107,7 +107,7 @@
   }
 
   public static List<RegistrationHandle> attachSets(
-      Injector src, Map<TypeLiteral<?>, DynamicSet<?>> sets) {
+      Injector src, String pluginName, Map<TypeLiteral<?>, DynamicSet<?>> sets) {
     if (src == null || sets == null || sets.isEmpty()) {
       return Collections.emptyList();
     }
@@ -123,7 +123,7 @@
 
         for (Binding<Object> b : bindings(src, type)) {
           if (b.getKey().getAnnotation() != null) {
-            handles.add(set.add(b.getKey(), b.getProvider()));
+            handles.add(set.add(pluginName, b.getKey(), b.getProvider()));
           }
         }
       }
@@ -174,8 +174,8 @@
         handles = new ArrayList<>(4);
         Injector parent = self.getParent();
         while (parent != null) {
-          handles.addAll(attachSets(self, dynamicSetsOf(parent)));
-          handles.addAll(attachMaps(self, "gerrit", dynamicMapsOf(parent)));
+          handles.addAll(attachSets(self, PluginName.GERRIT, dynamicSetsOf(parent)));
+          handles.addAll(attachMaps(self, PluginName.GERRIT, dynamicMapsOf(parent)));
           parent = parent.getParent();
         }
         if (handles.isEmpty()) {
diff --git a/java/com/google/gerrit/httpd/AllRequestFilter.java b/java/com/google/gerrit/httpd/AllRequestFilter.java
index b8b0bc8..9d171d5 100644
--- a/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -76,7 +76,7 @@
         // synchronized.
         if (!initializedFilters.contains(filter)) {
           filter.init(filterConfig);
-          initializedFilters.add(filter);
+          initializedFilters.add("gerrit", filter);
         }
       } else {
         ret = false;
@@ -89,7 +89,7 @@
       initializedFilters = new DynamicSet<>();
       for (AllRequestFilter filter : filtersToCleanUp) {
         if (filters.contains(filter)) {
-          initializedFilters.add(filter);
+          initializedFilters.add("gerrit", filter);
         } else {
           filter.destroy();
         }
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 3e71098..fae7c6a 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -16,6 +16,7 @@
         "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/http",
diff --git a/java/com/google/gerrit/httpd/GerritAuthModule.java b/java/com/google/gerrit/httpd/GerritAuthModule.java
new file mode 100644
index 0000000..c0ef207
--- /dev/null
+++ b/java/com/google/gerrit/httpd/GerritAuthModule.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2018 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.httpd;
+
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_URL_WO_AUTH_REGEX;
+
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.servlet.ServletModule;
+import javax.servlet.Filter;
+
+/** Configures filter for authenticating REST requests. */
+public class GerritAuthModule extends ServletModule {
+  private static final String NOT_AUTHORIZED_LFS_URL_REGEX = "^(?:(?!/a/))" + LFS_URL_WO_AUTH_REGEX;
+  private final AuthConfig authConfig;
+
+  @Inject
+  GerritAuthModule(AuthConfig authConfig) {
+    this.authConfig = authConfig;
+  }
+
+  @Override
+  protected void configureServlets() {
+    Class<? extends Filter> authFilter = retreiveAuthFilterFromConfig(authConfig);
+
+    filterRegex(NOT_AUTHORIZED_LFS_URL_REGEX).through(authFilter);
+    filter("/a/*").through(authFilter);
+  }
+
+  static Class<? extends Filter> retreiveAuthFilterFromConfig(AuthConfig authConfig) {
+    Class<? extends Filter> authFilter;
+    if (authConfig.isTrustContainerAuth()) {
+      authFilter = ContainerAuthFilter.class;
+    } else {
+      authFilter =
+          authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.OAUTH
+              ? ProjectOAuthFilter.class
+              : ProjectBasicAuthFilter.class;
+    }
+    return authFilter;
+  }
+}
diff --git a/java/com/google/gerrit/httpd/GitOverHttpModule.java b/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 3f3737d..8400d60 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,20 +14,16 @@
 
 package com.google.gerrit.httpd;
 
-import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_URL_WO_AUTH_REGEX;
+import static com.google.gerrit.httpd.GitOverHttpServlet.URL_REGEX;
 
-import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
 import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
 import com.google.inject.servlet.ServletModule;
-import javax.servlet.Filter;
 
 /** Configures Git access over HTTP with authentication. */
 public class GitOverHttpModule extends ServletModule {
-  private static final String NOT_AUTHORIZED_LFS_URL_REGEX = "^(?:(?!/a/))" + LFS_URL_WO_AUTH_REGEX;
-
   private final AuthConfig authConfig;
   private final DownloadConfig downloadConfig;
 
@@ -39,28 +35,10 @@
 
   @Override
   protected void configureServlets() {
-    Class<? extends Filter> authFilter;
-    if (authConfig.isTrustContainerAuth()) {
-      authFilter = ContainerAuthFilter.class;
-    } else {
-      authFilter =
-          authConfig.getGitBasicAuthPolicy() == GitBasicAuthPolicy.OAUTH
-              ? ProjectOAuthFilter.class
-              : ProjectBasicAuthFilter.class;
+    if (downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.ANON_HTTP)
+        || downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.HTTP)) {
+      filterRegex(URL_REGEX).through(GerritAuthModule.retreiveAuthFilterFromConfig(authConfig));
+      serveRegex(URL_REGEX).with(GitOverHttpServlet.class);
     }
-
-    if (isHttpEnabled()) {
-      String git = GitOverHttpServlet.URL_REGEX;
-      filterRegex(git).through(authFilter);
-      serveRegex(git).with(GitOverHttpServlet.class);
-    }
-
-    filterRegex(NOT_AUTHORIZED_LFS_URL_REGEX).through(authFilter);
-    filter("/a/*").through(authFilter);
-  }
-
-  private boolean isHttpEnabled() {
-    return downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.ANON_HTTP)
-        || downloadConfig.getDownloadSchemes().contains(CoreDownloadSchemes.HTTP);
   }
 }
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 8e380f5..cb476af 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.httpd.GerritAuthModule;
 import com.google.gerrit.httpd.GetUserFilter;
 import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.H2CacheBasedWebSession;
@@ -422,9 +423,10 @@
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(RequestContextFilter.module());
-    modules.add(AllRequestFilter.module());
     modules.add(RequestMetricsFilter.module());
+    modules.add(sysInjector.getInstance(GerritAuthModule.class));
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
+    modules.add(AllRequestFilter.module());
     modules.add(sysInjector.getInstance(WebModule.class));
     modules.add(sysInjector.getInstance(RequireSslFilter.Module.class));
     if (sshInjector != null) {
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 9a24e47..74cadd3 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -456,8 +456,8 @@
       }
     }
 
-    Collections.sort(cmds, PluginEntry.COMPARATOR_BY_NAME);
-    Collections.sort(docs, PluginEntry.COMPARATOR_BY_NAME);
+    cmds.sort(PluginEntry.COMPARATOR_BY_NAME);
+    docs.sort(PluginEntry.COMPARATOR_BY_NAME);
 
     StringBuilder md = new StringBuilder();
     md.append(String.format("# Plugin %s #\n", pluginName));
diff --git a/java/com/google/gerrit/httpd/raw/BazelBuild.java b/java/com/google/gerrit/httpd/raw/BazelBuild.java
index 940a51b..2b390a9 100644
--- a/java/com/google/gerrit/httpd/raw/BazelBuild.java
+++ b/java/com/google/gerrit/httpd/raw/BazelBuild.java
@@ -23,14 +23,15 @@
 import com.google.common.html.HtmlEscapers;
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.util.http.CacheHeaders;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.PrintWriter;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Properties;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -122,20 +123,19 @@
     }
   }
 
-  private Properties loadBuildProperties(Path propPath) throws IOException {
-    Properties properties = new Properties();
-    try (InputStream in = Files.newInputStream(propPath)) {
-      properties.load(in);
-    } catch (NoSuchFileException e) {
-      // Ignore; will be run from PATH, with a descriptive error if it fails.
-    }
-    return properties;
-  }
-
   private ProcessBuilder newBuildProcess(Label label) throws IOException {
-    Properties properties = loadBuildProperties(sourceRoot.resolve(".bazel_path"));
+    Properties properties = GerritLauncher.loadBuildProperties(sourceRoot.resolve(".bazel_path"));
     String bazel = firstNonNull(properties.getProperty("bazel"), "bazel");
-    ProcessBuilder proc = new ProcessBuilder(bazel, "build", label.fullName());
+    List<String> cmd = new ArrayList<>();
+    cmd.add(bazel);
+    cmd.add("build");
+    if (GerritLauncher.isJdk9OrLater()) {
+      String v = GerritLauncher.getJdkVersionPostJdk8();
+      cmd.add("--host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java" + v);
+      cmd.add("--java_toolchain=@bazel_tools//tools/jdk:toolchain_java" + v);
+    }
+    cmd.add(label.fullName());
+    ProcessBuilder proc = new ProcessBuilder(cmd);
     if (properties.containsKey("PATH")) {
       proc.environment().put("PATH", properties.getProperty("PATH"));
     }
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
index 4af03a3..562687b 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.restapi;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.httpd.restapi.RestApiServlet.ViewData;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Counter2;
@@ -79,7 +80,8 @@
         break;
       }
     }
-    if (!Strings.isNullOrEmpty(viewData.pluginName) && !"gerrit".equals(viewData.pluginName)) {
+    if (!Strings.isNullOrEmpty(viewData.pluginName)
+        && !PluginName.GERRIT.equals(viewData.pluginName)) {
       impl = viewData.pluginName + '-' + impl;
     }
     return impl;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index cfe712b..e0559f1 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -70,6 +70,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -324,7 +325,7 @@
             viewData = new ViewData(null, rc.list());
           } else if (isPost(req)) {
             RestView<RestResource> restCollectionView =
-                rc.views().get("gerrit", "POST_ON_COLLECTION./");
+                rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
             if (restCollectionView != null) {
               viewData = new ViewData(null, restCollectionView);
             } else {
@@ -347,7 +348,7 @@
             }
 
             if (isPost(req) || isPut(req)) {
-              RestView<RestResource> createView = rc.views().get("gerrit", "CREATE./");
+              RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
               if (createView != null) {
                 viewData = new ViewData(null, createView);
                 status = SC_CREATED;
@@ -356,7 +357,8 @@
                 throw e;
               }
             } else if (isDelete(req)) {
-              RestView<RestResource> deleteView = rc.views().get("gerrit", "DELETE_MISSING./");
+              RestView<RestResource> deleteView =
+                  rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
               if (deleteView != null) {
                 viewData = new ViewData(null, deleteView);
                 status = SC_NO_CONTENT;
@@ -414,7 +416,7 @@
             }
 
             if (isPost(req) || isPut(req)) {
-              RestView<RestResource> createView = c.views().get("gerrit", "CREATE./");
+              RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
               if (createView != null) {
                 viewData = new ViewData(null, createView);
                 status = SC_CREATED;
@@ -423,7 +425,8 @@
                 throw e;
               }
             } else if (isDelete(req)) {
-              RestView<RestResource> deleteView = c.views().get("gerrit", "DELETE_MISSING./");
+              RestView<RestResource> deleteView =
+                  c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
               if (deleteView != null) {
                 viewData = new ViewData(null, deleteView);
                 status = SC_NO_CONTENT;
@@ -1245,14 +1248,14 @@
     }
 
     String name = method + "." + p.get(0);
-    RestView<RestResource> core = views.get("gerrit", name);
+    RestView<RestResource> core = views.get(PluginName.GERRIT, name);
     if (core != null) {
-      return new ViewData("gerrit", core);
+      return new ViewData(PluginName.GERRIT, core);
     }
 
-    core = views.get("gerrit", "GET." + p.get(0));
+    core = views.get(PluginName.GERRIT, "GET." + p.get(0));
     if (core != null) {
-      return new ViewData("gerrit", core);
+      return new ViewData(PluginName.GERRIT, core);
     }
 
     Map<String, RestView<RestResource>> r = new TreeMap<>();
@@ -1320,13 +1323,42 @@
   }
 
   private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
-    String v = req.getParameter(ParameterParser.TRACE_PARAMETER);
-    if (v != null && (v.isEmpty() || Boolean.parseBoolean(v))) {
-      RequestId traceId = new RequestId();
-      res.setHeader(X_GERRIT_TRACE, traceId.toString());
-      return TraceContext.open().forceLogging().addTag(RequestId.Type.TRACE_ID, traceId);
+    // There are 2 ways to enable tracing for REST calls:
+    // 1. by using the 'trace' or 'trace=<trace-id>' request parameter
+    // 2. by setting the 'X-Gerrit-Trace:' or 'X-Gerrit-Trace:<trace-id>' header
+    String traceValueFromHeader = req.getHeader(X_GERRIT_TRACE);
+    String traceValueFromRequestParam = req.getParameter(ParameterParser.TRACE_PARAMETER);
+    boolean doTrace = traceValueFromHeader != null || traceValueFromRequestParam != null;
+
+    // Check whether no trace ID, one trace ID or 2 different trace IDs have been specified.
+    String traceId1;
+    String traceId2;
+    if (!Strings.isNullOrEmpty(traceValueFromHeader)) {
+      traceId1 = traceValueFromHeader;
+      if (!Strings.isNullOrEmpty(traceValueFromRequestParam)
+          && !traceValueFromHeader.equals(traceValueFromRequestParam)) {
+        traceId2 = traceValueFromRequestParam;
+      } else {
+        traceId2 = null;
+      }
+    } else {
+      traceId1 = Strings.emptyToNull(traceValueFromRequestParam);
+      traceId2 = null;
     }
-    return TraceContext.DISABLED;
+
+    // Use the first trace ID to start tracing. If this trace ID is null, a trace ID will be
+    // generated.
+    TraceContext traceContext =
+        TraceContext.newTrace(
+            doTrace,
+            traceId1,
+            (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId.toString()));
+    // If a second trace ID was specified, add a tag for it as well.
+    if (traceId2 != null) {
+      traceContext.addTag(RequestId.Type.TRACE_ID, traceId2);
+      res.addHeader(X_GERRIT_TRACE, traceId2);
+    }
+    return traceContext;
   }
 
   private boolean isDelete(HttpServletRequest req) {
@@ -1360,10 +1392,15 @@
 
   private void checkRequiresCapability(ViewData d)
       throws AuthException, PermissionBackendException {
-    globals
-        .permissionBackend
-        .currentUser()
-        .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass()));
+    try {
+      globals.permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+    } catch (AuthException e) {
+      // Skiping
+      globals
+          .permissionBackend
+          .currentUser()
+          .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass()));
+    }
   }
 
   private static long handleException(
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 6604ca1..d5517e1 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -12,19 +12,6 @@
 )
 
 java_library(
-    name = "query_parser",
-    srcs = ["//antlr3:query"],
-    visibility = [
-        "//javatests/com/google/gerrit/index:__pkg__",
-        "//plugins:__pkg__",
-    ],
-    deps = [
-        ":query_exception",
-        "//lib/antlr:java-runtime",
-    ],
-)
-
-java_library(
     name = "index",
     srcs = glob(
         ["**/*.java"],
@@ -33,7 +20,7 @@
     visibility = ["//visibility:public"],
     deps = [
         ":query_exception",
-        ":query_parser",
+        "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/metrics",
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index 24b7a69..9c56396 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.index;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Stopwatch;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.RejectedExecutionException;
@@ -64,7 +66,7 @@
 
   protected int totalWork = -1;
   protected OutputStream progressOut = NullOutputStream.INSTANCE;
-  protected PrintWriter verboseWriter = new PrintWriter(NullOutputStream.INSTANCE);
+  protected PrintWriter verboseWriter = newPrintWriter(NullOutputStream.INSTANCE);
 
   public void setTotalWork(int num) {
     totalWork = num;
@@ -75,7 +77,7 @@
   }
 
   public void setVerboseOut(OutputStream out) {
-    verboseWriter = new PrintWriter(checkNotNull(out));
+    verboseWriter = newPrintWriter(checkNotNull(out));
   }
 
   public abstract Result indexAll(I index);
@@ -86,6 +88,10 @@
         new ErrorListener(future, desc, progress, ok), MoreExecutors.directExecutor());
   }
 
+  protected PrintWriter newPrintWriter(OutputStream out) {
+    return new PrintWriter(new OutputStreamWriter(out, UTF_8));
+  }
+
   private static class ErrorListener implements Runnable {
     private final ListenableFuture<?> future;
     private final String desc;
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index e2605f4..d1e1c30 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.index.query;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
@@ -26,7 +27,6 @@
 import com.google.gwtorm.server.ResultSet;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
@@ -175,10 +175,8 @@
     return cardinality;
   }
 
-  private List<Predicate<T>> sort(Collection<? extends Predicate<T>> that) {
-    List<Predicate<T>> r = new ArrayList<>(that);
-    Collections.sort(r, this);
-    return r;
+  private ImmutableList<Predicate<T>> sort(Collection<? extends Predicate<T>> that) {
+    return that.stream().sorted(this).collect(toImmutableList());
   }
 
   @Override
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 13dad0e..0d26fe7 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -34,6 +34,7 @@
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.CodeSource;
@@ -44,6 +45,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Scanner;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -644,6 +646,25 @@
     return resolveInSourceRoot("eclipse-out");
   }
 
+  public static boolean isJdk9OrLater() {
+    return Double.parseDouble(System.getProperty("java.class.version")) >= 53.0;
+  }
+
+  public static String getJdkVersionPostJdk8() {
+    // 9.0.4 => 9
+    return System.getProperty("java.version").substring(0, 1);
+  }
+
+  public static Properties loadBuildProperties(Path propPath) throws IOException {
+    Properties properties = new Properties();
+    try (InputStream in = Files.newInputStream(propPath)) {
+      properties.load(in);
+    } catch (NoSuchFileException e) {
+      // Ignore; will be run from PATH, with a descriptive error if it fails.
+    }
+    return properties;
+  }
+
   static final String SOURCE_ROOT_RESOURCE = "/com/google/gerrit/launcher/workspace-root.txt";
 
   /**
@@ -708,14 +729,36 @@
     return ret;
   }
 
-  private static ClassLoader useDevClasspath() throws MalformedURLException, FileNotFoundException {
+  private static ClassLoader useDevClasspath() throws IOException {
     Path out = getDeveloperEclipseOut();
     List<URL> dirs = new ArrayList<>();
     dirs.add(out.resolve("classes").toUri().toURL());
     ClassLoader cl = GerritLauncher.class.getClassLoader();
-    for (URL u : ((URLClassLoader) cl).getURLs()) {
-      if (includeJar(u)) {
-        dirs.add(u);
+
+    if (isJdk9OrLater()) {
+      Path rootPath = resolveInSourceRoot(".").normalize();
+
+      Properties properties = loadBuildProperties(rootPath.resolve(".bazel_path"));
+      Path outputBase = Paths.get(properties.getProperty("output_base"));
+
+      Path runtimeClasspath =
+          rootPath.resolve("bazel-bin/tools/eclipse/main_classpath_collect.runtime_classpath");
+      for (String f : Files.readAllLines(runtimeClasspath, UTF_8)) {
+        URL url;
+        if (f.startsWith("external")) {
+          url = outputBase.resolve(f).toUri().toURL();
+        } else {
+          url = rootPath.resolve(f).toUri().toURL();
+        }
+        if (includeJar(url)) {
+          dirs.add(url);
+        }
+      }
+    } else {
+      for (URL u : ((URLClassLoader) cl).getURLs()) {
+        if (includeJar(u)) {
+          dirs.add(u);
+        }
       }
     }
     return URLClassLoader.newInstance(
@@ -724,7 +767,9 @@
 
   private static boolean includeJar(URL u) {
     String path = u.getPath();
-    return path.endsWith(".jar") && !path.endsWith("-src.jar");
+    return path.endsWith(".jar")
+        && !path.endsWith("-src.jar")
+        && !path.contains("/com/google/gerrit");
   }
 
   private GerritLauncher() {}
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 12f88d5..dc293cd 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -39,7 +39,8 @@
 import com.google.gerrit.index.query.FieldBundle;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
+import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import java.io.IOException;
@@ -55,6 +56,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -100,7 +102,7 @@
   private final ReferenceManager<IndexSearcher> searcherManager;
   private final ControlledRealTimeReopenThread<IndexSearcher> reopenThread;
   private final Set<NrtFuture> notDoneNrtFutures;
-  private ScheduledThreadPoolExecutor autoCommitExecutor;
+  private ScheduledExecutorService autoCommitExecutor;
 
   AbstractLuceneIndex(
       Schema<V> schema,
@@ -129,13 +131,13 @@
       delegateWriter = autoCommitWriter;
 
       autoCommitExecutor =
-          new ScheduledThreadPoolExecutor(
-              1,
-              new ThreadFactoryBuilder()
-                  .setThreadFactory(new LoggingContextAwareThreadFactory())
-                  .setNameFormat(index + " Commit-%d")
-                  .setDaemon(true)
-                  .build());
+          new LoggingContextAwareScheduledExecutorService(
+              new ScheduledThreadPoolExecutor(
+                  1,
+                  new ThreadFactoryBuilder()
+                      .setNameFormat(index + " Commit-%d")
+                      .setDaemon(true)
+                      .build()));
       @SuppressWarnings("unused") // Error handling within Runnable.
       Future<?> possiblyIgnoredError =
           autoCommitExecutor.scheduleAtFixedRate(
@@ -170,13 +172,13 @@
 
     writerThread =
         MoreExecutors.listeningDecorator(
-            Executors.newFixedThreadPool(
-                1,
-                new ThreadFactoryBuilder()
-                    .setThreadFactory(new LoggingContextAwareThreadFactory())
-                    .setNameFormat(index + " Write-%d")
-                    .setDaemon(true)
-                    .build()));
+            new LoggingContextAwareExecutorService(
+                Executors.newFixedThreadPool(
+                    1,
+                    new ThreadFactoryBuilder()
+                        .setNameFormat(index + " Write-%d")
+                        .setDaemon(true)
+                        .build())));
 
     reopenThread =
         new ControlledRealTimeReopenThread<>(
diff --git a/java/com/google/gerrit/lucene/BUILD b/java/com/google/gerrit/lucene/BUILD
index 6cb7751..9c6ba74 100644
--- a/java/com/google/gerrit/lucene/BUILD
+++ b/java/com/google/gerrit/lucene/BUILD
@@ -33,6 +33,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/logging",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/metrics/DisabledMetricMaker.java b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
index ea408e2..d3c030d 100644
--- a/java/com/google/gerrit/metrics/DisabledMetricMaker.java
+++ b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
@@ -68,7 +68,7 @@
 
   @Override
   public Timer0 newTimer(String name, Description desc) {
-    return new Timer0() {
+    return new Timer0(name) {
       @Override
       public void record(long value, TimeUnit unit) {}
 
@@ -79,7 +79,7 @@
 
   @Override
   public <F1> Timer1<F1> newTimer(String name, Description desc, Field<F1> field1) {
-    return new Timer1<F1>() {
+    return new Timer1<F1>(name) {
       @Override
       public void record(F1 field1, long value, TimeUnit unit) {}
 
@@ -91,7 +91,7 @@
   @Override
   public <F1, F2> Timer2<F1, F2> newTimer(
       String name, Description desc, Field<F1> field1, Field<F2> field2) {
-    return new Timer2<F1, F2>() {
+    return new Timer2<F1, F2>(name) {
       @Override
       public void record(F1 field1, F2 field2, long value, TimeUnit unit) {}
 
@@ -103,7 +103,7 @@
   @Override
   public <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
       String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
-    return new Timer3<F1, F2, F3>() {
+    return new Timer3<F1, F2, F3>(name) {
       @Override
       public void record(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {}
 
diff --git a/java/com/google/gerrit/metrics/Timer0.java b/java/com/google/gerrit/metrics/Timer0.java
index 55d1ddf..225b76f 100644
--- a/java/com/google/gerrit/metrics/Timer0.java
+++ b/java/com/google/gerrit/metrics/Timer0.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import java.util.concurrent.TimeUnit;
 
@@ -31,6 +32,8 @@
  */
 public abstract class Timer0 implements RegistrationHandle {
   public static class Context extends TimerContext {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
     private final Timer0 timer;
 
     Context(Timer0 timer) {
@@ -39,10 +42,17 @@
 
     @Override
     public void record(long elapsed) {
+      logger.atFinest().log("%s took %dms", timer.name, TimeUnit.NANOSECONDS.toMillis(elapsed));
       timer.record(elapsed, NANOSECONDS);
     }
   }
 
+  protected final String name;
+
+  public Timer0(String name) {
+    this.name = name;
+  }
+
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
diff --git a/java/com/google/gerrit/metrics/Timer1.java b/java/com/google/gerrit/metrics/Timer1.java
index f623841..0db0353 100644
--- a/java/com/google/gerrit/metrics/Timer1.java
+++ b/java/com/google/gerrit/metrics/Timer1.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import java.util.concurrent.TimeUnit;
 
@@ -33,6 +34,8 @@
  */
 public abstract class Timer1<F1> implements RegistrationHandle {
   public static class Context extends TimerContext {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
     private final Timer1<Object> timer;
     private final Object field1;
 
@@ -44,10 +47,18 @@
 
     @Override
     public void record(long elapsed) {
+      logger.atFinest().log(
+          "%s (%s) took %dms", timer.name, field1, TimeUnit.NANOSECONDS.toMillis(elapsed));
       timer.record(field1, elapsed, NANOSECONDS);
     }
   }
 
+  protected final String name;
+
+  public Timer1(String name) {
+    this.name = name;
+  }
+
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
diff --git a/java/com/google/gerrit/metrics/Timer2.java b/java/com/google/gerrit/metrics/Timer2.java
index b03ff83..cfdfb7a 100644
--- a/java/com/google/gerrit/metrics/Timer2.java
+++ b/java/com/google/gerrit/metrics/Timer2.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import java.util.concurrent.TimeUnit;
 
@@ -34,6 +35,8 @@
  */
 public abstract class Timer2<F1, F2> implements RegistrationHandle {
   public static class Context extends TimerContext {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
     private final Timer2<Object, Object> timer;
     private final Object field1;
     private final Object field2;
@@ -47,10 +50,19 @@
 
     @Override
     public void record(long elapsed) {
+      logger.atFinest().log(
+          "%s (%s, %s) took %dms",
+          timer.name, field1, field2, TimeUnit.NANOSECONDS.toMillis(elapsed));
       timer.record(field1, field2, elapsed, NANOSECONDS);
     }
   }
 
+  protected final String name;
+
+  public Timer2(String name) {
+    this.name = name;
+  }
+
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
diff --git a/java/com/google/gerrit/metrics/Timer3.java b/java/com/google/gerrit/metrics/Timer3.java
index 91af42c..1711445 100644
--- a/java/com/google/gerrit/metrics/Timer3.java
+++ b/java/com/google/gerrit/metrics/Timer3.java
@@ -16,6 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import java.util.concurrent.TimeUnit;
 
@@ -35,6 +36,8 @@
  */
 public abstract class Timer3<F1, F2, F3> implements RegistrationHandle {
   public static class Context extends TimerContext {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
     private final Timer3<Object, Object, Object> timer;
     private final Object field1;
     private final Object field2;
@@ -50,10 +53,19 @@
 
     @Override
     public void record(long elapsed) {
+      logger.atFinest().log(
+          "%s (%s, %s, %s) took %dms",
+          timer.name, field1, field2, field3, TimeUnit.NANOSECONDS.toMillis(elapsed));
       timer.record(field1, field2, field3, elapsed, NANOSECONDS);
     }
   }
 
+  protected final String name;
+
+  public Timer3(String name) {
+    this.name = name;
+  }
+
   /**
    * Begin a timer for the current block, value will be recorded when closed.
    *
diff --git a/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java b/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
index 3b19a62..a7ffe07 100644
--- a/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
+++ b/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
@@ -26,7 +26,7 @@
 /** Abstract timer broken down into buckets by {@link Field} values. */
 abstract class BucketedTimer implements BucketedMetric {
   private final DropWizardMetricMaker metrics;
-  private final String name;
+  protected final String name;
   private final Description.FieldOrdering ordering;
   protected final Field<?>[] fields;
   protected final TimerImpl total;
diff --git a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
index fc53ee7..ead718f 100644
--- a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
+++ b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -393,11 +393,10 @@
   }
 
   class TimerImpl extends Timer0 {
-    private final String name;
     final com.codahale.metrics.Timer metric;
 
     private TimerImpl(String name, com.codahale.metrics.Timer metric) {
-      this.name = name;
+      super(name);
       this.metric = metric;
     }
 
diff --git a/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java b/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
index fe6f70e..fc4ba3f 100644
--- a/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
@@ -27,7 +27,7 @@
   }
 
   Timer1<F1> timer() {
-    return new Timer1<F1>() {
+    return new Timer1<F1>(name) {
       @Override
       public void record(F1 field1, long value, TimeUnit unit) {
         total.record(value, unit);
diff --git a/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java b/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
index 43cc290..d04a65e 100644
--- a/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
@@ -30,7 +30,7 @@
   }
 
   <F1, F2> Timer2<F1, F2> timer2() {
-    return new Timer2<F1, F2>() {
+    return new Timer2<F1, F2>(name) {
       @Override
       public void record(F1 field1, F2 field2, long value, TimeUnit unit) {
         total.record(value, unit);
@@ -45,7 +45,7 @@
   }
 
   <F1, F2, F3> Timer3<F1, F2, F3> timer3() {
-    return new Timer3<F1, F2, F3>() {
+    return new Timer3<F1, F2, F3>(name) {
       @Override
       public void record(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {
         total.record(value, unit);
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 517787c..1a9af55 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.httpd.GerritAuthModule;
 import com.google.gerrit.httpd.GetUserFilter;
 import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.H2CacheBasedWebSession;
@@ -575,10 +576,11 @@
       modules.add(new ProjectQoSFilter.Module());
     }
     modules.add(RequestContextFilter.module());
-    modules.add(AllRequestFilter.module());
     modules.add(RequestMetricsFilter.module());
     modules.add(H2CacheBasedWebSession.module());
+    modules.add(sysInjector.getInstance(GerritAuthModule.class));
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
+    modules.add(AllRequestFilter.module());
     modules.add(sysInjector.getInstance(WebModule.class));
     modules.add(sysInjector.getInstance(RequireSslFilter.Module.class));
     modules.add(new HttpPluginModule());
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
index 07da3f7..61d7ed9 100644
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 
@@ -39,6 +40,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
@@ -141,7 +143,7 @@
           migrator.migrate();
         }
       }
-      try (PrintWriter w = new PrintWriter(System.out, true)) {
+      try (PrintWriter w = new PrintWriter(new OutputStreamWriter(System.out, UTF_8), true)) {
         gcAllUsers.run(w);
       }
     } finally {
diff --git a/java/com/google/gerrit/pgm/init/InitLogging.java b/java/com/google/gerrit/pgm/init/InitLogging.java
new file mode 100644
index 0000000..52d0d2f
--- /dev/null
+++ b/java/com/google/gerrit/pgm/init/InitLogging.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2018 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.pgm.init;
+
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class InitLogging implements InitStep {
+  private static final String CONTAINER = "container";
+  private static final String JAVA_OPTIONS = "javaOptions";
+  private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
+  private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
+
+  private final Section container;
+
+  @Inject
+  public InitLogging(Section.Factory sections) {
+    this.container = sections.get(CONTAINER, null);
+  }
+
+  @Override
+  public void run() throws Exception {
+    List<String> javaOptions = new ArrayList<>(Arrays.asList(container.getList(JAVA_OPTIONS)));
+    if (!isSet(javaOptions, FLOGGER_BACKEND_PROPERTY)) {
+      javaOptions.add(
+          getJavaOption(
+              FLOGGER_BACKEND_PROPERTY,
+              "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance"));
+    }
+    if (!isSet(javaOptions, FLOGGER_LOGGING_CONTEXT)) {
+      javaOptions.add(
+          getJavaOption(
+              FLOGGER_LOGGING_CONTEXT,
+              "com.google.gerrit.server.logging.LoggingContext#getInstance"));
+    }
+    container.setList(JAVA_OPTIONS, javaOptions);
+  }
+
+  private static boolean isSet(List<String> javaOptions, String javaOptionName) {
+    return javaOptions
+        .stream()
+        .anyMatch(
+            o ->
+                o.startsWith("-D" + javaOptionName + "=")
+                    || o.startsWith("\"-D" + javaOptionName + "="));
+  }
+
+  private static String getJavaOption(String javaOptionName, String value) {
+    return String.format("-D%s=%s", javaOptionName, value);
+  }
+}
diff --git a/java/com/google/gerrit/pgm/init/InitModule.java b/java/com/google/gerrit/pgm/init/InitModule.java
index 65cf355..f677ceb 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -49,6 +49,7 @@
     if (initDb) {
       step().to(InitDatabase.class);
     }
+    step().to(InitLogging.class);
     step().to(InitIndex.class);
     step().to(InitAuth.class);
     step().to(InitAdminUser.class);
diff --git a/java/com/google/gerrit/pgm/init/InitPlugins.java b/java/com/google/gerrit/pgm/init/InitPlugins.java
index 385d20c..e43114c 100644
--- a/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.common.collect.FluentIterable;
+import static java.util.Comparator.comparing;
+
 import com.google.gerrit.common.PluginData;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
@@ -25,12 +26,10 @@
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Comparator;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
@@ -58,25 +57,16 @@
       throws IOException {
     final List<PluginData> result = new ArrayList<>();
     pluginsDistribution.foreach(
-        new PluginsDistribution.Processor() {
-          @Override
-          public void process(String pluginName, InputStream in) throws IOException {
-            Path tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
-            String pluginVersion = getVersion(tmpPlugin);
-            if (deleteTempPluginFile) {
-              Files.delete(tmpPlugin);
-            }
-            result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
+        (pluginName, in) -> {
+          Path tmpPlugin = JarPluginProvider.storeInTemp(pluginName, in, site);
+          String pluginVersion = getVersion(tmpPlugin);
+          if (deleteTempPluginFile) {
+            Files.delete(tmpPlugin);
           }
+          result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
         });
-    return FluentIterable.from(result)
-        .toSortedList(
-            new Comparator<PluginData>() {
-              @Override
-              public int compare(PluginData a, PluginData b) {
-                return a.name.compareTo(b.name);
-              }
-            });
+    result.sort(comparing(p -> p.name));
+    return result;
   }
 
   private final ConsoleUI ui;
diff --git a/java/com/google/gerrit/pgm/init/InitSshd.java b/java/com/google/gerrit/pgm/init/InitSshd.java
index 0cc30f8..68bdefc 100644
--- a/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -103,7 +103,7 @@
                 "-q" /* quiet */,
                 "-t",
                 "rsa",
-                "-P",
+                "-N",
                 emptyPassphraseArg,
                 "-C",
                 comment,
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index 009e989..baf37b6 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -23,6 +23,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 
@@ -59,6 +60,10 @@
     return flags.cfg.getString(section, subsection, name);
   }
 
+  public String[] getList(String name) {
+    return flags.cfg.getStringList(section, subsection, name);
+  }
+
   public void set(String name, String value) {
     final ArrayList<String> all = new ArrayList<>();
     all.addAll(Arrays.asList(flags.cfg.getStringList(section, subsection, name)));
@@ -79,6 +84,10 @@
     }
   }
 
+  public void setList(String name, List<String> values) {
+    flags.cfg.setStringList(section, subsection, name, values);
+  }
+
   public <T extends Enum<?>> void set(String name, T value) {
     if (value != null) {
       set(name, value.name());
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 8d77ed8..683a205 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -75,6 +75,7 @@
 import com.google.gerrit.server.query.change.ChangeQueryProcessor;
 import com.google.gerrit.server.restapi.group.GroupModule;
 import com.google.gerrit.server.rules.DefaultSubmitRule;
+import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -188,6 +189,7 @@
     factory(SubmitRuleEvaluator.Factory.class);
     install(new PrologModule());
     install(new DefaultSubmitRule.Module());
+    install(new IgnoreSelfApprovalRule.Module());
 
     bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
     bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 96fcd39..e5bc480 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -42,6 +42,7 @@
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/util/git",
         "//java/com/google/gerrit/util/cli",
         "//java/com/google/gerrit/util/ssl",
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index d90f5d0..571f322 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -37,17 +37,9 @@
   private static final Random UUID_RANDOM = new SecureRandom();
   private static final BaseEncoding UUID_ENCODING = BaseEncoding.base16().lowerCase();
 
-  private static final int SUBJECT_MAX_LENGTH = 80;
-  private static final String SUBJECT_CROP_APPENDIX = "...";
-  private static final int SUBJECT_CROP_RANGE = 10;
-
   public static final Ordering<PatchSet> PS_ID_ORDER =
       Ordering.from(comparingInt(PatchSet::getPatchSetId));
 
-  public static String formatChangeUrl(String canonicalWebUrl, Change change) {
-    return canonicalWebUrl + "c/" + change.getProject().get() + "/+/" + change.getChangeId();
-  }
-
   /** @return a new unique identifier for change message entities. */
   public static String messageUuid() {
     byte[] buf = new byte[8];
@@ -123,21 +115,6 @@
         id);
   }
 
-  public static String cropSubject(String subject) {
-    if (subject.length() > SUBJECT_MAX_LENGTH) {
-      int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length();
-      for (int cropPosition = maxLength;
-          cropPosition > maxLength - SUBJECT_CROP_RANGE;
-          cropPosition--) {
-        if (Character.isWhitespace(subject.charAt(cropPosition - 1))) {
-          return subject.substring(0, cropPosition) + SUBJECT_CROP_APPENDIX;
-        }
-      }
-      return subject.substring(0, maxLength) + SUBJECT_CROP_APPENDIX;
-    }
-    return subject;
-  }
-
   public static String status(Change c) {
     return c != null ? c.getStatus().name().toLowerCase() : "deleted";
   }
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 18d9b3d..99dfbbb 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -502,7 +502,7 @@
   }
 
   private static <T extends Comment> List<T> sort(List<T> comments) {
-    Collections.sort(comments, COMMENT_ORDER);
+    comments.sort(COMMENT_ORDER);
     return comments;
   }
 
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 16546f9..d9a4cae 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.flogger.LazyArgs.lazy;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.AccountCache;
@@ -54,6 +56,8 @@
 
 /** An authenticated user. */
 public class IdentifiedUser extends CurrentUser {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   /** Create an IdentifiedUser, ignoring any per-request state. */
   @Singleton
   public static class GenericFactory {
@@ -375,8 +379,13 @@
     if (effectiveGroups == null) {
       if (authConfig.isIdentityTrustable(state().getExternalIds())) {
         effectiveGroups = groupBackend.membershipsOf(this);
+        logger.atFinest().log(
+            "Known groups of %s: %s", getLoggableName(), lazy(effectiveGroups::getKnownGroups));
       } else {
         effectiveGroups = registeredGroups;
+        logger.atFinest().log(
+            "%s has a non-trusted identity, falling back to %s as known groups",
+            getLoggableName(), lazy(registeredGroups::getKnownGroups));
       }
     }
     return effectiveGroups;
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index c74f9d4..2cde94c 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -154,12 +154,14 @@
   @Override
   public void evict(@Nullable Account.Id accountId) {
     if (accountId != null) {
+      logger.atFine().log("Evict account %d", accountId.get());
       byId.invalidate(accountId);
     }
   }
 
   @Override
   public void evictAll() {
+    logger.atFine().log("Evict all accounts");
     byId.invalidateAll();
   }
 
diff --git a/java/com/google/gerrit/server/account/GroupBackends.java b/java/com/google/gerrit/server/account/GroupBackends.java
index 803d491..1b15512 100644
--- a/java/com/google/gerrit/server/account/GroupBackends.java
+++ b/java/com/google/gerrit/server/account/GroupBackends.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import static java.util.Comparator.comparing;
+
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
@@ -25,12 +27,7 @@
 public class GroupBackends {
 
   public static final Comparator<GroupReference> GROUP_REF_NAME_COMPARATOR =
-      new Comparator<GroupReference>() {
-        @Override
-        public int compare(GroupReference a, GroupReference b) {
-          return a.getName().compareTo(b.getName());
-        }
-      };
+      comparing(GroupReference::getName);
 
   /**
    * Runs {@link GroupBackend#suggest(String, ProjectState)} and filters the result to return the
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 06b51a7..1f8cb88 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -116,6 +116,7 @@
   @Override
   public void evict(AccountGroup.Id groupId) {
     if (groupId != null) {
+      logger.atFine().log("Evict group %s by ID", groupId.get());
       byId.invalidate(groupId);
     }
   }
@@ -123,6 +124,7 @@
   @Override
   public void evict(AccountGroup.NameKey groupName) {
     if (groupName != null) {
+      logger.atFine().log("Evict group '%s' by name", groupName.get());
       byName.invalidate(groupName.get());
     }
   }
@@ -130,6 +132,7 @@
   @Override
   public void evict(AccountGroup.UUID groupUuid) {
     if (groupUuid != null) {
+      logger.atFine().log("Evict group %s by UUID", groupUuid.get());
       byUUID.invalidate(groupUuid.get());
     }
   }
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 5906a06..5fb4ca1 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -112,6 +112,7 @@
   @Override
   public void evictGroupsWithMember(Account.Id memberId) {
     if (memberId != null) {
+      logger.atFine().log("Evict groups with member %d", memberId.get());
       groupsWithMember.invalidate(memberId);
     }
   }
@@ -119,9 +120,11 @@
   @Override
   public void evictParentGroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
+      logger.atFine().log("Evict parent groups of %s", groupId.get());
       parentGroups.invalidate(groupId);
 
       if (!AccountGroup.isInternalGroup(groupId)) {
+        logger.atFine().log("Evict external group %s", groupId.get());
         external.invalidate(EXTERNAL_NAME);
       }
     }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index db8ea41..96ea0cc 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -47,14 +47,12 @@
   // corresponding regular expressions in the
   // com.google.gerrit.client.account.UsernameField class.
   private static final String USER_NAME_PATTERN_FIRST_REGEX = "[a-zA-Z0-9]";
-  private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9._@-]";
+  private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9.!#$%&’*+=?^_`\\{|\\}~@-]";
   private static final String USER_NAME_PATTERN_LAST_REGEX = "[a-zA-Z0-9]";
 
   /** Regular expression that a username must match. */
   private static final String USER_NAME_PATTERN_REGEX =
-      "^"
-          + //
-          "("
+      "^("
           + //
           USER_NAME_PATTERN_FIRST_REGEX
           + //
@@ -67,9 +65,7 @@
           + //
           USER_NAME_PATTERN_FIRST_REGEX
           + //
-          ")"
-          + //
-          "$";
+          ")$";
 
   private static final Pattern USER_NAME_PATTERN = Pattern.compile(USER_NAME_PATTERN_REGEX);
 
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index 11f2034..3435652 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -18,6 +18,7 @@
 import com.google.common.cache.CacheStats;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.metrics.CallbackMetric;
 import com.google.gerrit.metrics.CallbackMetric1;
 import com.google.gerrit.metrics.Description;
@@ -95,7 +96,7 @@
   }
 
   private static String metricNameOf(DynamicMap.Entry<Cache<?, ?>> e) {
-    if ("gerrit".equals(e.getPluginName())) {
+    if (PluginName.GERRIT.equals(e.getPluginName())) {
       return e.getExportName();
     }
     return String.format("plugin/%s/%s", e.getPluginName(), e.getExportName());
diff --git a/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java b/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
index be06601..a7fdbbd 100644
--- a/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
+++ b/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
@@ -18,6 +18,7 @@
 import com.google.common.cache.RemovalListener;
 import com.google.common.cache.RemovalNotification;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -36,7 +37,7 @@
 
   private final DynamicSet<CacheRemovalListener> listeners;
   private final String cacheName;
-  private String pluginName = "gerrit";
+  private String pluginName = PluginName.GERRIT;
 
   @Inject
   ForwardingRemovalListener(
diff --git a/java/com/google/gerrit/server/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index 2ce756b..f6418e3 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -9,6 +9,7 @@
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/serialize",
+        "//java/com/google/gerrit/server/logging",
         "//lib:guava",
         "//lib:h2",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index a7824ea..af1228d 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -28,7 +28,8 @@
 import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
+import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -75,20 +76,17 @@
 
     if (cacheDir != null) {
       executor =
-          Executors.newFixedThreadPool(
-              1,
-              new ThreadFactoryBuilder()
-                  .setThreadFactory(new LoggingContextAwareThreadFactory())
-                  .setNameFormat("DiskCache-Store-%d")
-                  .build());
+          new LoggingContextAwareExecutorService(
+              Executors.newFixedThreadPool(
+                  1, new ThreadFactoryBuilder().setNameFormat("DiskCache-Store-%d").build()));
       cleanup =
-          Executors.newScheduledThreadPool(
-              1,
-              new ThreadFactoryBuilder()
-                  .setThreadFactory(new LoggingContextAwareThreadFactory())
-                  .setNameFormat("DiskCache-Prune-%d")
-                  .setDaemon(true)
-                  .build());
+          new LoggingContextAwareScheduledExecutorService(
+              Executors.newScheduledThreadPool(
+                  1,
+                  new ThreadFactoryBuilder()
+                      .setNameFormat("DiskCache-Prune-%d")
+                      .setDaemon(true)
+                      .build()));
     } else {
       executor = null;
       cleanup = null;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index e8c55e8..38c97f7 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -552,8 +552,6 @@
       return;
     }
 
-    PermissionBackend.ForRef perm =
-        permissionBackend.user(ctx.getUser()).project(ctx.getProject()).ref(refName);
     try {
       try (CommitReceivedEvent event =
           new CommitReceivedEvent(
@@ -565,7 +563,7 @@
               ctx.getIdentifiedUser())) {
         commitValidatorsFactory
             .forGerritCommits(
-                perm,
+                permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
                 new Branch.NameKey(ctx.getProject(), refName),
                 ctx.getIdentifiedUser(),
                 new NoSshInfo(),
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index e02f666..173d1da 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -1405,8 +1405,7 @@
         out.commitWithFooters =
             mergeUtilFactory
                 .create(projectCache.get(project))
-                .createCommitMessageOnSubmit(
-                    commit, mergeTip, cd.notes(), userProvider.get(), in.getId());
+                .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.getId());
       }
     }
 
diff --git a/java/com/google/gerrit/server/change/IncludedIn.java b/java/com/google/gerrit/server/change/IncludedIn.java
index 8f8925a..d5d54ec 100644
--- a/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/java/com/google/gerrit/server/change/IncludedIn.java
@@ -63,13 +63,13 @@
       ListMultimap<String, String> external = MultimapBuilder.hashKeys().arrayListValues().build();
       for (ExternalIncludedIn ext : externalIncludedIn) {
         ListMultimap<String, String> extIncludedIns =
-            ext.getIncludedIn(project.get(), rev.name(), d.getTags(), d.getBranches());
+            ext.getIncludedIn(project.get(), rev.name(), d.tags(), d.branches());
         if (extIncludedIns != null) {
           external.putAll(extIncludedIns);
         }
       }
       return new IncludedInInfo(
-          d.getBranches(), d.getTags(), (!external.isEmpty() ? external.asMap() : null));
+          d.branches(), d.tags(), (!external.isEmpty() ? external.asMap() : null));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java
index d1bc0a2..62e9454 100644
--- a/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -14,6 +14,13 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.naturalOrder;
+import static java.util.stream.Collectors.toList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -22,7 +29,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -93,11 +99,9 @@
     parseCommits(allTagsAndBranches);
     Set<String> allMatchingTagsAndBranches = includedIn(tipsByCommitTime, 0);
 
-    Result detail = new Result();
-    detail.setBranches(getMatchingRefNames(allMatchingTagsAndBranches, branches));
-    detail.setTags(getMatchingRefNames(allMatchingTagsAndBranches, tags));
-
-    return detail;
+    return new AutoValue_IncludedInResolver_Result(
+        getMatchingRefNames(allMatchingTagsAndBranches, branches),
+        getMatchingRefNames(allMatchingTagsAndBranches, tags));
   }
 
   private boolean includedInOne(Collection<Ref> refs) throws IOException {
@@ -151,15 +155,7 @@
    */
   private void partition(List<RevCommit> before, List<RevCommit> after) {
     int insertionPoint =
-        Collections.binarySearch(
-            tipsByCommitTime,
-            target,
-            new Comparator<RevCommit>() {
-              @Override
-              public int compare(RevCommit c1, RevCommit c2) {
-                return c1.getCommitTime() - c2.getCommitTime();
-              }
-            });
+        Collections.binarySearch(tipsByCommitTime, target, comparing(RevCommit::getCommitTime));
     if (insertionPoint < 0) {
       insertionPoint = -(insertionPoint + 1);
     }
@@ -175,15 +171,14 @@
    * Returns the short names of refs which are as well in the matchingRefs list as well as in the
    * allRef list.
    */
-  private static List<String> getMatchingRefNames(
+  private static ImmutableSortedSet<String> getMatchingRefNames(
       Set<String> matchingRefs, Collection<Ref> allRefs) {
-    List<String> refNames = Lists.newArrayListWithCapacity(matchingRefs.size());
-    for (Ref r : allRefs) {
-      if (matchingRefs.contains(r.getName())) {
-        refNames.add(Repository.shortenRefName(r.getName()));
-      }
-    }
-    return refNames;
+    return allRefs
+        .stream()
+        .map(Ref::getName)
+        .filter(matchingRefs::contains)
+        .map(Repository::shortenRefName)
+        .collect(toImmutableSortedSet(naturalOrder()));
   }
 
   /** Parse commit of ref and store the relation between ref and commit. */
@@ -211,43 +206,14 @@
       }
       commitToRef.put(commit, ref.getName());
     }
-    tipsByCommitTime = Lists.newArrayList(commitToRef.keySet());
-    sortOlderFirst(tipsByCommitTime);
+    tipsByCommitTime =
+        commitToRef.keySet().stream().sorted(comparing(RevCommit::getCommitTime)).collect(toList());
   }
 
-  private void sortOlderFirst(List<RevCommit> tips) {
-    Collections.sort(
-        tips,
-        new Comparator<RevCommit>() {
-          @Override
-          public int compare(RevCommit c1, RevCommit c2) {
-            return c1.getCommitTime() - c2.getCommitTime();
-          }
-        });
-  }
+  @AutoValue
+  public abstract static class Result {
+    public abstract ImmutableSortedSet<String> branches();
 
-  public static class Result {
-    private List<String> branches;
-    private List<String> tags;
-
-    public Result() {}
-
-    public void setBranches(List<String> b) {
-      Collections.sort(b);
-      branches = b;
-    }
-
-    public List<String> getBranches() {
-      return branches;
-    }
-
-    public void setTags(List<String> t) {
-      Collections.sort(t);
-      tags = t;
-    }
-
-    public List<String> getTags() {
-      return tags;
-    }
+    public abstract ImmutableSortedSet<String> tags();
   }
 }
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index d71a93d..8bd6c17 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -323,9 +323,6 @@
       return;
     }
 
-    PermissionBackend.ForRef perm =
-        permissionBackend.user(ctx.getUser()).ref(origNotes.getChange().getDest());
-
     String refName = getPatchSetId().toRefName();
     try (CommitReceivedEvent event =
         new CommitReceivedEvent(
@@ -340,7 +337,7 @@
             ctx.getIdentifiedUser())) {
       commitValidatorsFactory
           .forGerritCommits(
-              perm,
+              permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
               origNotes.getChange().getDest(),
               ctx.getIdentifiedUser(),
               new NoSshInfo(),
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 909ea3a..1f216f0 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -165,8 +165,7 @@
       rw.parseBody(baseCommit);
       newCommitMessage =
           newMergeUtil()
-              .createCommitMessageOnSubmit(
-                  original, baseCommit, notes, changeOwner, originalPatchSet.getId());
+              .createCommitMessageOnSubmit(original, baseCommit, notes, originalPatchSet.getId());
     } else {
       newCommitMessage = original.getFullMessage();
     }
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index cff1ac7..916a62b 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -34,7 +34,6 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.List;
@@ -110,7 +109,7 @@
     for (Map.Entry<Project.NameKey, Collection<ChangeData>> e : byProject.asMap().entrySet()) {
       sortedByProject.add(sortProject(e.getKey(), e.getValue()));
     }
-    Collections.sort(sortedByProject, PROJECT_LIST_SORTER);
+    sortedByProject.sort(PROJECT_LIST_SORTER);
     return Iterables.concat(sortedByProject);
   }
 
diff --git a/java/com/google/gerrit/server/config/CacheResource.java b/java/com/google/gerrit/server/config/CacheResource.java
index 16c7508..ffa7b5a 100644
--- a/java/com/google/gerrit/server/config/CacheResource.java
+++ b/java/com/google/gerrit/server/config/CacheResource.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
@@ -52,7 +53,7 @@
   }
 
   public static String cacheNameOf(String plugin, String name) {
-    if ("gerrit".equals(plugin)) {
+    if (PluginName.GERRIT.equals(plugin)) {
       return name;
     }
     return plugin + "-" + name;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index b6a257b..0761d2e 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -170,6 +170,7 @@
 import com.google.gerrit.server.restapi.change.SuggestReviewers;
 import com.google.gerrit.server.restapi.group.GroupModule;
 import com.google.gerrit.server.rules.DefaultSubmitRule;
+import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
 import com.google.gerrit.server.rules.PrologModule;
 import com.google.gerrit.server.rules.RulesCache;
 import com.google.gerrit.server.rules.SubmitRule;
@@ -244,6 +245,7 @@
     install(new NoteDbModule(cfg));
     install(new PrologModule());
     install(new DefaultSubmitRule.Module());
+    install(new IgnoreSelfApprovalRule.Module());
     install(new ReceiveCommitsModule());
     install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
diff --git a/java/com/google/gerrit/server/config/SysExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
index 2e97a58..f552434 100644
--- a/java/com/google/gerrit/server/config/SysExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -19,7 +19,7 @@
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
@@ -83,18 +83,18 @@
       return MoreExecutors.newDirectExecutorService();
     }
     return MoreExecutors.listeningDecorator(
-        MoreExecutors.getExitingExecutorService(
-            new ThreadPoolExecutor(
-                1,
-                poolSize,
-                10,
-                TimeUnit.MINUTES,
-                new ArrayBlockingQueue<Runnable>(poolSize),
-                new ThreadFactoryBuilder()
-                    .setThreadFactory(new LoggingContextAwareThreadFactory())
-                    .setNameFormat("ChangeUpdate-%d")
-                    .setDaemon(true)
-                    .build(),
-                new ThreadPoolExecutor.CallerRunsPolicy())));
+        new LoggingContextAwareExecutorService(
+            MoreExecutors.getExitingExecutorService(
+                new ThreadPoolExecutor(
+                    1,
+                    poolSize,
+                    10,
+                    TimeUnit.MINUTES,
+                    new ArrayBlockingQueue<Runnable>(poolSize),
+                    new ThreadFactoryBuilder()
+                        .setNameFormat("ChangeUpdate-%d")
+                        .setDaemon(true)
+                        .build(),
+                    new ThreadPoolExecutor.CallerRunsPolicy()))));
   }
 }
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 2fbc1c7..fbce4b2 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -73,7 +73,6 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -329,10 +328,9 @@
       }
     }
     // Sort by original parent order.
-    Collections.sort(
-        ca.dependsOn,
+    ca.dependsOn.sort(
         comparing(
-            (DependencyAttribute d) -> {
+            d -> {
               for (int i = 0; i < parentNames.size(); i++) {
                 if (parentNames.get(i).equals(d.revision)) {
                   return i;
diff --git a/java/com/google/gerrit/server/extensions/webui/UiActions.java b/java/com/google/gerrit/server/extensions/webui/UiActions.java
index f8cb4ce..af28bed3 100644
--- a/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -169,7 +170,7 @@
 
     PrivateInternals_UiActionDescription.setMethod(dsc, e.getExportName().substring(0, d));
     PrivateInternals_UiActionDescription.setId(
-        dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name);
+        dsc, PluginName.GERRIT.equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name);
     return dsc;
   }
 }
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index 1c87a63..513d909 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -14,12 +14,16 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
 
 /** Print a change description for use in git command-line progress. */
 public class DefaultChangeReportFormatter implements ChangeReportFormatter {
+  private static final int SUBJECT_MAX_LENGTH = 80;
+  private static final String SUBJECT_CROP_APPENDIX = "...";
+  private static final int SUBJECT_CROP_RANGE = 10;
+
   private final String canonicalWebUrl;
 
   @Inject
@@ -37,19 +41,37 @@
     return formatChangeUrl(canonicalWebUrl, input);
   }
 
-  @Override
-  public String changeClosed(ChangeReportFormatter.Input input) {
-    return String.format(
-        "change %s closed", ChangeUtil.formatChangeUrl(canonicalWebUrl, input.change()));
+  public static String formatChangeUrl(String canonicalWebUrl, Change change) {
+    return canonicalWebUrl + "c/" + change.getProject().get() + "/+/" + change.getChangeId();
   }
 
-  private String formatChangeUrl(String url, Input input) {
+  @Override
+  public String changeClosed(ChangeReportFormatter.Input input) {
+    return String.format("change %s closed", formatChangeUrl(canonicalWebUrl, input.change()));
+  }
+
+  protected String cropSubject(String subject) {
+    if (subject.length() > SUBJECT_MAX_LENGTH) {
+      int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length();
+      for (int cropPosition = maxLength;
+          cropPosition > maxLength - SUBJECT_CROP_RANGE;
+          cropPosition--) {
+        if (Character.isWhitespace(subject.charAt(cropPosition - 1))) {
+          return subject.substring(0, cropPosition) + SUBJECT_CROP_APPENDIX;
+        }
+      }
+      return subject.substring(0, maxLength) + SUBJECT_CROP_APPENDIX;
+    }
+    return subject;
+  }
+
+  protected String formatChangeUrl(String url, Input input) {
     StringBuilder m =
         new StringBuilder()
             .append("  ")
-            .append(ChangeUtil.formatChangeUrl(url, input.change()))
+            .append(formatChangeUrl(url, input.change()))
             .append(" ")
-            .append(ChangeUtil.cropSubject(input.subject()));
+            .append(cropSubject(input.subject()));
     if (input.isEdit()) {
       m.append(" [EDIT]");
     }
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 0231378..c035269 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -41,7 +41,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -220,7 +219,7 @@
     } catch (IOException e) {
       throw new IntegrationException("Branch head sorting failed", e);
     }
-    Collections.sort(result, CodeReviewCommit.ORDER);
+    result.sort(CodeReviewCommit.ORDER);
     return result;
   }
 
@@ -315,12 +314,10 @@
    *
    * @param n
    * @param notes
-   * @param user
    * @param psId
    * @return new message
    */
-  private String createDetailedCommitMessage(
-      RevCommit n, ChangeNotes notes, CurrentUser user, PatchSet.Id psId) {
+  private String createDetailedCommitMessage(RevCommit n, ChangeNotes notes, PatchSet.Id psId) {
     Change c = notes.getChange();
     final List<FooterLine> footers = n.getFooterLines();
     final StringBuilder msgbuf = new StringBuilder();
@@ -424,12 +421,7 @@
   }
 
   public String createCommitMessageOnSubmit(CodeReviewCommit n, RevCommit mergeTip) {
-    return createCommitMessageOnSubmit(
-        n,
-        mergeTip,
-        n.notes(),
-        identifiedUserFactory.create(n.notes().getChange().getOwner()),
-        n.getPatchsetId());
+    return createCommitMessageOnSubmit(n, mergeTip, n.notes(), n.getPatchsetId());
   }
 
   /**
@@ -442,14 +434,13 @@
    * @param n
    * @param mergeTip
    * @param notes
-   * @param user
    * @param id
    * @return new message
    */
   public String createCommitMessageOnSubmit(
-      RevCommit n, RevCommit mergeTip, ChangeNotes notes, CurrentUser user, Id id) {
+      RevCommit n, RevCommit mergeTip, ChangeNotes notes, Id id) {
     return commitMessageGenerator.generate(
-        n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, user, id));
+        n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id));
   }
 
   private static boolean isCodeReview(LabelId id) {
diff --git a/java/com/google/gerrit/server/git/TransferConfig.java b/java/com/google/gerrit/server/git/TransferConfig.java
index f85f24b..8c93833 100644
--- a/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/java/com/google/gerrit/server/git/TransferConfig.java
@@ -28,6 +28,7 @@
   private final PackConfig packConfig;
   private final long maxObjectSizeLimit;
   private final String maxObjectSizeLimitFormatted;
+  private final boolean inheritProjectMaxObjectSizeLimit;
 
   @Inject
   TransferConfig(@GerritServerConfig Config cfg) {
@@ -42,6 +43,8 @@
                 TimeUnit.SECONDS);
     maxObjectSizeLimit = cfg.getLong("receive", "maxObjectSizeLimit", 0);
     maxObjectSizeLimitFormatted = cfg.getString("receive", null, "maxObjectSizeLimit");
+    inheritProjectMaxObjectSizeLimit =
+        cfg.getBoolean("receive", "inheritProjectMaxObjectSizeLimit", false);
 
     packConfig = new PackConfig();
     packConfig.setDeltaCompress(false);
@@ -65,4 +68,8 @@
   public String getFormattedMaxObjectSizeLimit() {
     return maxObjectSizeLimitFormatted;
   }
+
+  public boolean getInheritProjectMaxObjectSizeLimit() {
+    return inheritProjectMaxObjectSizeLimit;
+  }
 }
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index a2c12df..a7336f0 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.CaseFormat;
 import com.google.common.base.Supplier;
 import com.google.common.flogger.FluentLogger;
@@ -24,7 +26,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ScheduleConfig.Schedule;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.logging.LoggingContextAwareRunnable;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -43,6 +46,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.RunnableScheduledFuture;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
@@ -166,12 +170,11 @@
     if (threadPriority != Thread.NORM_PRIORITY) {
       ThreadFactory parent = executor.getThreadFactory();
       executor.setThreadFactory(
-          new LoggingContextAwareThreadFactory(
-              task -> {
-                Thread t = parent.newThread(task);
-                t.setPriority(threadPriority);
-                return t;
-              }));
+          task -> {
+            Thread t = parent.newThread(task);
+            t.setPriority(threadPriority);
+            return t;
+          });
     }
 
     return executor;
@@ -253,19 +256,18 @@
     Executor(int corePoolSize, final String queueName) {
       super(
           corePoolSize,
-          new LoggingContextAwareThreadFactory(
-              new ThreadFactory() {
-                private final ThreadFactory parent = Executors.defaultThreadFactory();
-                private final AtomicInteger tid = new AtomicInteger(1);
+          new ThreadFactory() {
+            private final ThreadFactory parent = Executors.defaultThreadFactory();
+            private final AtomicInteger tid = new AtomicInteger(1);
 
-                @Override
-                public Thread newThread(Runnable task) {
-                  final Thread t = parent.newThread(task);
-                  t.setName(queueName + "-" + tid.getAndIncrement());
-                  t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
-                  return t;
-                }
-              }));
+            @Override
+            public Thread newThread(Runnable task) {
+              final Thread t = parent.newThread(task);
+              t.setName(queueName + "-" + tid.getAndIncrement());
+              t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
+              return t;
+            }
+          });
 
       all =
           new ConcurrentHashMap<>( //
@@ -277,6 +279,75 @@
     }
 
     @Override
+    public void execute(Runnable command) {
+      super.execute(LoggingContext.copy(command));
+    }
+
+    @Override
+    public <T> Future<T> submit(Callable<T> task) {
+      return super.submit(LoggingContext.copy(task));
+    }
+
+    @Override
+    public <T> Future<T> submit(Runnable task, T result) {
+      return super.submit(LoggingContext.copy(task), result);
+    }
+
+    @Override
+    public Future<?> submit(Runnable task) {
+      return super.submit(LoggingContext.copy(task));
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
+        throws InterruptedException {
+      return super.invokeAll(tasks.stream().map(LoggingContext::copy).collect(toList()));
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(
+        Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+        throws InterruptedException {
+      return super.invokeAll(
+          tasks.stream().map(LoggingContext::copy).collect(toList()), timeout, unit);
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
+        throws InterruptedException, ExecutionException {
+      return super.invokeAny(tasks.stream().map(LoggingContext::copy).collect(toList()));
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+        throws InterruptedException, ExecutionException, TimeoutException {
+      return super.invokeAny(
+          tasks.stream().map(LoggingContext::copy).collect(toList()), timeout, unit);
+    }
+
+    @Override
+    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+      return super.schedule(LoggingContext.copy(command), delay, unit);
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+      return super.schedule(LoggingContext.copy(callable), delay, unit);
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleAtFixedRate(
+        Runnable command, long initialDelay, long period, TimeUnit unit) {
+      return super.scheduleAtFixedRate(LoggingContext.copy(command), initialDelay, period, unit);
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleWithFixedDelay(
+        Runnable command, long initialDelay, long delay, TimeUnit unit) {
+      return super.scheduleWithFixedDelay(LoggingContext.copy(command), initialDelay, delay, unit);
+    }
+
+    @Override
     protected void terminated() {
       super.terminated();
       queues.remove(this);
@@ -370,6 +441,10 @@
 
         Task<V> task;
 
+        if (runnable instanceof LoggingContextAwareRunnable) {
+          runnable = ((LoggingContextAwareRunnable) runnable).unwrap();
+        }
+
         if (runnable instanceof ProjectRunnable) {
           task = new ProjectTask<>((ProjectRunnable) runnable, r, this, id);
         } else {
diff --git a/java/com/google/gerrit/server/git/meta/TabFile.java b/java/com/google/gerrit/server/git/meta/TabFile.java
index ef25cd8..4c0378a 100644
--- a/java/com/google/gerrit/server/git/meta/TabFile.java
+++ b/java/com/google/gerrit/server/git/meta/TabFile.java
@@ -14,13 +14,15 @@
 
 package com.google.gerrit.server.git.meta;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.git.ValidationError;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -122,10 +124,8 @@
     return buf.toString();
   }
 
-  protected static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
-    ArrayList<T> r = new ArrayList<>(m);
-    Collections.sort(r);
-    return r;
+  protected static <T extends Comparable<? super T>> ImmutableList<T> sort(Collection<T> m) {
+    return m.stream().sorted().collect(toImmutableList());
   }
 
   protected static String pad(int len, String src) {
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 7fe0c04..eb62d54 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -94,6 +94,7 @@
       // Don't expose the binding for ReceiveCommits.Factory. All callers should
       // be using AsyncReceiveCommits.Factory instead.
       install(new FactoryModuleBuilder().build(ReceiveCommits.Factory.class));
+      install(new FactoryModuleBuilder().build(BranchCommitValidator.Factory.class));
     }
 
     @Provides
@@ -224,7 +225,7 @@
     receivePack.setAllowNonFastForwards(true);
     receivePack.setRefLogIdent(user.newRefLogIdent());
     receivePack.setTimeout(transferConfig.getTimeout());
-    receivePack.setMaxObjectSizeLimit(projectState.getEffectiveMaxObjectSizeLimit());
+    receivePack.setMaxObjectSizeLimit(projectState.getEffectiveMaxObjectSizeLimit().value);
     receivePack.setCheckReceivedObjects(projectState.getConfig().getCheckReceivedObjects());
     receivePack.setRefFilter(new ReceiveRefFilter());
     receivePack.setAllowPushOptions(true);
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index fddb9d6..f1c604b 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/util/cli",
         "//lib:args4j",
         "//lib:guava",
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
new file mode 100644
index 0000000..24b6ab1
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2018 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.git.receive;
+
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Validates single commits for a branch. */
+public class BranchCommitValidator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final IdentifiedUser user;
+  private final PermissionBackend.ForProject permissions;
+  private final Project project;
+  private final Branch.NameKey branch;
+  private final SshInfo sshInfo;
+
+  interface Factory {
+    BranchCommitValidator create(
+        ProjectState projectState, Branch.NameKey branch, IdentifiedUser user);
+  }
+
+  @Inject
+  BranchCommitValidator(
+      CommitValidators.Factory commitValidatorsFactory,
+      PermissionBackend permissionBackend,
+      SshInfo sshInfo,
+      @Assisted ProjectState projectState,
+      @Assisted Branch.NameKey branch,
+      @Assisted IdentifiedUser user) {
+    this.sshInfo = sshInfo;
+    this.user = user;
+    this.branch = branch;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    project = projectState.getProject();
+    permissions = permissionBackend.user(user).project(project.getNameKey());
+  }
+
+  /**
+   * Validates a single commit. If the commit does not validate, the command is rejected.
+   *
+   * @param objectReader the object reader to use.
+   * @param cmd the ReceiveCommand executing the push.
+   * @param commit the commit being validated.
+   * @param isMerged whether this is a merge commit created by magicBranch --merge option
+   * @param change the change for which this is a new patchset.
+   */
+  public boolean validCommit(
+      ObjectReader objectReader,
+      ReceiveCommand cmd,
+      RevCommit commit,
+      boolean isMerged,
+      List<ValidationMessage> messages,
+      NoteMap rejectCommits,
+      @Nullable Change change)
+      throws IOException {
+    try (CommitReceivedEvent receiveEvent =
+        new CommitReceivedEvent(cmd, project, branch.get(), objectReader, commit, user)) {
+      CommitValidators validators;
+      if (isMerged) {
+        validators =
+            commitValidatorsFactory.forMergedCommits(permissions, branch, user.asIdentifiedUser());
+      } else {
+        validators =
+            commitValidatorsFactory.forReceiveCommits(
+                permissions,
+                branch,
+                user.asIdentifiedUser(),
+                sshInfo,
+                rejectCommits,
+                receiveEvent.revWalk,
+                change);
+      }
+
+      for (CommitValidationMessage m : validators.validate(receiveEvent)) {
+        messages.add(
+            new CommitValidationMessage(messageForCommit(commit, m.getMessage()), m.isError()));
+      }
+    } catch (CommitValidationException e) {
+      logger.atFine().log("Commit validation failed on %s", commit.name());
+      for (CommitValidationMessage m : e.getMessages()) {
+        // The non-error messages may contain background explanation for the
+        // fatal error, so have to preserve all messages.
+        messages.add(
+            new CommitValidationMessage(messageForCommit(commit, m.getMessage()), m.isError()));
+      }
+      cmd.setResult(REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage()));
+      return false;
+    }
+    return true;
+  }
+
+  private String messageForCommit(RevCommit c, String msg) {
+    return String.format("commit %s: %s", c.abbreviate(RevId.ABBREV_LEN).name(), msg);
+  }
+}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 4632083..4b475f9 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -39,7 +39,6 @@
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
@@ -94,10 +93,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
-import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.SetHashtagsOp;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -106,7 +102,6 @@
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
-import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.ChangeReportFormatter;
 import com.google.gerrit.server.git.GroupCollector;
@@ -116,9 +111,7 @@
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.ValidationError;
-import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.git.validators.RefOperationValidationException;
 import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.git.validators.ValidationMessage;
@@ -145,7 +138,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.submit.MergeOp;
 import com.google.gerrit.server.submit.MergeOpRepoManager;
 import com.google.gerrit.server.submit.SubmoduleException;
@@ -218,38 +210,16 @@
  *
  * <p>Conceptually, most use of Gerrit is a push of some commits to refs/for/BRANCH. However, the
  * receive-pack protocol that this is based on allows multiple ref updates to be processed at once.
+ * So we have to be prepared to also handle normal pushes (refs/heads/BRANCH), and legacy pushes
+ * (refs/changes/CHANGE). It is hard to split this class up further, because normal pushes can also
+ * result in updates to reviews, through the autoclose mechanism.
  */
 class ReceiveCommits {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private enum ReceiveError {
-    CONFIG_UPDATE(
-        "You are not allowed to perform this operation.\n"
-            + "Configuration changes can only be pushed by project owners\n"
-            + "who also have 'Push' rights on "
-            + RefNames.REFS_CONFIG),
-    UPDATE(
-        "You are not allowed to perform this operation.\n"
-            + "To push into this reference you need 'Push' rights."),
-    DELETE(
-        "You need 'Delete Reference' rights or 'Push' rights with the \n"
-            + "'Force Push' flag set to delete references."),
-    DELETE_CHANGES("Cannot delete from '" + REFS_CHANGES + "'"),
-    CODE_REVIEW(
-        "You need 'Push' rights to upload code review requests.\n"
-            + "Verify that you are pushing to the right branch.");
-
-    private final String value;
-
-    ReceiveError(String value) {
-      this.value = value;
-    }
-
-    String get() {
-      return value;
-    }
-  }
-
+  private static final String CODE_REVIEW_ERROR =
+      "You need 'Push' rights to upload code review requests.\n"
+          + "Verify that you are pushing to the right branch.";
   private static final String CANNOT_DELETE_CHANGES = "Cannot delete from '" + REFS_CHANGES + "'";
   private static final String CANNOT_DELETE_CONFIG =
       "Cannot delete project configuration from '" + RefNames.REFS_CONFIG + "'";
@@ -319,7 +289,6 @@
 
   // Injected fields.
   private final AccountResolver accountResolver;
-  private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final AllProjectsName allProjectsName;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final ChangeEditUtil editUtil;
@@ -328,7 +297,7 @@
   private final ChangeNotes.Factory notesFactory;
   private final ChangeReportFormatter changeFormatter;
   private final CmdLineParser.Factory optionParserFactory;
-  private final CommitValidators.Factory commitValidatorsFactory;
+  private final BranchCommitValidator.Factory commitValidatorFactory;
   private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
   private final CreateRefControl createRefControl;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
@@ -350,7 +319,6 @@
   private final ReviewDb db;
   private final Sequences seq;
   private final SetHashtagsOp.Factory hashtagsFactory;
-  private final SshInfo sshInfo;
   private final SubmoduleOp.Factory subOpFactory;
   private final TagCache tagCache;
 
@@ -379,15 +347,6 @@
   private final ListMultimap<String, String> pushOptions;
   private final Map<Change.Id, ReplaceRequest> replaceByChange;
 
-  @AutoValue
-  protected abstract static class ValidCommitKey {
-    abstract ObjectId getObjectId();
-
-    abstract Branch.NameKey getBranch();
-  }
-
-  private final Set<ValidCommitKey> validCommits;
-
   // Collections lazily populated during processing.
   private ListMultimap<Change.Id, Ref> refsByChange;
   private ListMultimap<ObjectId, Ref> refsById;
@@ -395,17 +354,15 @@
   // Other settings populated during processing.
   private MagicBranchInput magicBranch;
   private boolean newChangeForAllNotInTarget;
-  private String setFullNameTo;
   private boolean setChangeAsPrivate;
   private Optional<NoteDbPushOption> noteDbPushOption;
-  private Optional<Boolean> tracePushOption;
+  private Optional<String> tracePushOption;
 
   private MessageSender messageSender;
 
   @Inject
   ReceiveCommits(
       AccountResolver accountResolver,
-      @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       AllProjectsName allProjectsName,
       BatchUpdate.Factory batchUpdateFactory,
       @GerritServerConfig Config cfg,
@@ -415,7 +372,7 @@
       ChangeNotes.Factory notesFactory,
       DynamicItem<ChangeReportFormatter> changeFormatterProvider,
       CmdLineParser.Factory optionParserFactory,
-      CommitValidators.Factory commitValidatorsFactory,
+      BranchCommitValidator.Factory commitValidatorFactory,
       CreateGroupPermissionSyncer createGroupPermissionSyncer,
       CreateRefControl createRefControl,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
@@ -437,7 +394,6 @@
       ReviewDb db,
       Sequences seq,
       SetHashtagsOp.Factory hashtagsFactory,
-      SshInfo sshInfo,
       SubmoduleOp.Factory subOpFactory,
       TagCache tagCache,
       @Assisted ProjectState projectState,
@@ -449,12 +405,11 @@
       throws IOException {
     // Injected fields.
     this.accountResolver = accountResolver;
-    this.accountsUpdateProvider = accountsUpdateProvider;
     this.allProjectsName = allProjectsName;
     this.batchUpdateFactory = batchUpdateFactory;
     this.changeFormatter = changeFormatterProvider.get();
     this.changeInserterFactory = changeInserterFactory;
-    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.commitValidatorFactory = commitValidatorFactory;
     this.createRefControl = createRefControl;
     this.createGroupPermissionSyncer = createGroupPermissionSyncer;
     this.db = db;
@@ -480,7 +435,6 @@
     this.retryHelper = retryHelper;
     this.requestScopePropagator = requestScopePropagator;
     this.seq = seq;
-    this.sshInfo = sshInfo;
     this.subOpFactory = subOpFactory;
     this.tagCache = tagCache;
 
@@ -505,7 +459,6 @@
     pushOptions = LinkedListMultimap.create();
     replaceByChange = new LinkedHashMap<>();
     updateGroups = new ArrayList<>();
-    validCommits = new HashSet<>();
 
     this.allowProjectOwnersToChangeParent =
         cfg.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
@@ -567,10 +520,6 @@
 
     commandProgress.end();
     progress.end();
-
-    // Update account info with details discovered during commit walking. The account update happens
-    // in a separate batch update, and failure doesn't cause the push itself to fail.
-    updateAccountInfo();
   }
 
   // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
@@ -578,52 +527,54 @@
       Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
     parsePushOptions();
     try (TraceContext traceContext =
-        TraceContext.open()
-            .addTag(RequestId.Type.RECEIVE_ID, RequestId.forProject(project.getNameKey()))) {
-      if (tracePushOption.orElse(false)) {
-        RequestId traceId = new RequestId();
-        traceContext.forceLogging().addTag(RequestId.Type.TRACE_ID, traceId);
-        addMessage(RequestId.Type.TRACE_ID.name() + ": " + traceId);
+        TraceContext.newTrace(
+            tracePushOption.isPresent(),
+            tracePushOption.orElse(null),
+            (tagName, traceId) -> addMessage(tagName + ": " + traceId))) {
+      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
+      // into the trace if tracing is enabled.
+      logger.atFine().log("push options: %s", receivePack.getPushOptions());
+
+      if (!projectState.getProject().getState().permitsWrite()) {
+        for (ReceiveCommand cmd : commands) {
+          reject(cmd, "prohibited by Gerrit: project state does not permit write");
+        }
+        return;
+      }
+
+      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);
+
+      if (commandTypes > 1) {
+        for (ReceiveCommand cmd : commands) {
+          if (cmd.getResult() == NOT_ATTEMPTED) {
+            cmd.setResult(REJECTED_OTHER_REASON, "cannot combine normal pushes and magic pushes");
+          }
+        }
+        return;
       }
 
       try {
-        if (!projectState.getProject().getState().permitsWrite()) {
-          for (ReceiveCommand cmd : commands) {
-            reject(cmd, "prohibited by Gerrit: project state does not permit write");
-          }
-          return;
-        }
-
-        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);
-
-        if (commandTypes > 1) {
-          for (ReceiveCommand cmd : commands) {
-            if (cmd.getResult() == NOT_ATTEMPTED) {
-              cmd.setResult(REJECTED_OTHER_REASON, "cannot combine normal pushes and magic pushes");
-            }
-          }
-          return;
-        }
-
         if (!regularCommands.isEmpty()) {
           handleRegularCommands(regularCommands, progress);
           return;
@@ -960,7 +911,7 @@
     List<String> traceValues = pushOptions.get("trace");
     if (!traceValues.isEmpty()) {
       String value = traceValues.get(traceValues.size() - 1);
-      tracePushOption = Optional.of(value.isEmpty() || Boolean.parseBoolean(value));
+      tracePushOption = Optional.of(value);
     } else {
       tracePushOption = Optional.empty();
     }
@@ -1083,131 +1034,139 @@
     }
 
     if (isConfig(cmd)) {
-      logger.atFine().log("Processing %s command", cmd.getRefName());
-      try {
-        permissions.check(ProjectPermission.WRITE_CONFIG);
-      } catch (AuthException e) {
-        reject(
-            cmd,
-            String.format(
-                "must be either project owner or have %s permission",
-                ProjectPermission.WRITE_CONFIG.describeForException()));
-        return;
-      }
+      validateConfigPush(cmd);
+    }
+  }
 
-      switch (cmd.getType()) {
-        case CREATE:
-        case UPDATE:
-        case UPDATE_NONFASTFORWARD:
-          try {
-            ProjectConfig cfg = new ProjectConfig(project.getNameKey());
-            cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
-            if (!cfg.getValidationErrors().isEmpty()) {
-              addError("Invalid project configuration:");
-              for (ValidationError err : cfg.getValidationErrors()) {
-                addError("  " + err.getMessage());
-              }
-              reject(cmd, "invalid project configuration");
-              logger.atSevere().log(
-                  "User %s tried to push invalid project configuration %s for %s",
-                  user.getLoggableName(), cmd.getNewId().name(), project.getName());
-              return;
+  /** Validates a push to refs/meta/config, and reject the command if it fails. */
+  private void validateConfigPush(ReceiveCommand cmd) throws PermissionBackendException {
+    logger.atFine().log("Processing %s command", cmd.getRefName());
+    try {
+      permissions.check(ProjectPermission.WRITE_CONFIG);
+    } catch (AuthException e) {
+      reject(
+          cmd,
+          String.format(
+              "must be either project owner or have %s permission",
+              ProjectPermission.WRITE_CONFIG.describeForException()));
+      return;
+    }
+
+    switch (cmd.getType()) {
+      case CREATE:
+      case UPDATE:
+      case UPDATE_NONFASTFORWARD:
+        try {
+          ProjectConfig cfg = new ProjectConfig(project.getNameKey());
+          cfg.load(project.getNameKey(), receivePack.getRevWalk(), cmd.getNewId());
+          if (!cfg.getValidationErrors().isEmpty()) {
+            addError("Invalid project configuration:");
+            for (ValidationError err : cfg.getValidationErrors()) {
+              addError("  " + err.getMessage());
             }
-            Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
-            Project.NameKey oldParent = project.getParent(allProjectsName);
-            if (oldParent == null) {
-              // update of the 'All-Projects' project
-              if (newParent != null) {
-                reject(cmd, "invalid project configuration: root project cannot have parent");
-                return;
-              }
-            } else {
-              if (!oldParent.equals(newParent)) {
-                if (allowProjectOwnersToChangeParent) {
-                  try {
-                    permissionBackend
-                        .user(user)
-                        .project(project.getNameKey())
-                        .check(ProjectPermission.WRITE_CONFIG);
-                  } catch (AuthException e) {
-                    reject(
-                        cmd, "invalid project configuration: only project owners can set parent");
-                    return;
-                  }
-                } else {
-                  try {
-                    permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
-                  } catch (AuthException e) {
-                    reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
-                    return;
-                  }
-                }
-              }
-
-              if (projectCache.get(newParent) == null) {
-                reject(cmd, "invalid project configuration: parent does not exist");
-                return;
-              }
-            }
-
-            for (Entry<ProjectConfigEntry> e : pluginConfigEntries) {
-              PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName());
-              ProjectConfigEntry configEntry = e.getProvider().get();
-              String value = pluginCfg.getString(e.getExportName());
-              String oldValue =
-                  projectState
-                      .getConfig()
-                      .getPluginConfig(e.getPluginName())
-                      .getString(e.getExportName());
-              if (configEntry.getType() == ProjectConfigEntryType.ARRAY) {
-                oldValue =
-                    Arrays.stream(
-                            projectState
-                                .getConfig()
-                                .getPluginConfig(e.getPluginName())
-                                .getStringList(e.getExportName()))
-                        .collect(joining("\n"));
-              }
-
-              if ((value == null ? oldValue != null : !value.equals(oldValue))
-                  && !configEntry.isEditable(projectState)) {
-                reject(
-                    cmd,
-                    String.format(
-                        "invalid project configuration: Not allowed to set parameter"
-                            + " '%s' of plugin '%s' on project '%s'.",
-                        e.getExportName(), e.getPluginName(), project.getName()));
-                continue;
-              }
-
-              if (ProjectConfigEntryType.LIST.equals(configEntry.getType())
-                  && value != null
-                  && !configEntry.getPermittedValues().contains(value)) {
-                reject(
-                    cmd,
-                    String.format(
-                        "invalid project configuration: The value '%s' is "
-                            + "not permitted for parameter '%s' of plugin '%s'.",
-                        value, e.getExportName(), e.getPluginName()));
-              }
-            }
-          } catch (Exception e) {
             reject(cmd, "invalid project configuration");
-            logger.atSevere().withCause(e).log(
+            logger.atSevere().log(
                 "User %s tried to push invalid project configuration %s for %s",
                 user.getLoggableName(), cmd.getNewId().name(), project.getName());
             return;
           }
-          break;
+          Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
+          Project.NameKey oldParent = project.getParent(allProjectsName);
+          if (oldParent == null) {
+            // update of the 'All-Projects' project
+            if (newParent != null) {
+              reject(cmd, "invalid project configuration: root project cannot have parent");
+              return;
+            }
+          } else {
+            if (!oldParent.equals(newParent)) {
+              if (allowProjectOwnersToChangeParent) {
+                try {
+                  permissionBackend
+                      .user(user)
+                      .project(project.getNameKey())
+                      .check(ProjectPermission.WRITE_CONFIG);
+                } catch (AuthException e) {
+                  reject(cmd, "invalid project configuration: only project owners can set parent");
+                  return;
+                }
+              } else {
+                try {
+                  permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+                } catch (AuthException e) {
+                  reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
+                  return;
+                }
+              }
+            }
 
-        case DELETE:
-          break;
+            if (projectCache.get(newParent) == null) {
+              reject(cmd, "invalid project configuration: parent does not exist");
+              return;
+            }
+          }
+          validatePluginConfig(cmd, cfg);
+        } catch (Exception e) {
+          reject(cmd, "invalid project configuration");
+          logger.atSevere().withCause(e).log(
+              "User %s tried to push invalid project configuration %s for %s",
+              user.getLoggableName(), cmd.getNewId().name(), project.getName());
+          return;
+        }
+        break;
 
-        default:
-          reject(
-              cmd,
-              "prohibited by Gerrit: don't know how to handle config update of type "
-                  + cmd.getType());
+      case DELETE:
+        break;
+
+      default:
+        reject(
+            cmd,
+            "prohibited by Gerrit: don't know how to handle config update of type "
+                + cmd.getType());
+    }
+  }
+
+  /**
+   * validates a push to refs/meta/config for plugin configuration, and rejects the push if it
+   * fails.
+   */
+  private void validatePluginConfig(ReceiveCommand cmd, ProjectConfig cfg) {
+    for (Entry<ProjectConfigEntry> e : pluginConfigEntries) {
+      PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName());
+      ProjectConfigEntry configEntry = e.getProvider().get();
+      String value = pluginCfg.getString(e.getExportName());
+      String oldValue =
+          projectState.getConfig().getPluginConfig(e.getPluginName()).getString(e.getExportName());
+      if (configEntry.getType() == ProjectConfigEntryType.ARRAY) {
+        oldValue =
+            Arrays.stream(
+                    projectState
+                        .getConfig()
+                        .getPluginConfig(e.getPluginName())
+                        .getStringList(e.getExportName()))
+                .collect(joining("\n"));
+      }
+
+      if ((value == null ? oldValue != null : !value.equals(oldValue))
+          && !configEntry.isEditable(projectState)) {
+        reject(
+            cmd,
+            String.format(
+                "invalid project configuration: Not allowed to set parameter"
+                    + " '%s' of plugin '%s' on project '%s'.",
+                e.getExportName(), e.getPluginName(), project.getName()));
+        continue;
+      }
+
+      if (ProjectConfigEntryType.LIST.equals(configEntry.getType())
+          && value != null
+          && !configEntry.getPermittedValues().contains(value)) {
+        reject(
+            cmd,
+            String.format(
+                "invalid project configuration: The value '%s' is "
+                    + "not permitted for parameter '%s' of plugin '%s'.",
+                value, e.getExportName(), e.getPluginName()));
       }
     }
   }
@@ -1381,7 +1340,7 @@
     Set<String> hashtags = new HashSet<>();
 
     @Option(name = "--trace", metaVar = "NAME", usage = "enable tracing")
-    boolean trace;
+    String trace;
 
     @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
     List<ObjectId> base;
@@ -1696,7 +1655,7 @@
     // TODO(davido): Remove legacy support for drafts magic branch option
     // after repo-tool supports private and work-in-progress changes.
     if (magicBranch.draft && !receiveConfig.allowDrafts) {
-      errors.put(ReceiveError.CODE_REVIEW.get(), ref);
+      errors.put(CODE_REVIEW_ERROR, ref);
       reject(cmd, "draft workflow is disabled");
       return;
     }
@@ -1808,7 +1767,7 @@
       return;
     }
 
-    if (validateConnected(magicBranch.dest, tip)) {
+    if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
       this.magicBranch = magicBranch;
     }
   }
@@ -1817,7 +1776,7 @@
   // branch.  If they aren't, we want to abort. We do this check by
   // looking to see if we can compute a merge base between the new
   // commits and the target branch head.
-  private boolean validateConnected(Branch.NameKey dest, RevCommit tip) {
+  private boolean validateConnected(ReceiveCommand cmd, Branch.NameKey dest, RevCommit tip) {
     RevWalk walk = receivePack.getRevWalk();
     try {
       Ref targetRef = receivePack.getAdvertisedRefs().get(dest.get());
@@ -1840,7 +1799,7 @@
         walk.markStart(tip);
         walk.markStart(h);
         if (walk.next() == null) {
-          reject(magicBranch.cmd, "no common ancestry");
+          reject(cmd, "no common ancestry");
           return false;
         }
       } finally {
@@ -1848,7 +1807,7 @@
         walk.setRevFilter(oldRevFilter);
       }
     } catch (IOException e) {
-      magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
+      cmd.setResult(REJECTED_MISSING_OBJECT);
       logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
       return false;
     }
@@ -1908,13 +1867,16 @@
       return;
     }
 
+    BranchCommitValidator validator =
+        commitValidatorFactory.create(projectState, changeEnt.getDest(), user);
     try {
-      if (validCommit(
+      if (validator.validCommit(
           receivePack.getRevWalk().getObjectReader(),
-          changeEnt.getDest(),
           cmd,
           newCommit,
           false,
+          messages,
+          rejectCommits,
           changeEnt)) {
         logger.atFine().log("Replacing change %s", changeEnt.getId());
         requestReplace(cmd, true, changeEnt, newCommit);
@@ -1972,6 +1934,9 @@
     GroupCollector groupCollector =
         GroupCollector.create(changeRefsById(), db, psUtil, notesFactory, project.getNameKey());
 
+    BranchCommitValidator validator =
+        commitValidatorFactory.create(projectState, magicBranch.dest, user);
+
     try {
       RevCommit start = setUpWalkForSelectingChanges();
       if (start == null) {
@@ -2071,12 +2036,13 @@
           logger.atFine().log("Creating new change for %s even though it is already tracked", name);
         }
 
-        if (!validCommit(
+        if (!validator.validCommit(
             receivePack.getRevWalk().getObjectReader(),
-            magicBranch.dest,
             magicBranch.cmd,
             c,
             magicBranch.merged,
+            messages,
+            rejectCommits,
             null)) {
           // Not a change the user can propose? Abort as early as possible.
           logger.atFine().log("Aborting early due to invalid commit");
@@ -3036,7 +3002,7 @@
       return;
     }
 
-    boolean missingFullName = Strings.isNullOrEmpty(user.getAccount().getFullName());
+    BranchCommitValidator validator = commitValidatorFactory.create(projectState, branch, user);
     RevWalk walk = receivePack.getRevWalk();
     walk.reset();
     walk.sort(RevSort.NONE);
@@ -3063,15 +3029,10 @@
           continue;
         }
 
-        if (!validCommit(walk.getObjectReader(), branch, cmd, c, false, null)) {
+        if (!validator.validCommit(
+            walk.getObjectReader(), cmd, c, false, messages, rejectCommits, null)) {
           break;
         }
-
-        if (missingFullName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
-          logger.atFine().log("Will update full name of caller");
-          setFullNameTo = c.getCommitterIdent().getName();
-          missingFullName = false;
-        }
       }
       logger.atFine().log("Validated %d new commits", n);
     } catch (IOException err) {
@@ -3080,75 +3041,9 @@
     }
   }
 
-  private String messageForCommit(RevCommit c, String msg) {
-    return String.format("commit %s: %s", c.abbreviate(RevId.ABBREV_LEN).name(), msg);
-  }
-
-  /**
-   * Validates a single commit. If the commit does not validate, the command is rejected.
-   *
-   * @param objectReader the object reader to use.
-   * @param branch the branch to which this commit is pushed
-   * @param cmd the ReceiveCommand executing the push.
-   * @param commit the commit being validated.
-   * @param isMerged whether this is a merge commit created by magicBranch --merge option
-   * @param change the change for which this is a new patchset.
-   */
-  private boolean validCommit(
-      ObjectReader objectReader,
-      Branch.NameKey branch,
-      ReceiveCommand cmd,
-      RevCommit commit,
-      boolean isMerged,
-      @Nullable Change change)
-      throws IOException {
-    PermissionBackend.ForRef perm = permissions.ref(branch.get());
-
-    ValidCommitKey key = new AutoValue_ReceiveCommits_ValidCommitKey(commit.copy(), branch);
-    if (validCommits.contains(key)) {
-      return true;
-    }
-
-    try (CommitReceivedEvent receiveEvent =
-        new CommitReceivedEvent(cmd, project, branch.get(), objectReader, commit, user)) {
-      CommitValidators validators =
-          isMerged
-              ? commitValidatorsFactory.forMergedCommits(
-                  project.getNameKey(), perm, user.asIdentifiedUser())
-              : commitValidatorsFactory.forReceiveCommits(
-                  perm,
-                  branch,
-                  user.asIdentifiedUser(),
-                  sshInfo,
-                  repo,
-                  receiveEvent.revWalk,
-                  change);
-
-      for (CommitValidationMessage m : validators.validate(receiveEvent)) {
-        messages.add(
-            new CommitValidationMessage(messageForCommit(commit, m.getMessage()), m.isError()));
-      }
-    } catch (CommitValidationException e) {
-      logger.atFine().log("Commit validation failed on %s", commit.name());
-      for (CommitValidationMessage m : e.getMessages()) {
-        // TODO(hanwen): drop the non-error messages?
-        messages.add(
-            new CommitValidationMessage(messageForCommit(commit, m.getMessage()), m.isError()));
-      }
-      reject(cmd, messageForCommit(commit, e.getMessage()));
-      return false;
-    }
-    validCommits.add(key);
-    return true;
-  }
-
   private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
     logger.atFine().log("Starting auto-closing of changes");
     String refName = cmd.getRefName();
-    checkState(
-        !MagicBranch.isMagicBranch(refName),
-        "shouldn't be auto-closing changes on magic branch %s",
-        refName);
 
     // TODO(dborowitz): Combine this BatchUpdate with the main one in
     // handleRegularCommands
@@ -3266,31 +3161,6 @@
     }
   }
 
-  private void updateAccountInfo() {
-    if (setFullNameTo == null) {
-      return;
-    }
-    logger.atFine().log("Updating full name of caller");
-    try {
-      Optional<AccountState> accountState =
-          accountsUpdateProvider
-              .get()
-              .update(
-                  "Set Full Name on Receive Commits",
-                  user.getAccountId(),
-                  (a, u) -> {
-                    if (Strings.isNullOrEmpty(a.getAccount().getFullName())) {
-                      u.setFullName(setFullNameTo);
-                    }
-                  });
-      accountState
-          .map(AccountState::getAccount)
-          .ifPresent(a -> user.getAccount().setFullName(a.getFullName()));
-    } catch (OrmException | IOException | ConfigInvalidException e) {
-      logger.atWarning().withCause(e).log("Failed to update full name of caller");
-    }
-  }
-
   private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(Branch.NameKey branch)
       throws OrmException {
     Map<Change.Key, ChangeNotes> r = new HashMap<>();
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 3e382fb..7930fe8 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -45,11 +44,9 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
@@ -78,6 +75,9 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.SystemReader;
 
+/**
+ * Represents a list of CommitValidationListeners to run for a push to one branch of one project.
+ */
 public class CommitValidators {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
@@ -123,15 +123,15 @@
     }
 
     public CommitValidators forReceiveCommits(
-        PermissionBackend.ForRef perm,
+        PermissionBackend.ForProject forProject,
         Branch.NameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
-        Repository repo,
+        NoteMap rejectCommits,
         RevWalk rw,
         @Nullable Change change)
         throws IOException {
-      NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, rw);
+      PermissionBackend.ForRef perm = forProject.ref(branch.get());
       ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
       return new CommitValidators(
           ImmutableList.of(
@@ -157,13 +157,14 @@
     }
 
     public CommitValidators forGerritCommits(
-        ForRef perm,
+        PermissionBackend.ForProject forProject,
         NameKey branch,
         IdentifiedUser user,
         SshInfo sshInfo,
         RevWalk rw,
         @Nullable Change change)
         throws IOException {
+      PermissionBackend.ForRef perm = forProject.ref(branch.get());
       ProjectState projectState = projectCache.checkedGet(branch.getParentKey());
       return new CommitValidators(
           ImmutableList.of(
@@ -187,7 +188,7 @@
     }
 
     public CommitValidators forMergedCommits(
-        Project.NameKey project, PermissionBackend.ForRef perm, IdentifiedUser user)
+        PermissionBackend.ForProject forProject, Branch.NameKey branch, IdentifiedUser user)
         throws IOException {
       // Generally only include validators that are based on permissions of the
       // user creating a change for a merged commit; generally exclude
@@ -202,10 +203,11 @@
       //    discuss what to do about it.
       //  - Plugin validators may do things like require certain commit message
       //    formats, so we play it safe and exclude them.
+      PermissionBackend.ForRef perm = forProject.ref(branch.get());
       return new CommitValidators(
           ImmutableList.of(
               new UploadMergesPermissionValidator(perm),
-              new ProjectStateValidationListener(projectCache.checkedGet(project)),
+              new ProjectStateValidationListener(projectCache.checkedGet(branch.getParentKey())),
               new AuthorUploaderValidator(user, perm, canonicalWebUrl),
               new CommitterUploaderValidator(user, perm, canonicalWebUrl)));
     }
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index b8ececb..f8c17d1 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -23,6 +23,8 @@
 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.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -77,17 +79,25 @@
     Optional<AccountState> accountState = byIdCache.get(id);
 
     if (accountState.isPresent()) {
-      logger.atInfo().log("Replace account %d in index", id.get());
+      logger.atFine().log("Replace account %d in index", id.get());
     } else {
-      logger.atInfo().log("Delete account %d from index", id.get());
+      logger.atFine().log("Delete account %d from index", id.get());
     }
 
     for (Index<Account.Id, AccountState> i : getWriteIndexes()) {
       // Evict the cache to get an up-to-date value for sure.
       if (accountState.isPresent()) {
-        i.replace(accountState.get());
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Replacing account %d in index version %d", id.get(), i.getSchema().getVersion())) {
+          i.replace(accountState.get());
+        }
       } else {
-        i.delete(id);
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Deleteing account %d in index version %d", id.get(), i.getSchema().getVersion())) {
+          i.delete(id);
+        }
       }
     }
     fireAccountIndexedEvent(id.get());
diff --git a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
index 0015268..acb7236 100644
--- a/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -30,7 +30,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -60,7 +59,7 @@
 
   @Override
   public SiteIndexer.Result indexAll(AccountIndex index) {
-    ProgressMonitor progress = new TextProgressMonitor(new PrintWriter(progressOut));
+    ProgressMonitor progress = new TextProgressMonitor(newPrintWriter(progressOut));
     progress.start(2);
     Stopwatch sw = Stopwatch.createStarted();
     List<Account.Id> ids;
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 8573862..d0a9749 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -33,6 +33,8 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -212,9 +214,14 @@
   }
 
   private void indexImpl(ChangeData cd) throws IOException {
-    logger.atInfo().log("Replace change %d in index.", cd.getId().get());
+    logger.atFine().log("Replace change %d in index.", cd.getId().get());
     for (Index<?, ChangeData> i : getWriteIndexes()) {
-      i.replace(cd);
+      try (TraceTimer traceTimer =
+          TraceContext.newTimer(
+              "Replacing change %d in index version %d",
+              cd.getId().get(), i.getSchema().getVersion())) {
+        i.replace(cd);
+      }
     }
     fireChangeIndexedEvent(cd.project().get(), cd.getId().get());
   }
@@ -412,12 +419,16 @@
 
     @Override
     public Void call() throws IOException {
-      logger.atInfo().log("Delete change %d from index.", id.get());
+      logger.atFine().log("Delete change %d from index.", id.get());
       // Don't bother setting a RequestContext to provide the DB.
       // Implementations should not need to access the DB in order to delete a
       // change ID.
       for (ChangeIndex i : getWriteIndexes()) {
-        i.delete(id);
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Deleteing change %d in index version %d", id.get(), i.getSchema().getVersion())) {
+          i.delete(id);
+        }
       }
       fireChangeDeletedFromIndexEvent(id.get());
       return null;
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 2823c2e..3474934 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -33,7 +33,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -64,7 +63,7 @@
 
   @Override
   public SiteIndexer.Result indexAll(GroupIndex index) {
-    ProgressMonitor progress = new TextProgressMonitor(new PrintWriter(progressOut));
+    ProgressMonitor progress = new TextProgressMonitor(newPrintWriter(progressOut));
     progress.start(2);
     Stopwatch sw = Stopwatch.createStarted();
     List<AccountGroup.UUID> uuids;
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index 3f4c7be..d6ba253 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -23,6 +23,8 @@
 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.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
@@ -78,16 +80,24 @@
     Optional<InternalGroup> internalGroup = groupCache.get(uuid);
 
     if (internalGroup.isPresent()) {
-      logger.atInfo().log("Replace group %s in index", uuid.get());
+      logger.atFine().log("Replace group %s in index", uuid.get());
     } else {
-      logger.atInfo().log("Delete group %s from index", uuid.get());
+      logger.atFine().log("Delete group %s from index", uuid.get());
     }
 
     for (Index<AccountGroup.UUID, InternalGroup> i : getWriteIndexes()) {
       if (internalGroup.isPresent()) {
-        i.replace(internalGroup.get());
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Replacing group %s in index version %d", uuid.get(), i.getSchema().getVersion())) {
+          i.replace(internalGroup.get());
+        }
       } else {
-        i.delete(uuid);
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Deleting group %s in index version %d", uuid.get(), i.getSchema().getVersion())) {
+          i.delete(uuid);
+        }
       }
     }
     fireGroupIndexedEvent(uuid.get());
diff --git a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
index f5bf8b5..c2a28af 100644
--- a/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/project/ProjectIndexerImpl.java
@@ -24,6 +24,8 @@
 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.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.assistedinject.Assisted;
@@ -72,16 +74,26 @@
   public void index(Project.NameKey nameKey) throws IOException {
     ProjectState projectState = projectCache.get(nameKey);
     if (projectState != null) {
-      logger.atInfo().log("Replace project %s in index", nameKey.get());
+      logger.atFine().log("Replace project %s in index", nameKey.get());
       ProjectData projectData = projectState.toProjectData();
       for (ProjectIndex i : getWriteIndexes()) {
-        i.replace(projectData);
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Replacing project %s in index version %d",
+                nameKey.get(), i.getSchema().getVersion())) {
+          i.replace(projectData);
+        }
       }
       fireProjectIndexedEvent(nameKey.get());
     } else {
-      logger.atInfo().log("Delete project %s from index", nameKey.get());
+      logger.atFine().log("Delete project %s from index", nameKey.get());
       for (ProjectIndex i : getWriteIndexes()) {
-        i.delete(nameKey);
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Deleting project %s in index version %d",
+                nameKey.get(), i.getSchema().getVersion())) {
+          i.delete(nameKey);
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/logging/BUILD b/java/com/google/gerrit/server/logging/BUILD
new file mode 100644
index 0000000..d3211f0
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/BUILD
@@ -0,0 +1,13 @@
+java_library(
+    name = "logging",
+    srcs = glob(
+        ["**/*.java"],
+    ),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/common:server",
+        "//lib:guava",
+        "//lib/flogger:api",
+    ],
+)
diff --git a/java/com/google/gerrit/server/logging/LoggingContext.java b/java/com/google/gerrit/server/logging/LoggingContext.java
index 2ce4c93..1e81c29 100644
--- a/java/com/google/gerrit/server/logging/LoggingContext.java
+++ b/java/com/google/gerrit/server/logging/LoggingContext.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.flogger.backend.Tags;
+import java.util.concurrent.Callable;
 import java.util.logging.Level;
 
 /**
@@ -42,6 +43,20 @@
     return INSTANCE;
   }
 
+  public static Runnable copy(Runnable runnable) {
+    if (runnable instanceof LoggingContextAwareRunnable) {
+      return runnable;
+    }
+    return new LoggingContextAwareRunnable(runnable);
+  }
+
+  public static <T> Callable<T> copy(Callable<T> callable) {
+    if (callable instanceof LoggingContextAwareCallable) {
+      return callable;
+    }
+    return new LoggingContextAwareCallable<>(callable);
+  }
+
   @Override
   public boolean shouldForceLogging(String loggerName, Level level, boolean isEnabled) {
     return isLoggingForced();
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
new file mode 100644
index 0000000..6aff5c4
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareCallable.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2018 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.common.collect.ImmutableSetMultimap;
+import java.util.concurrent.Callable;
+
+/**
+ * Wrapper for a {@link Callable} that copies the {@link LoggingContext} from the current thread to
+ * the thread that executes the callable.
+ *
+ * <p>The state of the logging context that is copied to the thread that executes the callable is
+ * fixed at the creation time of this wrapper. If the callable is submitted to an executor and is
+ * executed later this means that changes that are done to the logging context in between creating
+ * and executing the callable do not apply.
+ *
+ * <p>See {@link LoggingContextAwareRunnable} for an example.
+ *
+ * @see LoggingContextAwareRunnable
+ */
+class LoggingContextAwareCallable<T> implements Callable<T> {
+  private final Callable<T> callable;
+  private final Thread callingThread;
+  private final ImmutableSetMultimap<String, String> tags;
+  private final boolean forceLogging;
+
+  LoggingContextAwareCallable(Callable<T> callable) {
+    this.callable = callable;
+    this.callingThread = Thread.currentThread();
+    this.tags = LoggingContext.getInstance().getTagsAsMap();
+    this.forceLogging = LoggingContext.getInstance().isLoggingForced();
+  }
+
+  @Override
+  public T call() throws Exception {
+    if (callingThread.equals(Thread.currentThread())) {
+      // propagation of logging context is not needed
+      return callable.call();
+    }
+
+    // propagate logging context
+    LoggingContext loggingCtx = LoggingContext.getInstance();
+    ImmutableSetMultimap<String, String> oldTags = loggingCtx.getTagsAsMap();
+    boolean oldForceLogging = loggingCtx.isLoggingForced();
+    loggingCtx.setTags(tags);
+    loggingCtx.forceLogging(forceLogging);
+    try {
+      return callable.call();
+    } finally {
+      loggingCtx.setTags(oldTags);
+      loggingCtx.forceLogging(oldForceLogging);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareExecutorService.java b/java/com/google/gerrit/server/logging/LoggingContextAwareExecutorService.java
new file mode 100644
index 0000000..17e152e
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareExecutorService.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2018 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 static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * An {@link ExecutorService} that copies the {@link LoggingContext} on executing a {@link Runnable}
+ * to the executing thread.
+ */
+public class LoggingContextAwareExecutorService implements ExecutorService {
+  private final ExecutorService executorService;
+
+  public LoggingContextAwareExecutorService(ExecutorService executorService) {
+    this.executorService = executorService;
+  }
+
+  @Override
+  public void execute(Runnable command) {
+    executorService.execute(LoggingContext.copy(command));
+  }
+
+  @Override
+  public void shutdown() {
+    executorService.shutdown();
+  }
+
+  @Override
+  public List<Runnable> shutdownNow() {
+    return executorService.shutdownNow();
+  }
+
+  @Override
+  public boolean isShutdown() {
+    return executorService.isShutdown();
+  }
+
+  @Override
+  public boolean isTerminated() {
+    return executorService.isTerminated();
+  }
+
+  @Override
+  public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+    return executorService.awaitTermination(timeout, unit);
+  }
+
+  @Override
+  public <T> Future<T> submit(Callable<T> task) {
+    return executorService.submit(LoggingContext.copy(task));
+  }
+
+  @Override
+  public <T> Future<T> submit(Runnable task, T result) {
+    return executorService.submit(LoggingContext.copy(task), result);
+  }
+
+  @Override
+  public Future<?> submit(Runnable task) {
+    return executorService.submit(LoggingContext.copy(task));
+  }
+
+  @Override
+  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
+      throws InterruptedException {
+    return executorService.invokeAll(tasks.stream().map(LoggingContext::copy).collect(toList()));
+  }
+
+  @Override
+  public <T> List<Future<T>> invokeAll(
+      Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+      throws InterruptedException {
+    return executorService.invokeAll(
+        tasks.stream().map(LoggingContext::copy).collect(toList()), timeout, unit);
+  }
+
+  @Override
+  public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
+      throws InterruptedException, ExecutionException {
+    return executorService.invokeAny(tasks.stream().map(LoggingContext::copy).collect(toList()));
+  }
+
+  @Override
+  public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+      throws InterruptedException, ExecutionException, TimeoutException {
+    return executorService.invokeAny(
+        tasks.stream().map(LoggingContext::copy).collect(toList()), timeout, unit);
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
new file mode 100644
index 0000000..0bd7d00
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2018 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.common.collect.ImmutableSetMultimap;
+
+/**
+ * Wrapper for a {@link Runnable} that copies the {@link LoggingContext} from the current thread to
+ * the thread that executes the runnable.
+ *
+ * <p>The state of the logging context that is copied to the thread that executes the runnable is
+ * fixed at the creation time of this wrapper. If the runnable is submitted to an executor and is
+ * executed later this means that changes that are done to the logging context in between creating
+ * and executing the runnable do not apply.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ *   try (TraceContext traceContext = TraceContext.newTrace(true, ...)) {
+ *     executor
+ *         .submit(new LoggingContextAwareRunnable(
+ *             () -> {
+ *               // Tracing is enabled since the runnable is created within the TraceContext.
+ *               // Tracing is even enabled if the executor runs the runnable only after the
+ *               // TraceContext was closed.
+ *
+ *               // The tag "foo=bar" is not set, since it was added to the logging context only
+ *               // after this runnable was created.
+ *
+ *               // do stuff
+ *             }))
+ *         .get();
+ *     traceContext.addTag("foo", "bar");
+ *   }
+ * </pre>
+ *
+ * @see LoggingContextAwareCallable
+ */
+public class LoggingContextAwareRunnable implements Runnable {
+  private final Runnable runnable;
+  private final Thread callingThread;
+  private final ImmutableSetMultimap<String, String> tags;
+  private final boolean forceLogging;
+
+  LoggingContextAwareRunnable(Runnable runnable) {
+    this.runnable = runnable;
+    this.callingThread = Thread.currentThread();
+    this.tags = LoggingContext.getInstance().getTagsAsMap();
+    this.forceLogging = LoggingContext.getInstance().isLoggingForced();
+  }
+
+  public Runnable unwrap() {
+    return runnable;
+  }
+
+  @Override
+  public void run() {
+    if (callingThread.equals(Thread.currentThread())) {
+      // propagation of logging context is not needed
+      runnable.run();
+      return;
+    }
+
+    // propagate logging context
+    LoggingContext loggingCtx = LoggingContext.getInstance();
+    ImmutableSetMultimap<String, String> oldTags = loggingCtx.getTagsAsMap();
+    boolean oldForceLogging = loggingCtx.isLoggingForced();
+    loggingCtx.setTags(tags);
+    loggingCtx.forceLogging(forceLogging);
+    try {
+      runnable.run();
+    } finally {
+      loggingCtx.setTags(oldTags);
+      loggingCtx.forceLogging(oldForceLogging);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareScheduledExecutorService.java b/java/com/google/gerrit/server/logging/LoggingContextAwareScheduledExecutorService.java
new file mode 100644
index 0000000..e17a91e
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareScheduledExecutorService.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2018 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 java.util.concurrent.Callable;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link ScheduledExecutorService} that copies the {@link LoggingContext} on executing a {@link
+ * Runnable} to the executing thread.
+ */
+public class LoggingContextAwareScheduledExecutorService extends LoggingContextAwareExecutorService
+    implements ScheduledExecutorService {
+  private final ScheduledExecutorService scheduledExecutorService;
+
+  public LoggingContextAwareScheduledExecutorService(
+      ScheduledExecutorService scheduledExecutorService) {
+    super(scheduledExecutorService);
+    this.scheduledExecutorService = scheduledExecutorService;
+  }
+
+  @Override
+  public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+    return scheduledExecutorService.schedule(LoggingContext.copy(command), delay, unit);
+  }
+
+  @Override
+  public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+    return scheduledExecutorService.schedule(LoggingContext.copy(callable), delay, unit);
+  }
+
+  @Override
+  public ScheduledFuture<?> scheduleAtFixedRate(
+      Runnable command, long initialDelay, long period, TimeUnit unit) {
+    return scheduledExecutorService.scheduleAtFixedRate(
+        LoggingContext.copy(command), initialDelay, period, unit);
+  }
+
+  @Override
+  public ScheduledFuture<?> scheduleWithFixedDelay(
+      Runnable command, long initialDelay, long delay, TimeUnit unit) {
+    return scheduledExecutorService.scheduleWithFixedDelay(
+        LoggingContext.copy(command), initialDelay, delay, unit);
+  }
+}
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareThreadFactory.java b/java/com/google/gerrit/server/logging/LoggingContextAwareThreadFactory.java
deleted file mode 100644
index 05ff0d3..0000000
--- a/java/com/google/gerrit/server/logging/LoggingContextAwareThreadFactory.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2018 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.common.collect.ImmutableSetMultimap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-
-/**
- * ThreadFactory that copies the logging context of the current thread to any new thread that is
- * created by this ThreadFactory.
- */
-public class LoggingContextAwareThreadFactory implements ThreadFactory {
-  private final ThreadFactory parentThreadFactory;
-
-  public LoggingContextAwareThreadFactory() {
-    this.parentThreadFactory = Executors.defaultThreadFactory();
-  }
-
-  public LoggingContextAwareThreadFactory(ThreadFactory parentThreadFactory) {
-    this.parentThreadFactory = parentThreadFactory;
-  }
-
-  @Override
-  public Thread newThread(Runnable r) {
-    Thread callingThread = Thread.currentThread();
-    ImmutableSetMultimap<String, String> tags = LoggingContext.getInstance().getTagsAsMap();
-    boolean forceLogging = LoggingContext.getInstance().isLoggingForced();
-    return parentThreadFactory.newThread(
-        () -> {
-          if (callingThread.equals(Thread.currentThread())) {
-            // propagation of logging context is not needed
-            r.run();
-            return;
-          }
-
-          // propagate logging context
-          LoggingContext loggingCtx = LoggingContext.getInstance();
-          loggingCtx.setTags(tags);
-          loggingCtx.forceLogging(forceLogging);
-          try {
-            r.run();
-          } finally {
-            loggingCtx.clearTags();
-            loggingCtx.forceLogging(false);
-          }
-        });
-  }
-}
diff --git a/java/com/google/gerrit/server/logging/RequestId.java b/java/com/google/gerrit/server/logging/RequestId.java
index 81619fb..b0a8ad9 100644
--- a/java/com/google/gerrit/server/logging/RequestId.java
+++ b/java/com/google/gerrit/server/logging/RequestId.java
@@ -19,8 +19,6 @@
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
@@ -52,21 +50,13 @@
     return LoggingContext.getInstance().getTagsAsMap().keySet().stream().anyMatch(Type::isId);
   }
 
-  public static RequestId forChange(Change c) {
-    return new RequestId(c.getId().toString());
-  }
-
-  public static RequestId forProject(Project.NameKey p) {
-    return new RequestId(p.toString());
-  }
-
   private final String str;
 
   public RequestId() {
     this(null);
   }
 
-  private RequestId(@Nullable String resourceId) {
+  public RequestId(@Nullable String resourceId) {
     Hasher h = Hashing.murmur3_128().newHasher();
     h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID);
     str =
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 55a055a..977baa5 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -16,16 +16,212 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Strings;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.Table;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
+/**
+ * TraceContext that allows to set logging tags and enforce logging.
+ *
+ * <p>The logging tags are attached to all log entries that are triggered while the trace context is
+ * open. If force logging is enabled all logs that are triggered while the trace context is open are
+ * written to the log file regardless of the configured log level.
+ *
+ * <pre>
+ * try (TraceContext traceContext = TraceContext.open()
+ *         .addTag("tag-name", "tag-value")
+ *         .forceLogging()) {
+ *     // This gets logged as: A log [CONTEXT forced=true tag-name="tag-value" ]
+ *     // Since force logging is enabled this gets logged independently of the configured log
+ *     // level.
+ *     logger.atFinest().log("A log");
+ *
+ *     // do stuff
+ * }
+ * </pre>
+ *
+ * <p>The logging tags and the force logging flag are stored in the {@link LoggingContext}. {@link
+ * LoggingContextAwareExecutorService}, {@link LoggingContextAwareScheduledExecutorService} and the
+ * executor in {@link com.google.gerrit.server.git.WorkQueue} ensure that the logging context is
+ * automatically copied to background threads.
+ *
+ * <p>On close of the trace context newly set tags are unset. Force logging is disabled on close if
+ * it got enabled while the trace context was open.
+ *
+ * <p>Trace contexts can be nested:
+ *
+ * <pre>
+ * // Initially there are no tags
+ * logger.atSevere().log("log without tag");
+ *
+ * // a tag can be set by opening a trace context
+ * try (TraceContext ctx = TraceContext.open().addTag("tag1", "value1")) {
+ *   logger.atSevere().log("log with tag1=value1");
+ *
+ *   // while a trace context is open further tags can be added.
+ *   ctx.addTag("tag2", "value2")
+ *   logger.atSevere().log("log with tag1=value1 and tag2=value2");
+ *
+ *   // also by opening another trace context a another tag can be added
+ *   try (TraceContext ctx2 = TraceContext.open().addTag("tag3", "value3")) {
+ *     logger.atSevere().log("log with tag1=value1, tag2=value2 and tag3=value3");
+ *
+ *     // it's possible to have the same tag name with multiple values
+ *     ctx2.addTag("tag3", "value3a")
+ *     logger.atSevere().log("log with tag1=value1, tag2=value2, tag3=value3 and tag3=value3a");
+ *
+ *     // adding a tag with the same name and value as an existing tag has no effect
+ *     try (TraceContext ctx3 = TraceContext.open().addTag("tag3", "value3a")) {
+ *       logger.atSevere().log("log with tag1=value1, tag2=value2, tag3=value3 and tag3=value3a");
+ *     }
+ *
+ *     // closing ctx3 didn't remove tag3=value3a since it was already set before opening ctx3
+ *     logger.atSevere().log("log with tag1=value1, tag2=value2, tag3=value3 and tag3=value3a");
+ *   }
+ *
+ *   // closing ctx2 removed tag3=value3 and tag3-value3a
+ *   logger.atSevere().log("with tag1=value1 and tag2=value2");
+ * }
+ *
+ * // closing ctx1 removed tag1=value1 and tag2=value2
+ * logger.atSevere().log("log without tag");
+ * </pre>
+ */
 public class TraceContext implements AutoCloseable {
-  public static final TraceContext DISABLED = new TraceContext();
-
   public static TraceContext open() {
     return new TraceContext();
   }
 
+  /**
+   * Opens a new trace context for request tracing.
+   *
+   * <ul>
+   *   <li>sets a tag with a trace ID
+   *   <li>enables force logging
+   * </ul>
+   *
+   * <p>if no trace ID is provided a new trace ID is only generated if request tracing was not
+   * started yet. If request tracing was already started the given {@code traceIdConsumer} is
+   * invoked with the existing trace ID and no new logging tag is set.
+   *
+   * <p>No-op if {@code trace} is {@code false}.
+   *
+   * @param trace whether tracing should be started
+   * @param traceId trace ID that should be used for tracing, if {@code null} a trace ID is
+   *     generated
+   * @param traceIdConsumer consumer for the trace ID, should be used to return the generated trace
+   *     ID to the client, not invoked if {@code trace} is {@code false}
+   * @return the trace context
+   */
+  public static TraceContext newTrace(
+      boolean trace, @Nullable String traceId, TraceIdConsumer traceIdConsumer) {
+    if (!trace) {
+      // Create an empty trace context.
+      return open();
+    }
+
+    if (!Strings.isNullOrEmpty(traceId)) {
+      traceIdConsumer.accept(RequestId.Type.TRACE_ID.name(), traceId);
+      return open().addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
+    }
+
+    Optional<String> existingTraceId =
+        LoggingContext.getInstance()
+            .getTagsAsMap()
+            .get(RequestId.Type.TRACE_ID.name())
+            .stream()
+            .findAny();
+    if (existingTraceId.isPresent()) {
+      // request tracing was already started, no need to generate a new trace ID
+      traceIdConsumer.accept(RequestId.Type.TRACE_ID.name(), existingTraceId.get());
+      return open();
+    }
+
+    RequestId newTraceId = new RequestId();
+    traceIdConsumer.accept(RequestId.Type.TRACE_ID.name(), newTraceId.toString());
+    return open().addTag(RequestId.Type.TRACE_ID, newTraceId).forceLogging();
+  }
+
+  @FunctionalInterface
+  public interface TraceIdConsumer {
+    void accept(String tagName, String traceId);
+  }
+
+  /**
+   * Opens a new timer that logs the time for an operation if request tracing is enabled.
+   *
+   * <p>If request tracing is not enabled this is a no-op.
+   *
+   * @param message the message
+   * @return the trace timer
+   */
+  public static TraceTimer newTimer(String message) {
+    return new TraceTimer(message);
+  }
+
+  /**
+   * Opens a new timer that logs the time for an operation if request tracing is enabled.
+   *
+   * <p>If request tracing is not enabled this is a no-op.
+   *
+   * @param format the message format string
+   * @param arg argument for the message
+   * @return the trace timer
+   */
+  public static TraceTimer newTimer(String format, Object arg) {
+    return new TraceTimer(format, arg);
+  }
+
+  /**
+   * Opens a new timer that logs the time for an operation if request tracing is enabled.
+   *
+   * <p>If request tracing is not enabled this is a no-op.
+   *
+   * @param format the message format string
+   * @param arg1 first argument for the message
+   * @param arg2 second argument for the message
+   * @return the trace timer
+   */
+  public static TraceTimer newTimer(String format, Object arg1, Object arg2) {
+    return new TraceTimer(format, arg1, arg2);
+  }
+
+  public static class TraceTimer implements AutoCloseable {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+    private final Consumer<Long> logFn;
+    private final Stopwatch stopwatch;
+
+    private TraceTimer(String message) {
+      this(elapsedMs -> logger.atFine().log(message + " (%d ms)", elapsedMs));
+    }
+
+    private TraceTimer(String format, @Nullable Object arg) {
+      this(elapsedMs -> logger.atFine().log(format + " (%d ms)", arg, elapsedMs));
+    }
+
+    private TraceTimer(String format, @Nullable Object arg1, @Nullable Object arg2) {
+      this(elapsedMs -> logger.atFine().log(format + " (%d ms)", arg1, arg2, elapsedMs));
+    }
+
+    private TraceTimer(Consumer<Long> logFn) {
+      this.logFn = logFn;
+      this.stopwatch = Stopwatch.createStarted();
+    }
+
+    @Override
+    public void close() {
+      stopwatch.stop();
+      logFn.accept(stopwatch.elapsed(TimeUnit.MILLISECONDS));
+    }
+  }
+
   // Table<TAG_NAME, TAG_VALUE, REMOVE_ON_CLOSE>
   private final Table<String, String, Boolean> tags = HashBasedTable.create();
 
diff --git a/java/com/google/gerrit/server/mail/ListMailFilter.java b/java/com/google/gerrit/server/mail/ListMailFilter.java
index eee8c60..1549f8d 100644
--- a/java/com/google/gerrit/server/mail/ListMailFilter.java
+++ b/java/com/google/gerrit/server/mail/ListMailFilter.java
@@ -53,7 +53,8 @@
     }
 
     boolean match = mailPattern.matcher(message.from().getEmail()).find();
-    if (mode == ListFilterMode.WHITELIST && !match || mode == ListFilterMode.BLACKLIST && match) {
+    if ((mode == ListFilterMode.WHITELIST && !match)
+        || (mode == ListFilterMode.BLACKLIST && match)) {
       logger.atInfo().log("Mail message from %s rejected by list filter", message.from());
       return false;
     }
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 54176e2..0baaa11c 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -239,7 +239,7 @@
       }
     }
 
-    Collections.sort(groups, Comparator.comparing(g -> g.filename, FilenameComparator.INSTANCE));
+    groups.sort(Comparator.comparing(g -> g.filename, FilenameComparator.INSTANCE));
     return groups;
   }
 
diff --git a/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java b/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
index eecf935..0e9a2b7 100644
--- a/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
+++ b/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mime;
 
+import static java.util.Comparator.comparing;
+
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -22,12 +24,9 @@
 import eu.medsea.mimeutil.MimeType;
 import eu.medsea.mimeutil.MimeUtil2;
 import java.io.InputStream;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 
@@ -115,16 +114,7 @@
       return MimeUtil2.UNKNOWN_MIME_TYPE;
     }
 
-    final List<MimeType> types = new ArrayList<>(mimeTypes);
-    Collections.sort(
-        types,
-        new Comparator<MimeType>() {
-          @Override
-          public int compare(MimeType a, MimeType b) {
-            return getCorrectedMimeSpecificity(b) - getCorrectedMimeSpecificity(a);
-          }
-        });
-    return types.get(0);
+    return Collections.max(mimeTypes, comparing(this::getCorrectedMimeSpecificity));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/notedb/ChangeBundle.java b/java/com/google/gerrit/server/notedb/ChangeBundle.java
index 1d3c752..0ebee1a 100644
--- a/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -17,11 +17,16 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
 import static com.google.gerrit.common.TimeUtil.truncateToSecond;
 import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
 import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
 import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
 import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.naturalOrder;
+import static java.util.Comparator.nullsFirst;
 import static java.util.stream.Collectors.toList;
 
 import com.google.auto.value.AutoValue;
@@ -31,7 +36,6 @@
 import com.google.common.base.Predicates;
 import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -43,13 +47,14 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -70,7 +75,6 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.TreeMap;
 
 /**
  * A bundle of all entities rooted at a single {@link Change} entity.
@@ -105,110 +109,65 @@
         Source.NOTE_DB);
   }
 
-  private static Map<ChangeMessage.Key, ChangeMessage> changeMessageMap(
-      Iterable<ChangeMessage> in) {
-    Map<ChangeMessage.Key, ChangeMessage> out =
-        new TreeMap<>(
-            new Comparator<ChangeMessage.Key>() {
-              @Override
-              public int compare(ChangeMessage.Key a, ChangeMessage.Key b) {
-                return ComparisonChain.start()
-                    .compare(a.getParentKey().get(), b.getParentKey().get())
-                    .compare(a.get(), b.get())
-                    .result();
-              }
-            });
-    for (ChangeMessage cm : in) {
-      out.put(cm.getKey(), cm);
-    }
-    return out;
+  private static ImmutableSortedMap<ChangeMessage.Key, ChangeMessage> changeMessageMap(
+      Collection<ChangeMessage> in) {
+    return in.stream()
+        .collect(
+            toImmutableSortedMap(
+                comparing((ChangeMessage.Key k) -> k.getParentKey().get())
+                    .thenComparing(k -> k.get()),
+                cm -> cm.getKey(),
+                cm -> cm));
   }
 
   // Unlike the *Map comparators, which are intended to make key lists diffable,
   // this comparator sorts first on timestamp, then on every other field.
-  private static final Ordering<ChangeMessage> CHANGE_MESSAGE_ORDER =
-      new Ordering<ChangeMessage>() {
-        final Ordering<Comparable<?>> nullsFirst = Ordering.natural().nullsFirst();
-
-        @Override
-        public int compare(ChangeMessage a, ChangeMessage b) {
-          return ComparisonChain.start()
-              .compare(a.getWrittenOn(), b.getWrittenOn())
-              .compare(a.getKey().getParentKey().get(), b.getKey().getParentKey().get())
-              .compare(psId(a), psId(b), nullsFirst)
-              .compare(a.getAuthor(), b.getAuthor(), intKeyOrdering())
-              .compare(a.getMessage(), b.getMessage(), nullsFirst)
-              .result();
-        }
-
-        private Integer psId(ChangeMessage m) {
-          return m.getPatchSetId() != null ? m.getPatchSetId().get() : null;
-        }
-      };
+  private static final Comparator<ChangeMessage> CHANGE_MESSAGE_COMPARATOR =
+      comparing(ChangeMessage::getWrittenOn)
+          .thenComparing(m -> m.getKey().getParentKey().get())
+          .thenComparing(
+              m -> m.getPatchSetId() != null ? m.getPatchSetId().get() : null,
+              nullsFirst(naturalOrder()))
+          .thenComparing(ChangeMessage::getAuthor, intKeyOrdering())
+          .thenComparing(ChangeMessage::getMessage, nullsFirst(naturalOrder()));
 
   private static ImmutableList<ChangeMessage> changeMessageList(Iterable<ChangeMessage> in) {
-    return CHANGE_MESSAGE_ORDER.immutableSortedCopy(in);
+    return Streams.stream(in).sorted(CHANGE_MESSAGE_COMPARATOR).collect(toImmutableList());
   }
 
-  private static TreeMap<PatchSet.Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
-    TreeMap<PatchSet.Id, PatchSet> out =
-        new TreeMap<>(
-            new Comparator<PatchSet.Id>() {
-              @Override
-              public int compare(PatchSet.Id a, PatchSet.Id b) {
-                return patchSetIdChain(a, b).result();
-              }
-            });
-    for (PatchSet ps : in) {
-      out.put(ps.getId(), ps);
-    }
-    return out;
+  private static ImmutableSortedMap<Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
+    return Streams.stream(in)
+        .collect(toImmutableSortedMap(patchSetIdComparator(), PatchSet::getId, ps -> ps));
   }
 
-  private static Map<PatchSetApproval.Key, PatchSetApproval> patchSetApprovalMap(
+  private static ImmutableSortedMap<PatchSetApproval.Key, PatchSetApproval> patchSetApprovalMap(
       Iterable<PatchSetApproval> in) {
-    Map<PatchSetApproval.Key, PatchSetApproval> out =
-        new TreeMap<>(
-            new Comparator<PatchSetApproval.Key>() {
-              @Override
-              public int compare(PatchSetApproval.Key a, PatchSetApproval.Key b) {
-                return patchSetIdChain(a.getParentKey(), b.getParentKey())
-                    .compare(a.getAccountId().get(), b.getAccountId().get())
-                    .compare(a.getLabelId(), b.getLabelId())
-                    .result();
-              }
-            });
-    for (PatchSetApproval psa : in) {
-      out.put(psa.getKey(), psa);
-    }
-    return out;
+    return Streams.stream(in)
+        .collect(
+            toImmutableSortedMap(
+                comparing(PatchSetApproval.Key::getParentKey, patchSetIdComparator())
+                    .thenComparing(PatchSetApproval.Key::getAccountId, intKeyOrdering())
+                    .thenComparing(PatchSetApproval.Key::getLabelId),
+                PatchSetApproval::getKey,
+                a -> a));
   }
 
-  private static Map<PatchLineComment.Key, PatchLineComment> patchLineCommentMap(
+  private static ImmutableSortedMap<PatchLineComment.Key, PatchLineComment> patchLineCommentMap(
       Iterable<PatchLineComment> in) {
-    Map<PatchLineComment.Key, PatchLineComment> out =
-        new TreeMap<>(
-            new Comparator<PatchLineComment.Key>() {
-              @Override
-              public int compare(PatchLineComment.Key a, PatchLineComment.Key b) {
-                Patch.Key pka = a.getParentKey();
-                Patch.Key pkb = b.getParentKey();
-                return patchSetIdChain(pka.getParentKey(), pkb.getParentKey())
-                    .compare(pka.get(), pkb.get())
-                    .compare(a.get(), b.get())
-                    .result();
-              }
-            });
-    for (PatchLineComment plc : in) {
-      out.put(plc.getKey(), plc);
-    }
-    return out;
+    return Streams.stream(in)
+        .collect(
+            toImmutableSortedMap(
+                comparing(
+                        (PatchLineComment.Key k) -> k.getParentKey().getParentKey(),
+                        patchSetIdComparator())
+                    .thenComparing(PatchLineComment.Key::getParentKey)
+                    .thenComparing(PatchLineComment.Key::get),
+                PatchLineComment::getKey,
+                c -> c));
   }
 
-  private static ComparisonChain patchSetIdChain(PatchSet.Id a, PatchSet.Id b) {
-    return ComparisonChain.start()
-        .compare(a.getParentKey().get(), b.getParentKey().get())
-        .compare(a.get(), b.get());
+  private static Comparator<PatchSet.Id> patchSetIdComparator() {
+    return comparing((PatchSet.Id id) -> id.getParentKey().get()).thenComparing(id -> id.get());
   }
 
   static {
@@ -436,7 +395,7 @@
       excludeOrigSubj = true;
       String aTopic = trimOrNull(a.getTopic());
       excludeTopic =
-          Objects.equals(aTopic, b.getTopic()) || "".equals(aTopic) && b.getTopic() == null;
+          Objects.equals(aTopic, b.getTopic()) || ("".equals(aTopic) && b.getTopic() == null);
       aUpdated = bundleA.getLatestTimestamp();
     } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
       boolean createdOnMatchesFirstPs =
@@ -454,7 +413,7 @@
       excludeOrigSubj = true;
       String bTopic = trimOrNull(b.getTopic());
       excludeTopic =
-          Objects.equals(bTopic, a.getTopic()) || a.getTopic() == null && "".equals(bTopic);
+          Objects.equals(bTopic, a.getTopic()) || (a.getTopic() == null && "".equals(bTopic));
       bUpdated = bundleB.getLatestTimestamp();
     }
 
@@ -598,9 +557,10 @@
     }
     if (!bs.isEmpty()) {
       sb.append("Only in B:");
-      for (ChangeMessage cm : CHANGE_MESSAGE_ORDER.sortedCopy(bs.values())) {
-        sb.append("\n  ").append(cm);
-      }
+      bs.values()
+          .stream()
+          .sorted(CHANGE_MESSAGE_COMPARATOR)
+          .forEach(cm -> sb.append("\n  ").append(cm));
     }
     diffs.add(sb.toString());
   }
@@ -758,7 +718,8 @@
         excludePostSubmit = a.getValue() == 0 && b.isPostSubmit();
       } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
         excludeGranted =
-            tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn()) || tb.compareTo(ta) < 0;
+            (tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn()))
+                || (tb.compareTo(ta) < 0);
         excludePostSubmit = b.getValue() == 0 && a.isPostSubmit();
       }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 4eeab81..cbb7020 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -79,7 +79,6 @@
 import java.sql.Timestamp;
 import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -297,9 +296,7 @@
       }
       result.put(a.getPatchSetId(), a);
     }
-    for (Collection<PatchSetApproval> v : result.asMap().values()) {
-      Collections.sort((List<PatchSetApproval>) v, ChangeNotes.PSA_BY_TIME);
-    }
+    result.keySet().forEach(k -> result.get(k).sort(ChangeNotes.PSA_BY_TIME));
     return result;
   }
 
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
index 7931d88..c9711b5 100644
--- a/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
+++ b/java/com/google/gerrit/server/notedb/LegacyChangeNoteWrite.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -30,8 +32,6 @@
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -87,8 +87,7 @@
       return;
     }
 
-    List<Integer> psIds = new ArrayList<>(comments.keySet());
-    Collections.sort(psIds);
+    ImmutableList<Integer> psIds = comments.keySet().stream().sorted().collect(toImmutableList());
 
     OutputStreamWriter streamWriter = new OutputStreamWriter(out, UTF_8);
     try (PrintWriter writer = new PrintWriter(streamWriter)) {
diff --git a/java/com/google/gerrit/server/patch/DiffExecutorModule.java b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
index f3776e0..eb6a280 100644
--- a/java/com/google/gerrit/server/patch/DiffExecutorModule.java
+++ b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.patch;
 
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
@@ -32,11 +32,8 @@
   @Singleton
   @DiffExecutor
   public ExecutorService createDiffExecutor() {
-    return Executors.newCachedThreadPool(
-        new ThreadFactoryBuilder()
-            .setThreadFactory(new LoggingContextAwareThreadFactory())
-            .setNameFormat("Diff-%d")
-            .setDaemon(true)
-            .build());
+    return new LoggingContextAwareExecutorService(
+        Executors.newCachedThreadPool(
+            new ThreadFactoryBuilder().setNameFormat("Diff-%d").setDaemon(true).build()));
   }
 }
diff --git a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
index 8bca19f..9153638 100644
--- a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
+++ b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -19,7 +19,6 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Callable;
 
@@ -66,8 +65,9 @@
           break;
       }
     }
-    Collections.sort(r);
     return new DiffSummary(
-        r.toArray(new String[r.size()]), patchList.getInsertions(), patchList.getDeletions());
+        r.stream().sorted().toArray(String[]::new),
+        patchList.getInsertions(),
+        patchList.getDeletions());
   }
 }
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index cf5df4a..dd717ba 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -47,12 +47,7 @@
   private static final long serialVersionUID = PatchListKey.serialVersionUID;
 
   private static final Comparator<PatchListEntry> PATCH_CMP =
-      new Comparator<PatchListEntry>() {
-        @Override
-        public int compare(PatchListEntry a, PatchListEntry b) {
-          return comparePaths(a.getNewName(), b.getNewName());
-        }
-      };
+      Comparator.comparing(PatchListEntry::getNewName, PatchList::comparePaths);
 
   @VisibleForTesting
   static int comparePaths(String a, String b) {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index a3d9048..61f0180 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.patch;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.comparing;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.CommentDetail;
@@ -34,7 +35,6 @@
 import eu.medsea.mimeutil.MimeUtil2;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
@@ -55,13 +55,7 @@
   static final int MAX_CONTEXT = 5000000;
   static final int BIG_FILE = 9000;
 
-  private static final Comparator<Edit> EDIT_SORT =
-      new Comparator<Edit>() {
-        @Override
-        public int compare(Edit o1, Edit o2) {
-          return o1.getBeginA() - o2.getBeginA();
-        }
-      };
+  private static final Comparator<Edit> EDIT_SORT = comparing(Edit::getBeginA);
 
   private Repository db;
   private Project.NameKey projectKey;
@@ -369,7 +363,7 @@
     // them correctly later.
     //
     edits.addAll(empty);
-    Collections.sort(edits, EDIT_SORT);
+    edits.sort(EDIT_SORT);
   }
 
   private void safeAdd(List<Edit> empty, Edit toAdd) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 136b4ae..51a0f95 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -19,6 +19,7 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
@@ -46,6 +47,8 @@
 
 @Singleton
 public class DefaultPermissionBackend extends PermissionBackend {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final CurrentUser.PropertyKey<Boolean> IS_ADMIN = CurrentUser.PropertyKey.create();
 
   private final Provider<CurrentUser> currentUser;
@@ -186,6 +189,13 @@
     private boolean isAdmin() {
       if (admin == null) {
         admin = computeAdmin();
+        if (admin) {
+          logger.atFinest().log(
+              "user %s is an administrator of the server", user.getLoggableName());
+        } else {
+          logger.atFinest().log(
+              "user %s is not an administrator of the server", user.getLoggableName());
+        }
       }
       return admin;
     }
@@ -210,11 +220,32 @@
 
     private boolean canEmailReviewers() {
       List<PermissionRule> email = capabilities().emailReviewers;
-      return allow(email) || notDenied(email);
+      if (allow(email)) {
+        logger.atFinest().log(
+            "user %s can email reviewers (allowed by %s)", user.getLoggableName(), email);
+        return true;
+      }
+
+      if (notDenied(email)) {
+        logger.atFinest().log(
+            "user %s can email reviewers (not denied by %s)", user.getLoggableName(), email);
+        return true;
+      }
+
+      logger.atFinest().log("user %s cannot email reviewers", user.getLoggableName());
+      return false;
     }
 
     private boolean has(String permissionName) {
-      return allow(capabilities().getPermission(checkNotNull(permissionName)));
+      boolean has = allow(capabilities().getPermission(checkNotNull(permissionName)));
+      if (has) {
+        logger.atFinest().log(
+            "user %s has global capability %s", user.getLoggableName(), permissionName);
+      } else {
+        logger.atFinest().log(
+            "user %s doesn't have global capability %s", user.getLoggableName(), permissionName);
+      }
+      return has;
     }
 
     private boolean allow(Collection<PermissionRule> rules) {
diff --git a/java/com/google/gerrit/server/permissions/GlobalPermission.java b/java/com/google/gerrit/server/permissions/GlobalPermission.java
index 71718fb..01ef725 100644
--- a/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.extensions.api.access.GerritPermission;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
 import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.registration.PluginName;
 import java.lang.annotation.Annotation;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -116,7 +117,7 @@
       Class<?> annotationClass)
       throws PermissionBackendException {
     if (pluginName != null
-        && !"gerrit".equals(pluginName)
+        && !PluginName.GERRIT.equals(pluginName)
         && (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) {
       return new PluginPermission(pluginName, capability, fallBackToAdmin);
     }
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 67662c7..4a4ea37 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.permissions;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_TAGS;
 
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
@@ -212,6 +213,10 @@
     return (canPerformOnAnyRef(Permission.CREATE) || isAdmin());
   }
 
+  private boolean canAddTagRefs() {
+    return (canPerformOnTagRef(Permission.CREATE) || isAdmin());
+  }
+
   private boolean canCreateChanges() {
     for (SectionMatcher matcher : access()) {
       AccessSection section = matcher.getSection();
@@ -233,6 +238,26 @@
     return declaredOwner;
   }
 
+  private boolean canPerformOnTagRef(String permissionName) {
+    for (SectionMatcher matcher : access()) {
+      AccessSection section = matcher.getSection();
+
+      if (section.getName().startsWith(REFS_TAGS)) {
+        Permission permission = section.getPermission(permissionName);
+        if (permission == null) {
+          continue;
+        }
+
+        Boolean can = canPerform(permissionName, section, permission);
+        if (can != null) {
+          return can;
+        }
+      }
+    }
+
+    return false;
+  }
+
   private boolean canPerformOnAnyRef(String permissionName) {
     for (SectionMatcher matcher : access()) {
       AccessSection section = matcher.getSection();
@@ -241,25 +266,33 @@
         continue;
       }
 
-      for (PermissionRule rule : permission.getRules()) {
-        if (rule.isBlock() || rule.isDeny() || !match(rule)) {
-          continue;
-        }
-
-        // Being in a group that was granted this permission is only an
-        // approximation.  There might be overrides and doNotInherit
-        // that would render this to be false.
-        //
-        if (controlForRef(section.getName()).canPerform(permissionName)) {
-          return true;
-        }
-        break;
+      Boolean can = canPerform(permissionName, section, permission);
+      if (can != null) {
+        return can;
       }
     }
 
     return false;
   }
 
+  private Boolean canPerform(String permissionName, AccessSection section, Permission permission) {
+    for (PermissionRule rule : permission.getRules()) {
+      if (rule.isBlock() || rule.isDeny() || !match(rule)) {
+        continue;
+      }
+
+      // Being in a group that was granted this permission is only an
+      // approximation.  There might be overrides and doNotInherit
+      // that would render this to be false.
+      //
+      if (controlForRef(section.getName()).canPerform(permissionName)) {
+        return true;
+      }
+      break;
+    }
+    return null;
+  }
+
   private boolean canPerformOnAllRefs(String permission, Set<String> ignore) {
     boolean canPerform = false;
     Set<String> patterns = allRefPatterns(permission);
@@ -403,6 +436,8 @@
 
         case CREATE_REF:
           return canAddRefs();
+        case CREATE_TAG_REF:
+          return canAddTagRefs();
         case CREATE_CHANGE:
           return canCreateChanges();
 
diff --git a/java/com/google/gerrit/server/permissions/ProjectPermission.java b/java/com/google/gerrit/server/permissions/ProjectPermission.java
index 3fee6cf..7c58ccb 100644
--- a/java/com/google/gerrit/server/permissions/ProjectPermission.java
+++ b/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -51,6 +51,21 @@
   CREATE_REF,
 
   /**
+   * Can create at least one tag reference in the project.
+   *
+   * <p>This project level permission only validates the user may create some tag reference within
+   * the project. The exact reference name must be checked at creation:
+   *
+   * <pre>permissionBackend
+   *    .user(user)
+   *    .project(proj)
+   *    .ref(ref)
+   *    .check(RefPermission.CREATE);
+   * </pre>
+   */
+  CREATE_TAG_REF,
+
+  /**
    * Can create at least one change in the project.
    *
    * <p>This project level permission only validates the user may create a change for some branch
diff --git a/java/com/google/gerrit/server/permissions/SectionSortCache.java b/java/com/google/gerrit/server/permissions/SectionSortCache.java
index 48c8bff..e5392b0 100644
--- a/java/com/google/gerrit/server/permissions/SectionSortCache.java
+++ b/java/com/google/gerrit/server/permissions/SectionSortCache.java
@@ -26,7 +26,6 @@
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 
@@ -88,7 +87,7 @@
         poison |= srcMap.put(sections.get(i), i) != null;
       }
 
-      Collections.sort(sections, new MostSpecificComparator(ref));
+      sections.sort(new MostSpecificComparator(ref));
 
       int[] srcIdx;
       if (isIdentityTransform(sections, srcMap)) {
diff --git a/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 229f394..5b80059 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.SitePaths;
@@ -90,7 +91,7 @@
 
   @Override
   public String getProviderPluginName() {
-    return "gerrit";
+    return PluginName.GERRIT;
   }
 
   private static String getExtension(Path path) {
diff --git a/java/com/google/gerrit/server/plugins/PluginEntry.java b/java/com/google/gerrit/server/plugins/PluginEntry.java
index f7b1e82..3a6c7b2 100644
--- a/java/com/google/gerrit/server/plugins/PluginEntry.java
+++ b/java/com/google/gerrit/server/plugins/PluginEntry.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.plugins;
 
+import static java.util.Comparator.comparing;
+
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Map;
@@ -28,13 +30,7 @@
 public class PluginEntry {
   public static final String ATTR_CHARACTER_ENCODING = "Character-Encoding";
   public static final String ATTR_CONTENT_TYPE = "Content-Type";
-  public static final Comparator<PluginEntry> COMPARATOR_BY_NAME =
-      new Comparator<PluginEntry>() {
-        @Override
-        public int compare(PluginEntry a, PluginEntry b) {
-          return a.getName().compareTo(b.getName());
-        }
-      };
+  public static final Comparator<PluginEntry> COMPARATOR_BY_NAME = comparing(PluginEntry::getName);
 
   private static final Map<Object, String> EMPTY_ATTRS = Collections.emptyMap();
   private static final Optional<Long> NO_SIZE = Optional.empty();
diff --git a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 300bec7..8bc04a3 100644
--- a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -281,7 +281,8 @@
 
   private void attachSet(
       Map<TypeLiteral<?>, DynamicSet<?>> sets, @Nullable Injector src, Plugin plugin) {
-    for (RegistrationHandle h : PrivateInternals_DynamicTypes.attachSets(src, sets)) {
+    for (RegistrationHandle h :
+        PrivateInternals_DynamicTypes.attachSets(src, plugin.getName(), sets)) {
       plugin.add(h);
     }
   }
@@ -434,7 +435,7 @@
           oi.remove();
           replace(newPlugin, h2, b);
         } else {
-          newPlugin.add(set.add(b.getKey(), b.getProvider()));
+          newPlugin.add(set.add(newPlugin.getName(), b.getKey(), b.getProvider()));
         }
       }
     }
diff --git a/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
index 0bef1e5..4d89482 100644
--- a/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.nio.file.Path;
@@ -60,7 +61,7 @@
 
   @Override
   public String getProviderPluginName() {
-    return "gerrit";
+    return PluginName.GERRIT;
   }
 
   private ServerPluginProvider providerOf(Path srcPath) {
diff --git a/java/com/google/gerrit/server/project/ProjectCacheClock.java b/java/com/google/gerrit/server/project/ProjectCacheClock.java
index 188ee08..eb451fd 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheClock.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheClock.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.concurrent.Executors;
@@ -54,14 +54,14 @@
       // Start with generation 1 (to avoid magic 0 below).
       generation.set(1);
       executor =
-          Executors.newScheduledThreadPool(
-              1,
-              new ThreadFactoryBuilder()
-                  .setThreadFactory(new LoggingContextAwareThreadFactory())
-                  .setNameFormat("ProjectCacheClock-%d")
-                  .setDaemon(true)
-                  .setPriority(Thread.MIN_PRIORITY)
-                  .build());
+          new LoggingContextAwareScheduledExecutorService(
+              Executors.newScheduledThreadPool(
+                  1,
+                  new ThreadFactoryBuilder()
+                      .setNameFormat("ProjectCacheClock-%d")
+                      .setDaemon(true)
+                      .setPriority(Thread.MIN_PRIORITY)
+                      .build()));
       @SuppressWarnings("unused") // Runnable already handles errors
       Future<?> possiblyIgnoredError =
           executor.scheduleAtFixedRate(
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 4942318..dd6ce56 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -178,6 +178,7 @@
   @Override
   public void evict(Project.NameKey p) throws IOException {
     if (p != null) {
+      logger.atFine().log("Evict project '%s'", p.get());
       byName.invalidate(p.get());
     }
     indexer.get().index(p);
diff --git a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index adfaf62..10cf2de 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -19,11 +19,11 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 
@@ -44,13 +44,11 @@
   public void start() {
     int cpus = Runtime.getRuntime().availableProcessors();
     if (config.getBoolean("cache", "projects", "loadOnStartup", false)) {
-      ThreadPoolExecutor pool =
-          new ScheduledThreadPoolExecutor(
-              config.getInt("cache", "projects", "loadThreads", cpus),
-              new ThreadFactoryBuilder()
-                  .setThreadFactory(new LoggingContextAwareThreadFactory())
-                  .setNameFormat("ProjectCacheLoader-%d")
-                  .build());
+      ExecutorService pool =
+          new LoggingContextAwareExecutorService(
+              new ScheduledThreadPoolExecutor(
+                  config.getInt("cache", "projects", "loadThreads", cpus),
+                  new ThreadFactoryBuilder().setNameFormat("ProjectCacheLoader-%d").build()));
       Thread scheduler =
           new Thread(
               () -> {
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 59fc323..bccc415 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.server.project;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.common.data.Permission.isPermission;
 import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
@@ -70,6 +72,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -89,6 +92,7 @@
   public static final String KEY_DEFAULT_VALUE = "defaultValue";
   public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
   public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
+  public static final String KEY_IGNORE_SELF_APPROVAL = "ignoreSelfApproval";
   public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
   public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
       "copyAllScoresOnMergeFirstParentUpdate";
@@ -880,6 +884,8 @@
       }
       label.setAllowPostSubmit(
           rc.getBoolean(LABEL, name, KEY_ALLOW_POST_SUBMIT, LabelType.DEF_ALLOW_POST_SUBMIT));
+      label.setIgnoreSelfApproval(
+          rc.getBoolean(LABEL, name, KEY_IGNORE_SELF_APPROVAL, LabelType.DEF_IGNORE_SELF_APPROVAL));
       label.setCopyMinScore(
           rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, LabelType.DEF_COPY_MIN_SCORE));
       label.setCopyMaxScore(
@@ -1162,21 +1168,20 @@
 
   private void saveNotifySections(Config rc, Set<AccountGroup.UUID> keepGroups) {
     for (NotifyConfig nc : sort(notifySections.values())) {
-      List<String> email = new ArrayList<>();
-      for (GroupReference gr : nc.getGroups()) {
-        if (gr.getUUID() != null) {
-          keepGroups.add(gr.getUUID());
-        }
-        email.add(new PermissionRule(gr).asString(false));
-      }
-      Collections.sort(email);
+      nc.getGroups()
+          .stream()
+          .map(gr -> gr.getUUID())
+          .filter(Objects::nonNull)
+          .forEach(keepGroups::add);
+      List<String> email =
+          nc.getGroups()
+              .stream()
+              .map(gr -> new PermissionRule(gr).asString(false))
+              .sorted()
+              .collect(toList());
 
-      List<String> addrs = new ArrayList<>();
-      for (Address addr : nc.getAddresses()) {
-        addrs.add(addr.toString());
-      }
-      Collections.sort(addrs);
-      email.addAll(addrs);
+      // Separate stream operation so that emails list contains 2 sorted sub-lists.
+      nc.getAddresses().stream().map(Address::toString).sorted().forEach(email::add);
 
       set(rc, NOTIFY, nc.getName(), KEY_HEADER, nc.getHeader(), NotifyConfig.Header.BCC);
       if (email.isEmpty()) {
@@ -1320,6 +1325,13 @@
           rc,
           LABEL,
           name,
+          KEY_IGNORE_SELF_APPROVAL,
+          label.ignoreSelfApproval(),
+          LabelType.DEF_IGNORE_SELF_APPROVAL);
+      setBooleanConfigKey(
+          rc,
+          LABEL,
+          name,
           KEY_COPY_MIN_SCORE,
           label.isCopyMinScore(),
           LabelType.DEF_COPY_MIN_SCORE);
@@ -1450,10 +1462,8 @@
     validationErrors.add(error);
   }
 
-  private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
-    ArrayList<T> r = new ArrayList<>(m);
-    Collections.sort(r);
-    return r;
+  private static <T extends Comparable<? super T>> ImmutableList<T> sort(Collection<T> m) {
+    return m.stream().sorted().collect(toImmutableList());
   }
 
   public boolean hasLegacyPermissions() {
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 3e379a6..a9b19d9 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.common.data.PermissionRule.Action.ALLOW;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -89,6 +90,7 @@
   private final Map<String, ProjectLevelConfig> configs;
   private final Set<AccountGroup.UUID> localOwners;
   private final long globalMaxObjectSizeLimit;
+  private final boolean inheritProjectMaxObjectSizeLimit;
 
   /** Last system time the configuration's revision was examined. */
   private volatile long lastCheckGeneration;
@@ -132,6 +134,7 @@
             ? limitsFactory.create(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
             : null;
     this.globalMaxObjectSizeLimit = transferConfig.getMaxObjectSizeLimit();
+    this.inheritProjectMaxObjectSizeLimit = transferConfig.getInheritProjectMaxObjectSizeLimit();
 
     if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
       localOwners = Collections.emptySet();
@@ -264,13 +267,58 @@
     }
   }
 
-  public long getEffectiveMaxObjectSizeLimit() {
-    long local = config.getMaxObjectSizeLimit();
-    if (globalMaxObjectSizeLimit > 0 && local > 0) {
-      return Math.min(globalMaxObjectSizeLimit, local);
+  public static class EffectiveMaxObjectSizeLimit {
+    public long value;
+    public String summary;
+  }
+
+  private static final String MAY_NOT_SET = "This project may not set a higher limit.";
+
+  @VisibleForTesting
+  public static final String INHERITED_FROM_PARENT = "Inherited from parent project '%s'.";
+
+  @VisibleForTesting
+  public static final String OVERRIDDEN_BY_PARENT =
+      "Overridden by parent project '%s'. " + MAY_NOT_SET;
+
+  @VisibleForTesting
+  public static final String INHERITED_FROM_GLOBAL = "Inherited from the global config.";
+
+  @VisibleForTesting
+  public static final String OVERRIDDEN_BY_GLOBAL =
+      "Overridden by the global config. " + MAY_NOT_SET;
+
+  public EffectiveMaxObjectSizeLimit getEffectiveMaxObjectSizeLimit() {
+    EffectiveMaxObjectSizeLimit result = new EffectiveMaxObjectSizeLimit();
+
+    result.value = config.getMaxObjectSizeLimit();
+
+    if (inheritProjectMaxObjectSizeLimit) {
+      for (ProjectState parent : parents()) {
+        long parentValue = parent.config.getMaxObjectSizeLimit();
+        if (parentValue > 0 && result.value > 0) {
+          if (parentValue < result.value) {
+            result.value = parentValue;
+            result.summary = String.format(OVERRIDDEN_BY_PARENT, parent.config.getName());
+          }
+        } else if (parentValue > 0) {
+          result.value = parentValue;
+          result.summary = String.format(INHERITED_FROM_PARENT, parent.config.getName());
+        }
+      }
     }
-    // zero means "no limit", in this case the max is more limiting
-    return Math.max(globalMaxObjectSizeLimit, local);
+
+    if (globalMaxObjectSizeLimit > 0 && result.value > 0) {
+      if (globalMaxObjectSizeLimit < result.value) {
+        result.value = globalMaxObjectSizeLimit;
+        result.summary = OVERRIDDEN_BY_GLOBAL;
+      }
+    } else if (globalMaxObjectSizeLimit > result.value) {
+      // zero means "no limit", in this case the max is more limiting
+      result.value = globalMaxObjectSizeLimit;
+      result.summary = INHERITED_FROM_GLOBAL;
+    }
+    return result;
   }
 
   /** Get the sections that pertain only to this project. */
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 78ec540..0e8eb70 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -115,8 +115,7 @@
       throw new BadRequestException("username must match URL");
     }
     if (!ExternalId.isValidUsername(username)) {
-      throw new BadRequestException(
-          "Username '" + username + "' must contain only letters, numbers, _, - or .");
+      throw new BadRequestException("Invalid username '" + username + "'");
     }
 
     Set<AccountGroup.UUID> groups = parseGroups(input.groups);
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 7889f6e..5c466bf 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -26,8 +26,8 @@
 import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
@@ -172,9 +172,9 @@
     }
 
     @Override
-    public BinaryResult apply(Capability resource) throws ResourceNotFoundException {
+    public Response<String> apply(Capability resource) throws ResourceNotFoundException {
       permissionBackend.checkUsesDefaultCapabilities();
-      return BinaryResult.create("ok\n");
+      return Response.ok("ok");
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetEmails.java b/java/com/google/gerrit/server/restapi/account/GetEmails.java
index 85262ee..ed3347f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetEmails.java
+++ b/java/com/google/gerrit/server/restapi/account/GetEmails.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -25,10 +28,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
 
 @Singleton
 public class GetEmails implements RestReadView<AccountResource> {
@@ -47,24 +48,19 @@
     if (!self.get().hasSameAccountId(rsrc.getUser())) {
       permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
     }
+    return rsrc.getUser()
+        .getEmailAddresses()
+        .stream()
+        .filter(Objects::nonNull)
+        .map(e -> toEmailInfo(rsrc, e))
+        .sorted(comparing((EmailInfo e) -> e.email))
+        .collect(toList());
+  }
 
-    List<EmailInfo> emails = new ArrayList<>();
-    for (String email : rsrc.getUser().getEmailAddresses()) {
-      if (email != null) {
-        EmailInfo e = new EmailInfo();
-        e.email = email;
-        e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
-        emails.add(e);
-      }
-    }
-    Collections.sort(
-        emails,
-        new Comparator<EmailInfo>() {
-          @Override
-          public int compare(EmailInfo a, EmailInfo b) {
-            return a.email.compareTo(b.email);
-          }
-        });
-    return emails;
+  private static EmailInfo toEmailInfo(AccountResource rsrc, String email) {
+    EmailInfo e = new EmailInfo();
+    e.email = email;
+    e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
+    return e;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 112bb24..61021be 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Strings;
-import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -36,11 +38,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
@@ -67,31 +65,28 @@
 
     Account.Id accountId = rsrc.getUser().getAccountId();
     AccountState account = accounts.get(accountId).orElseThrow(ResourceNotFoundException::new);
-    List<ProjectWatchInfo> projectWatchInfos = new ArrayList<>();
-    for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e :
-        account.getProjectWatches().entrySet()) {
-      ProjectWatchInfo pwi = new ProjectWatchInfo();
-      pwi.filter = e.getKey().filter();
-      pwi.project = e.getKey().project().get();
-      pwi.notifyAbandonedChanges = toBoolean(e.getValue().contains(NotifyType.ABANDONED_CHANGES));
-      pwi.notifyNewChanges = toBoolean(e.getValue().contains(NotifyType.NEW_CHANGES));
-      pwi.notifyNewPatchSets = toBoolean(e.getValue().contains(NotifyType.NEW_PATCHSETS));
-      pwi.notifySubmittedChanges = toBoolean(e.getValue().contains(NotifyType.SUBMITTED_CHANGES));
-      pwi.notifyAllComments = toBoolean(e.getValue().contains(NotifyType.ALL_COMMENTS));
-      projectWatchInfos.add(pwi);
-    }
-    Collections.sort(
-        projectWatchInfos,
-        new Comparator<ProjectWatchInfo>() {
-          @Override
-          public int compare(ProjectWatchInfo pwi1, ProjectWatchInfo pwi2) {
-            return ComparisonChain.start()
-                .compare(pwi1.project, pwi2.project)
-                .compare(Strings.nullToEmpty(pwi1.filter), Strings.nullToEmpty(pwi2.filter))
-                .result();
-          }
-        });
-    return projectWatchInfos;
+    return account
+        .getProjectWatches()
+        .entrySet()
+        .stream()
+        .map(e -> toProjectWatchInfo(e.getKey(), e.getValue()))
+        .sorted(
+            comparing((ProjectWatchInfo pwi) -> pwi.project)
+                .thenComparing(pwi -> Strings.nullToEmpty(pwi.filter)))
+        .collect(toList());
+  }
+
+  private static ProjectWatchInfo toProjectWatchInfo(
+      ProjectWatchKey key, ImmutableSet<NotifyType> watchTypes) {
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.filter = key.filter();
+    pwi.project = key.project().get();
+    pwi.notifyAbandonedChanges = toBoolean(watchTypes.contains(NotifyType.ABANDONED_CHANGES));
+    pwi.notifyNewChanges = toBoolean(watchTypes.contains(NotifyType.NEW_CHANGES));
+    pwi.notifyNewPatchSets = toBoolean(watchTypes.contains(NotifyType.NEW_PATCHSETS));
+    pwi.notifySubmittedChanges = toBoolean(watchTypes.contains(NotifyType.SUBMITTED_CHANGES));
+    pwi.notifyAllComments = toBoolean(watchTypes.contains(NotifyType.ALL_COMMENTS));
+    return pwi;
   }
 
   private static Boolean toBoolean(boolean value) {
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index e92abe1..a562592 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.server.CommentsUtil.COMMENT_INFO_ORDER;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.Side;
@@ -35,7 +37,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -96,9 +97,7 @@
         list.add(o);
       }
 
-      for (List<T> list : out.values()) {
-        Collections.sort(list, COMMENT_INFO_ORDER);
-      }
+      out.values().forEach(l -> l.sort(COMMENT_INFO_ORDER));
 
       if (loader != null) {
         loader.fill();
@@ -106,13 +105,14 @@
       return out;
     }
 
-    public List<T> formatAsList(Iterable<F> comments) throws PermissionBackendException {
+    public ImmutableList<T> formatAsList(Iterable<F> comments) throws PermissionBackendException {
       AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null;
 
-      List<T> out =
-          FluentIterable.from(comments)
-              .transform(c -> toInfo(c, loader))
-              .toSortedList(COMMENT_INFO_ORDER);
+      ImmutableList<T> out =
+          Streams.stream(comments)
+              .map(c -> toInfo(c, loader))
+              .sorted(COMMENT_INFO_ORDER)
+              .collect(toImmutableList());
 
       if (loader != null) {
         loader.fill();
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
index db8ef0c..dbd0ccf 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -61,7 +62,7 @@
         .format(listComments(rsrc));
   }
 
-  public List<CommentInfo> getComments(RevisionResource rsrc)
+  public ImmutableList<CommentInfo> getComments(RevisionResource rsrc)
       throws OrmException, PermissionBackendException {
     return commentJson
         .get()
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index 66138ab..99366aa 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.RobotComment;
@@ -52,7 +53,7 @@
         .format(listComments(rsrc));
   }
 
-  public List<RobotCommentInfo> getComments(RevisionResource rsrc)
+  public ImmutableList<RobotCommentInfo> getComments(RevisionResource rsrc)
       throws OrmException, PermissionBackendException {
     return commentJson
         .get()
diff --git a/java/com/google/gerrit/server/restapi/config/CachesCollection.java b/java/com/google/gerrit/server/restapi/config/CachesCollection.java
index 152fef9..a4b8802 100644
--- a/java/com/google/gerrit/server/restapi/config/CachesCollection.java
+++ b/java/com/google/gerrit/server/restapi/config/CachesCollection.java
@@ -20,6 +20,7 @@
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -66,7 +67,7 @@
     permissionBackend.currentUser().check(GlobalPermission.VIEW_CACHES);
 
     String cacheName = id.get();
-    String pluginName = "gerrit";
+    String pluginName = PluginName.GERRIT;
     int i = cacheName.lastIndexOf('-');
     if (i != -1) {
       pluginName = cacheName.substring(0, i);
diff --git a/java/com/google/gerrit/server/restapi/config/ListCaches.java b/java/com/google/gerrit/server/restapi/config/ListCaches.java
index c0a9d71..38664fb 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCaches.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
 import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
 import static com.google.gerrit.server.config.CacheResource.cacheNameOf;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
 
-import com.google.common.base.Joiner;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheStats;
+import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -29,11 +31,9 @@
 import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.stream.Stream;
 import org.kohsuke.args4j.Option;
 
 @RequiresAnyCapability({VIEW_CACHES, MAINTAIN_SERVER})
@@ -72,19 +72,17 @@
     if (format == null) {
       return getCacheInfos();
     }
-    List<String> cacheNames = new ArrayList<>();
-    for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
-      cacheNames.add(cacheNameOf(e.getPluginName(), e.getExportName()));
-    }
-    Collections.sort(cacheNames);
-
+    Stream<String> cacheNames =
+        Streams.stream(cacheMap)
+            .map(e -> cacheNameOf(e.getPluginName(), e.getExportName()))
+            .sorted();
     if (OutputFormat.TEXT_LIST.equals(format)) {
-      return BinaryResult.create(Joiner.on('\n').join(cacheNames))
+      return BinaryResult.create(cacheNames.collect(joining("\n")))
           .base64()
           .setContentType("text/plain")
           .setCharacterEncoding(UTF_8);
     }
-    return cacheNames;
+    return cacheNames.collect(toImmutableList());
   }
 
   public enum CacheType {
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index 7b69831..f77cda4 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.restapi.config;
 
-import com.google.common.collect.ComparisonChain;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Project;
@@ -35,8 +37,6 @@
 import com.google.inject.Singleton;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -106,20 +106,14 @@
   }
 
   private List<TaskInfo> getTasks() {
-    List<TaskInfo> taskInfos = workQueue.getTaskInfos(TaskInfo::new);
-    Collections.sort(
-        taskInfos,
-        new Comparator<TaskInfo>() {
-          @Override
-          public int compare(TaskInfo a, TaskInfo b) {
-            return ComparisonChain.start()
-                .compare(a.state.ordinal(), b.state.ordinal())
-                .compare(a.delay, b.delay)
-                .compare(a.command, b.command)
-                .result();
-          }
-        });
-    return taskInfos;
+    return workQueue
+        .getTaskInfos(TaskInfo::new)
+        .stream()
+        .sorted(
+            comparing((TaskInfo t) -> t.state.ordinal())
+                .thenComparing(t -> t.delay)
+                .thenComparing(t -> t.command))
+        .collect(toList());
   }
 
   public static class TaskInfo {
diff --git a/java/com/google/gerrit/server/restapi/config/PostCaches.java b/java/com/google/gerrit/server/restapi/config/PostCaches.java
index 7f9b756..57ba097 100644
--- a/java/com/google/gerrit/server/restapi/config/PostCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/PostCaches.java
@@ -20,6 +20,7 @@
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -110,7 +111,7 @@
     List<CacheResource> cacheResources = new ArrayList<>(cacheNames.size());
 
     for (String n : cacheNames) {
-      String pluginName = "gerrit";
+      String pluginName = PluginName.GERRIT;
       String cacheName = n;
       int i = cacheName.lastIndexOf('-');
       if (i != -1) {
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index 7af4284..dcdd8a8 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -41,7 +41,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -138,7 +137,7 @@
     accountLoader.fill();
 
     // sort by date and then reverse so that the newest audit event comes first
-    Collections.sort(auditEvents, comparing((GroupAuditEventInfo a) -> a.date).reversed());
+    auditEvents.sort(comparing((GroupAuditEventInfo a) -> a.date).reversed());
     return auditEvents;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index 97a260e..864b01b 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import static com.google.common.base.Strings.nullToEmpty;
+import static java.util.Comparator.comparing;
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.GroupDescription;
@@ -29,8 +30,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 @Singleton
@@ -72,18 +71,8 @@
         continue;
       }
     }
-    Collections.sort(
-        included,
-        new Comparator<GroupInfo>() {
-          @Override
-          public int compare(GroupInfo a, GroupInfo b) {
-            int cmp = nullToEmpty(a.name).compareTo(nullToEmpty(b.name));
-            if (cmp != 0) {
-              return cmp;
-            }
-            return nullToEmpty(a.id).compareTo(nullToEmpty(b.id));
-          }
-        });
+    included.sort(
+        comparing((GroupInfo g) -> nullToEmpty(g.name)).thenComparing(g -> nullToEmpty(g.id)));
     return included;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
index 076bf78..60b5dee 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
@@ -33,10 +33,10 @@
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.project.BooleanProjectConfigTransformations;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.ProjectState.EffectiveMaxObjectSizeLimit;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -48,7 +48,6 @@
       boolean serverEnableSignedPush,
       ProjectState projectState,
       CurrentUser user,
-      TransferConfig transferConfig,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
@@ -72,7 +71,7 @@
       this.requireSignedPush = null;
     }
 
-    this.maxObjectSizeLimit = getMaxObjectSizeLimit(projectState, transferConfig, p);
+    this.maxObjectSizeLimit = getMaxObjectSizeLimit(projectState, p);
 
     this.defaultSubmitType = new SubmitTypeInfo();
     this.defaultSubmitType.value = projectState.getSubmitType();
@@ -107,13 +106,13 @@
     this.extensionPanelNames = projectState.getConfig().getExtensionPanelSections();
   }
 
-  private MaxObjectSizeLimitInfo getMaxObjectSizeLimit(
-      ProjectState projectState, TransferConfig transferConfig, Project p) {
+  private MaxObjectSizeLimitInfo getMaxObjectSizeLimit(ProjectState projectState, Project p) {
     MaxObjectSizeLimitInfo info = new MaxObjectSizeLimitInfo();
-    long value = projectState.getEffectiveMaxObjectSizeLimit();
+    EffectiveMaxObjectSizeLimit limit = projectState.getEffectiveMaxObjectSizeLimit();
+    long value = limit.value;
     info.value = value == 0 ? null : String.valueOf(value);
     info.configuredValue = p.getMaxObjectSizeLimit();
-    info.inheritedValue = transferConfig.getFormattedMaxObjectSizeLimit();
+    info.summary = limit.summary;
     return info;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index d545f92..a6b9404 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
 import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_REF;
+import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_TAG_REF;
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 import static com.google.gerrit.server.permissions.RefPermission.READ;
 import static com.google.gerrit.server.permissions.RefPermission.WRITE_CONFIG;
@@ -270,6 +271,7 @@
                     || (canReadConfig
                         && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE))));
     info.canAdd = toBoolean(perm.testOrFalse(CREATE_REF));
+    info.canAddTags = toBoolean(perm.testOrFalse(CREATE_TAG_REF));
     info.configVisible = canReadConfig || canWriteConfig;
 
     info.groups =
diff --git a/java/com/google/gerrit/server/restapi/project/GetConfig.java b/java/com/google/gerrit/server/restapi/project/GetConfig.java
index 7fedc8f..b3ad962 100644
--- a/java/com/google/gerrit/server/restapi/project/GetConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/GetConfig.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -31,7 +30,6 @@
 @Singleton
 public class GetConfig implements RestReadView<ProjectResource> {
   private final boolean serverEnableSignedPush;
-  private final TransferConfig transferConfig;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
   private final AllProjectsName allProjects;
@@ -41,14 +39,12 @@
   @Inject
   public GetConfig(
       @EnableSignedPush boolean serverEnableSignedPush,
-      TransferConfig transferConfig,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
       UiActions uiActions,
       DynamicMap<RestView<ProjectResource>> views) {
     this.serverEnableSignedPush = serverEnableSignedPush;
-    this.transferConfig = transferConfig;
     this.pluginConfigEntries = pluginConfigEntries;
     this.allProjects = allProjects;
     this.cfgFactory = cfgFactory;
@@ -62,7 +58,6 @@
         serverEnableSignedPush,
         resource.getProjectState(),
         resource.getUser(),
-        transferConfig,
         pluginConfigEntries,
         cfgFactory,
         allProjects,
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index bf4a547..a0d2528 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -46,7 +46,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
@@ -226,7 +225,7 @@
         // Do nothing.
       }
     }
-    Collections.sort(branches, new BranchComparator());
+    branches.sort(new BranchComparator());
     return branches;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index e79fdca..f59e984 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+import static java.util.Comparator.comparing;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
@@ -39,8 +40,6 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -135,14 +134,7 @@
       }
     }
 
-    Collections.sort(
-        tags,
-        new Comparator<TagInfo>() {
-          @Override
-          public int compare(TagInfo a, TagInfo b) {
-            return a.ref.compareTo(b.ref);
-          }
-        });
+    tags.sort(comparing(t -> t.ref));
 
     return new RefFilter<TagInfo>(Constants.R_TAGS)
         .start(start)
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index f4eb781..76ea0c9 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -71,7 +70,6 @@
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final ProjectState.Factory projectStateFactory;
-  private final TransferConfig transferConfig;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
   private final AllProjectsName allProjects;
@@ -86,7 +84,6 @@
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       ProjectCache projectCache,
       ProjectState.Factory projectStateFactory,
-      TransferConfig transferConfig,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsName allProjects,
@@ -98,7 +95,6 @@
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.projectStateFactory = projectStateFactory;
-    this.transferConfig = transferConfig;
     this.pluginConfigEntries = pluginConfigEntries;
     this.cfgFactory = cfgFactory;
     this.allProjects = allProjects;
@@ -173,7 +169,6 @@
           serverEnableSignedPush,
           state,
           user.get(),
-          transferConfig,
           pluginConfigEntries,
           cfgFactory,
           allProjects,
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
new file mode 100644
index 0000000..b9ddbc6
--- /dev/null
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -0,0 +1,171 @@
+// Copyright (C) 2018 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.rules;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Rule to require an approval from a user that did not upload the current patch set or block
+ * submission.
+ */
+@Singleton
+public class IgnoreSelfApprovalRule implements SubmitRule {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String E_UNABLE_TO_FETCH_UPLOADER = "Unable to fetch uploader";
+  private static final String E_UNABLE_TO_FETCH_LABELS =
+      "Unable to fetch labels and approvals for the change";
+
+  public static class Module extends FactoryModule {
+    @Override
+    public void configure() {
+      bind(SubmitRule.class)
+          .annotatedWith(Exports.named("IgnoreSelfApprovalRule"))
+          .to(IgnoreSelfApprovalRule.class);
+    }
+  }
+
+  @Inject
+  IgnoreSelfApprovalRule() {}
+
+  @Override
+  public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions options) {
+    List<LabelType> labelTypes;
+    List<PatchSetApproval> approvals;
+    try {
+      labelTypes = cd.getLabelTypes().getLabelTypes();
+      approvals = cd.currentApprovals();
+    } catch (OrmException e) {
+      logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_LABELS);
+      return singletonRuleError(E_UNABLE_TO_FETCH_LABELS);
+    }
+
+    boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(l -> l.ignoreSelfApproval());
+    if (!shouldIgnoreSelfApproval) {
+      // Shortcut to avoid further processing if no label should ignore uploader approvals
+      return ImmutableList.of();
+    }
+
+    Account.Id uploader;
+    try {
+      uploader = cd.currentPatchSet().getUploader();
+    } catch (OrmException e) {
+      logger.atWarning().withCause(e).log(E_UNABLE_TO_FETCH_UPLOADER);
+      return singletonRuleError(E_UNABLE_TO_FETCH_UPLOADER);
+    }
+
+    SubmitRecord submitRecord = new SubmitRecord();
+    submitRecord.status = SubmitRecord.Status.OK;
+    submitRecord.labels = new ArrayList<>(labelTypes.size());
+    submitRecord.requirements = new ArrayList<>();
+
+    for (LabelType t : labelTypes) {
+      if (!t.ignoreSelfApproval()) {
+        // The default rules are enough in this case.
+        continue;
+      }
+
+      LabelFunction labelFunction = t.getFunction();
+      if (labelFunction == null) {
+        continue;
+      }
+
+      Collection<PatchSetApproval> allApprovalsForLabel = filterApprovalsByLabel(approvals, t);
+      SubmitRecord.Label allApprovalsCheckResult = labelFunction.check(t, allApprovalsForLabel);
+      SubmitRecord.Label ignoreSelfApprovalCheckResult =
+          labelFunction.check(t, filterOutPositiveApprovalsOfUser(allApprovalsForLabel, uploader));
+
+      if (labelCheckPassed(allApprovalsCheckResult)
+          && !labelCheckPassed(ignoreSelfApprovalCheckResult)) {
+        // The label has a valid approval from the uploader and no other valid approval. Set the
+        // label
+        // to NOT_READY and indicate the need for non-uploader approval as requirement.
+        submitRecord.labels.add(ignoreSelfApprovalCheckResult);
+        submitRecord.status = SubmitRecord.Status.NOT_READY;
+        // Add an additional requirement to be more descriptive on why the label counts as not
+        // approved.
+        submitRecord.requirements.add(
+            SubmitRequirement.builder()
+                .setFallbackText("Approval from non-uploader required")
+                .setType("non_uploader_approval")
+                .build());
+      }
+    }
+
+    if (submitRecord.labels.isEmpty()) {
+      return ImmutableList.of();
+    }
+
+    return ImmutableList.of(submitRecord);
+  }
+
+  private static boolean labelCheckPassed(SubmitRecord.Label label) {
+    switch (label.status) {
+      case OK:
+      case MAY:
+        return true;
+
+      case NEED:
+      case REJECT:
+      case IMPOSSIBLE:
+        return false;
+    }
+    return false;
+  }
+
+  private static Collection<SubmitRecord> singletonRuleError(String reason) {
+    SubmitRecord submitRecord = new SubmitRecord();
+    submitRecord.errorMessage = reason;
+    submitRecord.status = SubmitRecord.Status.RULE_ERROR;
+    return ImmutableList.of(submitRecord);
+  }
+
+  @VisibleForTesting
+  static Collection<PatchSetApproval> filterOutPositiveApprovalsOfUser(
+      Collection<PatchSetApproval> approvals, Account.Id user) {
+    return approvals
+        .stream()
+        .filter(input -> input.getValue() < 0 || !input.getAccountId().equals(user))
+        .collect(toImmutableList());
+  }
+
+  @VisibleForTesting
+  static Collection<PatchSetApproval> filterApprovalsByLabel(
+      Collection<PatchSetApproval> approvals, LabelType t) {
+    return approvals
+        .stream()
+        .filter(input -> input.getLabelId().get().equals(t.getLabelId().get()))
+        .collect(toImmutableList());
+  }
+}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 9a8895b..43d5f75 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -457,7 +457,7 @@
     this.caller = caller;
     this.ts = TimeUtil.nowTs();
     this.db = db;
-    this.submissionId = RequestId.forChange(change);
+    this.submissionId = new RequestId(change.getId().toString());
 
     try (TraceContext traceContext =
         TraceContext.open().addTag(RequestId.Type.SUBMISSION_ID, submissionId)) {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 290e917..51dad5b 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -53,7 +53,6 @@
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -184,8 +183,7 @@
         continue; // Bogus ref, can't be merged into tip so we don't care.
       }
     }
-    Collections.sort(
-        commits,
+    commits.sort(
         ReviewDbUtil.intKeyOrdering().reverse().onResultOf(CodeReviewCommit::getPatchsetId));
     CodeReviewCommit result = MergeUtil.findAnyMergedInto(rw, commits, tip);
     if (result == null) {
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 132c04b..10e3455 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -31,6 +31,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.metrics.Counter1;
@@ -52,6 +53,8 @@
 
 @Singleton
 public class RetryHelper {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   @FunctionalInterface
   public interface ChangeAction<T> {
     T call(BatchUpdate.Factory batchUpdateFactory) throws Exception;
@@ -277,6 +280,9 @@
       retryerBuilder.withRetryListener(listener);
       return executeWithTimeoutCount(actionType, action, retryerBuilder.build());
     } finally {
+      if (listener.getAttemptCount() > 1) {
+        logger.atFine().log("%s was attempted %d times", actionType, listener.getAttemptCount());
+      }
       metrics.attemptCounts.record(actionType, listener.getAttemptCount());
     }
   }
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 4743b35..47b1d89 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -15,6 +15,7 @@
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/git/receive",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/util/cli",
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 68ea7bb..1fdf7d8 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
+import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -78,12 +78,12 @@
     int threads = cfg.getInt("sshd", "commandStartThreads", 2);
     startExecutor = workQueue.createQueue(threads, "SshCommandStart", true);
     destroyExecutor =
-        Executors.newSingleThreadExecutor(
-            new ThreadFactoryBuilder()
-                .setThreadFactory(new LoggingContextAwareThreadFactory())
-                .setNameFormat("SshCommandDestroy-%s")
-                .setDaemon(true)
-                .build());
+        new LoggingContextAwareExecutorService(
+            Executors.newSingleThreadExecutor(
+                new ThreadFactoryBuilder()
+                    .setNameFormat("SshCommandDestroy-%s")
+                    .setDaemon(true)
+                    .build()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 96d89ab..99c8724 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -25,6 +24,9 @@
   @Option(name = "--trace", usage = "enable request tracing")
   private boolean trace;
 
+  @Option(name = "--trace-id", usage = "trace ID (can only be set if --trace was set too)")
+  private String traceId;
+
   protected PrintWriter stdout;
   protected PrintWriter stderr;
 
@@ -49,12 +51,13 @@
 
   protected abstract void run() throws UnloggedFailure, Failure, Exception;
 
-  private TraceContext enableTracing() {
-    if (trace) {
-      RequestId traceId = new RequestId();
-      stderr.println(String.format("%s: %s", RequestId.Type.TRACE_ID, traceId));
-      return TraceContext.open().forceLogging().addTag(RequestId.Type.TRACE_ID, traceId);
+  private TraceContext enableTracing() throws UnloggedFailure {
+    if (!trace && traceId != null) {
+      throw die("A trace ID can only be set if --trace was specified.");
     }
-    return TraceContext.DISABLED;
+    return TraceContext.newTrace(
+        trace,
+        traceId,
+        (tagName, traceId) -> stderr.println(String.format("%s: %s", tagName, traceId)));
   }
 }
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 3cd1a0c..81ce91d 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -86,6 +86,7 @@
   @Override
   public void evict(String username) {
     if (username != null) {
+      logger.atFine().log("Evict SSH key for username %s", username);
       cache.invalidate(username);
     }
   }
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 9b517c6..baadf02 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.sshd.commands;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -33,11 +35,7 @@
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Date;
-import java.util.List;
 import java.util.Optional;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoSession;
@@ -92,25 +90,27 @@
       throw new Failure(1, "fatal: sshd no longer running");
     }
 
-    final List<IoSession> list = new ArrayList<>(acceptor.getManagedSessions().values());
-    Collections.sort(
-        list,
-        new Comparator<IoSession>() {
-          @Override
-          public int compare(IoSession arg0, IoSession arg1) {
-            if (arg0 instanceof MinaSession) {
-              MinaSession mArg0 = (MinaSession) arg0;
-              MinaSession mArg1 = (MinaSession) arg1;
-              if (mArg0.getSession().getCreationTime() < mArg1.getSession().getCreationTime()) {
-                return -1;
-              } else if (mArg0.getSession().getCreationTime()
-                  > mArg1.getSession().getCreationTime()) {
-                return 1;
-              }
-            }
-            return (int) (arg0.getId() - arg1.getId());
-          }
-        });
+    final ImmutableList<IoSession> list =
+        acceptor
+            .getManagedSessions()
+            .values()
+            .stream()
+            .sorted(
+                (arg0, arg1) -> {
+                  if (arg0 instanceof MinaSession) {
+                    MinaSession mArg0 = (MinaSession) arg0;
+                    MinaSession mArg1 = (MinaSession) arg1;
+                    if (mArg0.getSession().getCreationTime()
+                        < mArg1.getSession().getCreationTime()) {
+                      return -1;
+                    } else if (mArg0.getSession().getCreationTime()
+                        > mArg1.getSession().getCreationTime()) {
+                      return 1;
+                    }
+                  }
+                  return (int) (arg0.getId() - arg1.getId());
+                })
+            .collect(toImmutableList());
 
     hostNameWidth = wide ? Integer.MAX_VALUE : columns - 9 - 9 - 10 - 32;
 
diff --git a/java/com/google/gerrit/sshd/commands/StreamEvents.java b/java/com/google/gerrit/sshd/commands/StreamEvents.java
index c97372c..ffd98d5 100644
--- a/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -151,6 +151,7 @@
     stdout = toPrintWriter(out);
     eventListenerRegistration =
         eventListeners.add(
+            "gerrit",
             new UserScopedEventListener() {
               @Override
               public void onEvent(Event event) {
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index cf65908..15ceb77 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -29,6 +29,7 @@
         "//java/com/google/gerrit/server/audit",
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/mem",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//lib:guava",
diff --git a/java/com/google/gerrit/testing/ConfigSuite.java b/java/com/google/gerrit/testing/ConfigSuite.java
index ff87fd8..9e45b7c 100644
--- a/java/com/google/gerrit/testing/ConfigSuite.java
+++ b/java/com/google/gerrit/testing/ConfigSuite.java
@@ -159,7 +159,7 @@
 
     @Override
     public Object createTest() throws Exception {
-      Object test = getTestClass().getJavaClass().newInstance();
+      Object test = getTestClass().getJavaClass().getDeclaredConstructor().newInstance();
       parameterField.set(test, new org.eclipse.jgit.lib.Config(cfg));
       if (nameField != null) {
         nameField.set(test, name);
diff --git a/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
index 1318125..20d093e 100644
--- a/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
+++ b/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -14,6 +14,9 @@
 
 package com.google.gwtexpui.globalkey.client;
 
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -31,10 +34,7 @@
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -228,15 +228,6 @@
   }
 
   private List<KeyCommand> sort(KeyCommandSet set) {
-    final List<KeyCommand> keys = new ArrayList<>(set.getKeys());
-    Collections.sort(
-        keys,
-        new Comparator<KeyCommand>() {
-          @Override
-          public int compare(KeyCommand arg0, KeyCommand arg1) {
-            return arg0.getHelpText().compareTo(arg1.getHelpText());
-          }
-        });
-    return keys;
+    return set.getKeys().stream().sorted(comparing(KeyCommand::getHelpText)).collect(toList());
   }
 }
diff --git a/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index ef80cdb..758521f 100644
--- a/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -14,11 +14,12 @@
 
 package com.google.gwtexpui.safehtml.client;
 
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
 import com.google.gwt.user.client.ui.SuggestOracle;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 /**
@@ -115,15 +116,8 @@
      * terms.
      */
     private static List<String> splitQuery(String query) {
-      List<String> queryTerms = Arrays.asList(query.split("\\s+"));
-      Collections.sort(
-          queryTerms,
-          new Comparator<String>() {
-            @Override
-            public int compare(String s1, String s2) {
-              return Integer.compare(s2.length(), s1.length());
-            }
-          });
+      List<String> queryTerms =
+          Arrays.stream(query.split("\\s+")).sorted(comparing(String::length)).collect(toList());
 
       List<String> result = new ArrayList<>();
       for (String s : queryTerms) {
diff --git a/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java b/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
index 3c7b966..bf387fd 100644
--- a/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
+++ b/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
@@ -39,7 +39,7 @@
 
   @Test
   public void universalGroupBackendHandlesTestGroup() throws Exception {
-    RegistrationHandle registrationHandle = groupBackends.add(testGroupBackend);
+    RegistrationHandle registrationHandle = groupBackends.add("gerrit", testGroupBackend);
     try {
       assertThat(universalGroupBackend.handles(testUUID)).isTrue();
     } finally {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 05642c9..de66b87 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -230,7 +230,7 @@
   @Before
   public void addAccountIndexEventCounter() {
     accountIndexedCounter = new AccountIndexedCounter();
-    accountIndexEventCounterHandle = accountIndexedListeners.add(accountIndexedCounter);
+    accountIndexEventCounterHandle = accountIndexedListeners.add("gerrit", accountIndexedCounter);
   }
 
   @After
@@ -243,7 +243,7 @@
   @Before
   public void addRefUpdateCounter() {
     refUpdateCounter = new RefUpdateCounter();
-    refUpdateCounterHandle = refUpdateListeners.add(refUpdateCounter);
+    refUpdateCounterHandle = refUpdateListeners.add("gerrit", refUpdateCounter);
   }
 
   @After
@@ -526,12 +526,13 @@
 
   @Test
   public void validateAccountActivation() throws Exception {
-    com.google.gerrit.acceptance.testsuite.account.TestAccount activatableAccount =
+    Account.Id activatableAccountId =
         accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
-    com.google.gerrit.acceptance.testsuite.account.TestAccount deactivatableAccount =
+    Account.Id deactivatableAccountId =
         accountOperations.newAccount().preferredEmail("foo@deactivatable.com").create();
     RegistrationHandle registrationHandle =
         accountActivationValidationListeners.add(
+            "gerrit",
             new AccountActivationValidationListener() {
               @Override
               public void validateActivation(AccountState account) throws ValidationException {
@@ -553,61 +554,56 @@
       /* Test account that can be activated, but not deactivated */
       // Deactivate account that is already inactive
       try {
-        gApi.accounts().id(activatableAccount.accountId().get()).setActive(false);
+        gApi.accounts().id(activatableAccountId.get()).setActive(false);
         fail("Expected exception");
       } catch (ResourceConflictException e) {
         assertThat(e.getMessage()).isEqualTo("account not active");
       }
-      assertThat(accountOperations.account(activatableAccount.accountId()).get().active())
-          .isFalse();
+      assertThat(accountOperations.account(activatableAccountId).get().active()).isFalse();
 
       // Activate account that can be activated
-      gApi.accounts().id(activatableAccount.accountId().get()).setActive(true);
-      assertThat(accountOperations.account(activatableAccount.accountId()).get().active()).isTrue();
+      gApi.accounts().id(activatableAccountId.get()).setActive(true);
+      assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
 
       // Activate account that is already active
-      gApi.accounts().id(activatableAccount.accountId().get()).setActive(true);
-      assertThat(accountOperations.account(activatableAccount.accountId()).get().active()).isTrue();
+      gApi.accounts().id(activatableAccountId.get()).setActive(true);
+      assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
 
       // Try deactivating account that cannot be deactivated
       try {
-        gApi.accounts().id(activatableAccount.accountId().get()).setActive(false);
+        gApi.accounts().id(activatableAccountId.get()).setActive(false);
         fail("Expected exception");
       } catch (ResourceConflictException e) {
         assertThat(e.getMessage()).isEqualTo("not allowed to deactive account");
       }
-      assertThat(accountOperations.account(activatableAccount.accountId()).get().active()).isTrue();
+      assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
 
       /* Test account that can be deactivated, but not activated */
       // Activate account that is already inactive
-      gApi.accounts().id(deactivatableAccount.accountId().get()).setActive(true);
-      assertThat(accountOperations.account(deactivatableAccount.accountId()).get().active())
-          .isTrue();
+      gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
+      assertThat(accountOperations.account(deactivatableAccountId).get().active()).isTrue();
 
       // Deactivate account that can be deactivated
-      gApi.accounts().id(deactivatableAccount.accountId().get()).setActive(false);
-      assertThat(accountOperations.account(deactivatableAccount.accountId()).get().active())
-          .isFalse();
+      gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
+      assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
 
       // Deactivate account that is already inactive
       try {
-        gApi.accounts().id(deactivatableAccount.accountId().get()).setActive(false);
+        gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
         fail("Expected exception");
       } catch (ResourceConflictException e) {
         assertThat(e.getMessage()).isEqualTo("account not active");
       }
-      assertThat(accountOperations.account(deactivatableAccount.accountId()).get().active())
-          .isFalse();
+      assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
 
       // Try activating account that cannot be activated
       try {
-        gApi.accounts().id(deactivatableAccount.accountId().get()).setActive(true);
+        gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
         fail("Expected exception");
       } catch (ResourceConflictException e) {
         assertThat(e.getMessage()).isEqualTo("not allowed to active account");
       }
-      assertThat(accountOperations.account(deactivatableAccount.accountId()).get().active())
-          .isFalse();
+      assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
     } finally {
       registrationHandle.remove();
     }
@@ -2168,6 +2164,38 @@
   }
 
   @Test
+  public void createUserWithValidUsername() throws Exception {
+    ImmutableList<String> names =
+        ImmutableList.of(
+            "user@domain",
+            "user-name",
+            "user_name",
+            "1234",
+            "user1234",
+            "1234@domain",
+            "user!+alias{*}#$%&’^=~|@domain");
+    for (String name : names) {
+      gApi.accounts().create(name);
+    }
+  }
+
+  @Test
+  public void createUserWithInvalidUsername() throws Exception {
+    ImmutableList<String> invalidNames =
+        ImmutableList.of(
+            "@", "@foo", "-", "-foo", "_", "_foo", "!", "+", "{", "}", "*", "%", "#", "$", "&", "’",
+            "^", "=", "~");
+    for (String name : invalidNames) {
+      try {
+        gApi.accounts().create(name);
+        fail(String.format("Expected BadRequestException for username [%s]", name));
+      } catch (BadRequestException e) {
+        assertThat(e).hasMessageThat().isEqualTo(String.format("Invalid username '%s'", name));
+      }
+    }
+  }
+
+  @Test
   public void allGroupsForAUserAccountCanBeRetrieved() throws Exception {
     String username = name("user1");
     accountOperations.newAccount().username(username).create();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 6fd9545..b3a5e2d 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -68,7 +68,6 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
-import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelFunction;
@@ -203,7 +202,7 @@
   @Before
   public void addChangeIndexedCounter() {
     changeIndexedCounter = new ChangeIndexedCounter();
-    changeIndexedCounterHandle = changeIndexedListeners.add(changeIndexedCounter);
+    changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
   }
 
   @After
@@ -1688,7 +1687,7 @@
     // create a group named "ab" with one user: testUser
     String email = "abcd@test.com";
     String fullname = "abcd";
-    TestAccount testUser =
+    Account.Id accountIdOfTestUser =
         accountOperations
             .newAccount()
             .username("abcd")
@@ -1721,7 +1720,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.accountId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(accountIdOfTestUser.get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
@@ -1748,7 +1747,7 @@
 
     String myGroupUserEmail = "lee@test.com";
     String myGroupUserFullname = "lee";
-    TestAccount myGroupUser =
+    Account.Id accountIdOfGroupUser =
         accountOperations
             .newAccount()
             .username("lee")
@@ -1785,7 +1784,7 @@
     Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
     assertThat(reviewers).isNotNull();
     assertThat(reviewers).hasSize(1);
-    assertThat(reviewers.iterator().next()._accountId).isEqualTo(myGroupUser.accountId().get());
+    assertThat(reviewers.iterator().next()._accountId).isEqualTo(accountIdOfGroupUser.get());
 
     // Ensure ETag and lastUpdatedOn are updated.
     rsrc = parseResource(r);
@@ -2215,13 +2214,12 @@
 
     // notify unrelated account as TO
     String email = "user2@example.com";
-    TestAccount user2 =
-        accountOperations
-            .newAccount()
-            .username("user2")
-            .preferredEmail(email)
-            .fullname("User2")
-            .create();
+    accountOperations
+        .newAccount()
+        .username("user2")
+        .preferredEmail(email)
+        .fullname("User2")
+        .create();
     setApiUser(user);
     recommend(r.getChangeId());
     setApiUser(admin);
@@ -2229,7 +2227,7 @@
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
-    assertNotifyTo(user2);
+    assertNotifyTo(email, "User2");
 
     // notify unrelated account as CC
     setApiUser(user);
@@ -2239,7 +2237,7 @@
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
-    assertNotifyCc(user2);
+    assertNotifyCc(email, "User2");
 
     // notify unrelated account as BCC
     setApiUser(user);
@@ -2249,7 +2247,7 @@
     in.notifyDetails = new HashMap<>();
     in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
     gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
-    assertNotifyBcc(user2);
+    assertNotifyBcc(email, "User2");
   }
 
   @Test
@@ -2599,6 +2597,7 @@
     PushOneCommit.Result change = createChange();
     RegistrationHandle handle =
         changeMessageModifiers.add(
+            "gerrit",
             new ChangeMessageModifier() {
               @Override
               public String onSubmit(
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 4e3f048..d6be960 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -183,24 +183,23 @@
   @Test
   public void cachedGroupsForMemberAreUpdatedOnMemberAdditionAndRemoval() throws Exception {
     String username = name("user");
-    com.google.gerrit.acceptance.testsuite.account.TestAccount account =
-        accountOperations.newAccount().username(username).create();
+    Account.Id accountId = accountOperations.newAccount().username(username).create();
 
     // Fill the cache for the observed account.
-    groupIncludeCache.getGroupsWithMember(account.accountId());
+    groupIncludeCache.getGroupsWithMember(accountId);
     String groupName = createGroup("users");
     AccountGroup.UUID groupUuid = new AccountGroup.UUID(gApi.groups().id(groupName).get().id);
 
     gApi.groups().id(groupName).addMembers(username);
 
     Collection<AccountGroup.UUID> groupsWithMemberAfterAddition =
-        groupIncludeCache.getGroupsWithMember(account.accountId());
+        groupIncludeCache.getGroupsWithMember(accountId);
     assertThat(groupsWithMemberAfterAddition).contains(groupUuid);
 
     gApi.groups().id(groupName).removeMembers(username);
 
     Collection<AccountGroup.UUID> groupsWithMemberAfterRemoval =
-        groupIncludeCache.getGroupsWithMember(account.accountId());
+        groupIncludeCache.getGroupsWithMember(accountId);
     assertThat(groupsWithMemberAfterRemoval).doesNotContain(groupUuid);
   }
 
@@ -411,19 +410,17 @@
 
   @Test
   public void cachedGroupsForMemberAreUpdatedOnGroupCreation() throws Exception {
-    com.google.gerrit.acceptance.testsuite.account.TestAccount account =
-        accountOperations.newAccount().create();
+    Account.Id accountId = accountOperations.newAccount().create();
 
     // Fill the cache for the observed account.
-    groupIncludeCache.getGroupsWithMember(account.accountId());
+    groupIncludeCache.getGroupsWithMember(accountId);
 
     GroupInput groupInput = new GroupInput();
     groupInput.name = name("Users");
-    groupInput.members = ImmutableList.of(String.valueOf(account.accountId().get()));
+    groupInput.members = ImmutableList.of(String.valueOf(accountId.get()));
     GroupInfo group = gApi.groups().create(groupInput).get();
 
-    Collection<AccountGroup.UUID> groups =
-        groupIncludeCache.getGroupsWithMember(account.accountId());
+    Collection<AccountGroup.UUID> groups = groupIncludeCache.getGroupsWithMember(accountId);
     assertThat(groups).containsExactly(new AccountGroup.UUID(group.id));
   }
 
@@ -1298,7 +1295,7 @@
 
     GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
     RegistrationHandle groupIndexEventCounterHandle =
-        groupIndexedListeners.add(groupIndexedCounter);
+        groupIndexedListeners.add("gerrit", groupIndexedCounter);
     try {
       // Running the reindexer right after startup should not need to reindex any group since
       // reindexing was already done on startup.
@@ -1355,7 +1352,7 @@
 
     GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
     RegistrationHandle groupIndexEventCounterHandle =
-        groupIndexedListeners.add(groupIndexedCounter);
+        groupIndexedListeners.add("gerrit", groupIndexedCounter);
     try {
       // No group indexing happened on startup. All groups should be reindexed now.
       slaveGroupIndexer.run();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 2b1416a..e4194a3 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -20,16 +20,18 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.config.AccessCheckInfo;
 import com.google.gerrit.extensions.api.config.AccessCheckInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
@@ -39,35 +41,29 @@
 
 public class CheckAccessIT extends AbstractDaemonTest {
 
+  @Inject private GroupOperations groupOperations;
+
   private Project.NameKey normalProject;
   private Project.NameKey secretProject;
   private Project.NameKey secretRefProject;
   private TestAccount privilegedUser;
-  private InternalGroup privilegedGroup;
 
   @Before
   public void setUp() throws Exception {
     normalProject = createProject("normal");
     secretProject = createProject("secret");
     secretRefProject = createProject("secretRef");
-    privilegedGroup = group(createGroup("privilegedGroup"));
+    AccountGroup.UUID privilegedGroupUuid =
+        groupOperations.newGroup().name(name("privilegedGroup")).create();
 
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden");
-    gApi.groups().id(privilegedGroup.getGroupUUID().get()).addMembers(privilegedUser.username);
+    groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id).update();
 
-    assertThat(gApi.groups().id(privilegedGroup.getGroupUUID().get()).members().get(0).email)
-        .contains("snowden");
-
-    grant(secretProject, "refs/*", Permission.READ, false, privilegedGroup.getGroupUUID());
+    grant(secretProject, "refs/*", Permission.READ, false, privilegedGroupUuid);
     block(secretProject, "refs/*", Permission.READ, SystemGroupBackend.REGISTERED_USERS);
 
     deny(secretRefProject, "refs/*", Permission.READ, SystemGroupBackend.ANONYMOUS_USERS);
-    grant(
-        secretRefProject,
-        "refs/heads/secret/*",
-        Permission.READ,
-        false,
-        privilegedGroup.getGroupUUID());
+    grant(secretRefProject, "refs/heads/secret/*", Permission.READ, false, privilegedGroupUuid);
     block(
         secretRefProject,
         "refs/heads/secret/*",
@@ -81,13 +77,8 @@
         SystemGroupBackend.REGISTERED_USERS);
 
     // Ref permission
-    grant(
-        normalProject,
-        "refs/*",
-        Permission.VIEW_PRIVATE_CHANGES,
-        false,
-        privilegedGroup.getGroupUUID());
-    grant(normalProject, "refs/*", Permission.FORGE_SERVER, false, privilegedGroup.getGroupUUID());
+    grant(normalProject, "refs/*", Permission.VIEW_PRIVATE_CHANGES, false, privilegedGroupUuid);
+    grant(normalProject, "refs/*", Permission.FORGE_SERVER, false, privilegedGroupUuid);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 5be8dfd..18888ea 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -16,6 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_GLOBAL;
+import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_PARENT;
+import static com.google.gerrit.server.project.ProjectState.OVERRIDDEN_BY_GLOBAL;
+import static com.google.gerrit.server.project.ProjectState.OVERRIDDEN_BY_PARENT;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableList;
@@ -68,7 +72,7 @@
   @Before
   public void addProjectIndexedCounter() {
     projectIndexedCounter = new ProjectIndexedCounter();
-    projectIndexedCounterHandle = projectIndexedListeners.add(projectIndexedCounter);
+    projectIndexedCounterHandle = projectIndexedListeners.add("gerrit", projectIndexedCounter);
   }
 
   @After
@@ -416,7 +420,7 @@
     ConfigInfo info = getConfig();
     assertThat(info.maxObjectSizeLimit.value).isNull();
     assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
   }
 
   @Test
@@ -425,13 +429,30 @@
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
     assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
 
     // Clear the value
     info = setMaxObjectSize("0");
     assertThat(info.maxObjectSizeLimit.value).isNull();
     assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
+  public void maxObjectSizeIsInheritedFromParentProject() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
+    ConfigInfo info = setMaxObjectSize("100k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+
+    info = getConfig(child);
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary)
+        .isEqualTo(String.format(INHERITED_FROM_PARENT, project));
   }
 
   @Test
@@ -441,21 +462,75 @@
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
     assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
 
     info = getConfig(child);
     assertThat(info.maxObjectSizeLimit.value).isNull();
     assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+  }
+
+  @Test
+  public void maxObjectSizeOverridesParentProjectWhenNotSetOnParent() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
+    ConfigInfo info = setMaxObjectSize("0");
+    assertThat(info.maxObjectSizeLimit.value).isNull();
+    assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+
+    info = setMaxObjectSize(child, "100k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+  }
+
+  @Test
+  public void maxObjectSizeOverridesParentProjectWhenLower() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
+    ConfigInfo info = setMaxObjectSize("200k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("200k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+
+    info = setMaxObjectSize(child, "100k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
+  public void maxObjectSizeDoesNotOverrideParentProjectWhenHigher() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
+    ConfigInfo info = setMaxObjectSize("100k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+
+    info = setMaxObjectSize(child, "200k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("200k");
+    assertThat(info.maxObjectSizeLimit.summary)
+        .isEqualTo(String.format(OVERRIDDEN_BY_PARENT, project));
   }
 
   @Test
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
   public void maxObjectSizeIsInheritedFromGlobalConfig() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
     ConfigInfo info = getConfig();
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
     assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isEqualTo("200k");
+    assertThat(info.maxObjectSizeLimit.summary).isEqualTo(INHERITED_FROM_GLOBAL);
+
+    info = getConfig(child);
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isEqualTo(INHERITED_FROM_GLOBAL);
   }
 
   @Test
@@ -464,16 +539,40 @@
     ConfigInfo info = setMaxObjectSize("100k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
     assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isEqualTo("200k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "receive.maxObjectSizeLimit", value = "300k")
+  public void inheritedMaxObjectSizeOverridesGlobalConfigWhenLower() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
+    ConfigInfo info = setMaxObjectSize("200k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("200k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
+
+    info = setMaxObjectSize(child, "100k");
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+    assertThat(info.maxObjectSizeLimit.summary).isNull();
   }
 
   @Test
   @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
+  @GerritConfig(name = "receive.inheritProjectMaxObjectSizeLimit", value = "true")
   public void maxObjectSizeDoesNotOverrideGlobalConfigWhenHigher() throws Exception {
+    Project.NameKey child = createProject(name("child"), project);
+
     ConfigInfo info = setMaxObjectSize("300k");
     assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
     assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("300k");
-    assertThat(info.maxObjectSizeLimit.inheritedValue).isEqualTo("200k");
+    assertThat(info.maxObjectSizeLimit.summary).isEqualTo(OVERRIDDEN_BY_GLOBAL);
+
+    info = getConfig(child);
+    assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
+    assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+    assertThat(info.maxObjectSizeLimit.summary).isEqualTo(OVERRIDDEN_BY_GLOBAL);
   }
 
   @Test
@@ -487,10 +586,6 @@
     return gApi.projects().name(name.get()).config(input);
   }
 
-  private ConfigInfo setConfig(ConfigInput input) throws Exception {
-    return setConfig(project, input);
-  }
-
   private ConfigInfo getConfig(Project.NameKey name) throws Exception {
     return gApi.projects().name(name.get()).config();
   }
@@ -517,9 +612,13 @@
   }
 
   private ConfigInfo setMaxObjectSize(String value) throws Exception {
+    return setMaxObjectSize(project, value);
+  }
+
+  private ConfigInfo setMaxObjectSize(Project.NameKey name, String value) throws Exception {
     ConfigInput input = new ConfigInput();
     input.maxObjectSizeLimit = value;
-    return setConfig(input);
+    return setConfig(name, input);
   }
 
   private static class ProjectIndexedCounter implements ProjectIndexedListener {
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 8a3d0f3..ca4304e 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -920,6 +920,7 @@
     CountDownLatch reindexed = new CountDownLatch(1);
     RegistrationHandle handle =
         changeIndexedListeners.add(
+            "gerrit",
             new ChangeIndexedListener() {
               @Override
               public void onChangeIndexed(String projectName, int id) {
@@ -1086,6 +1087,7 @@
     WebLinkInfo expectedWebLinkInfo = new WebLinkInfo("foo", "imageUrl", "url");
     RegistrationHandle handle =
         patchSetLinks.add(
+            "gerrit",
             new PatchSetWebLink() {
               @Override
               public WebLinkInfo getPatchSetWebLink(String projectName, String commit) {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 7ad34a6..f1e67c1 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -213,6 +213,24 @@
   }
 
   @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void validateConnected() throws Exception {
+    RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
+    testRepo.reset(c);
+
+    String r = "refs/heads/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    RevCommit amended =
+        testRepo.amend(c).message("different initial commit").insertChangeId().create();
+    testRepo.reset(amended);
+    r = "refs/for/master";
+    pr = pushHead(testRepo, r, false);
+    assertPushRejected(pr, r, "no common ancestry");
+  }
+
+  @Test
   public void pushInitialCommitForRefsMetaConfigBranch() throws Exception {
     // delete refs/meta/config
     try (Repository repo = repoManager.openRepository(project);
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index c90b3d3..0d5d2cd 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -47,13 +47,8 @@
   }
 
   @ConfigSuite.Config
-  public static Config elasticsearchV6_2() {
-    return getConfig(ElasticVersion.V6_2);
-  }
-
-  @ConfigSuite.Config
-  public static Config elasticsearchV6_3() {
-    return getConfig(ElasticVersion.V6_3);
+  public static Config elasticsearchV6() {
+    return getConfig(ElasticVersion.V6_4);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/rest/BUILD b/javatests/com/google/gerrit/acceptance/rest/BUILD
index b4940bc..b94a98d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/BUILD
@@ -4,7 +4,10 @@
     srcs = glob(["*IT.java"]),
     group = "rest_bindings",
     labels = ["rest"],
-    deps = [":util"],
+    deps = [
+        ":util",
+        "//java/com/google/gerrit/server/logging",
+    ],
 )
 
 java_library(
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 1369eb3..137dc21 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -18,6 +18,9 @@
 import static org.apache.http.HttpStatus.SC_CREATED;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.truth.Expect;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
@@ -26,22 +29,31 @@
 import com.google.gerrit.httpd.restapi.ParameterParser;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 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.TraceContext;
 import com.google.gerrit.server.project.CreateProjectArgs;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
 import java.util.List;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import org.apache.http.message.BasicHeader;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 public class TraceIT extends AbstractDaemonTest {
+  @Rule public final Expect expect = Expect.create();
+
   @Inject private DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
   @Inject private DynamicSet<CommitValidationListener> commitValidationListeners;
+  @Inject private WorkQueue workQueue;
 
   private TraceValidatingProjectCreationValidationListener projectCreationListener;
   private RegistrationHandle projectCreationListenerRegistrationHandle;
@@ -52,9 +64,10 @@
   public void setup() {
     projectCreationListener = new TraceValidatingProjectCreationValidationListener();
     projectCreationListenerRegistrationHandle =
-        projectCreationValidationListeners.add(projectCreationListener);
+        projectCreationValidationListeners.add("gerrit", projectCreationListener);
     commitValidationListener = new TraceValidatingCommitValidationListener();
-    commitValidationRegistrationHandle = commitValidationListeners.add(commitValidationListener);
+    commitValidationRegistrationHandle =
+        commitValidationListeners.add("gerrit", commitValidationListener);
   }
 
   @After
@@ -68,38 +81,92 @@
     RestResponse response = adminRestSession.put("/projects/new1");
     assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
-    assertThat(projectCreationListener.foundTraceId).isFalse();
+    assertThat(projectCreationListener.traceId).isNull();
     assertThat(projectCreationListener.isLoggingForced).isFalse();
   }
 
   @Test
-  public void restCallWithTrace() throws Exception {
+  public void restCallWithTraceRequestParam() throws Exception {
     RestResponse response =
         adminRestSession.put("/projects/new2?" + ParameterParser.TRACE_PARAMETER);
     assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
-    assertThat(projectCreationListener.foundTraceId).isTrue();
+    assertThat(projectCreationListener.traceId).isNotNull();
     assertThat(projectCreationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void restCallWithTraceTrue() throws Exception {
+  public void restCallWithTraceRequestParamAndProvidedTraceId() throws Exception {
     RestResponse response =
-        adminRestSession.put("/projects/new3?" + ParameterParser.TRACE_PARAMETER + "=true");
+        adminRestSession.put("/projects/new3?" + ParameterParser.TRACE_PARAMETER + "=issue/123");
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+  }
+
+  @Test
+  public void restCallWithTraceHeader() throws Exception {
+    RestResponse response =
+        adminRestSession.putWithHeader(
+            "/projects/new4", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, null));
     assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
-    assertThat(projectCreationListener.foundTraceId).isTrue();
+    assertThat(projectCreationListener.traceId).isNotNull();
     assertThat(projectCreationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void restCallWithTraceFalse() throws Exception {
+  public void restCallWithTraceHeaderAndProvidedTraceId() throws Exception {
     RestResponse response =
-        adminRestSession.put("/projects/new4?" + ParameterParser.TRACE_PARAMETER + "=false");
+        adminRestSession.putWithHeader(
+            "/projects/new5", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
     assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
-    assertThat(projectCreationListener.foundTraceId).isFalse();
-    assertThat(projectCreationListener.isLoggingForced).isFalse();
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+  }
+
+  @Test
+  public void restCallWithTraceRequestParamAndTraceHeader() throws Exception {
+    // trace ID only specified by trace header
+    RestResponse response =
+        adminRestSession.putWithHeader(
+            "/projects/new6?trace", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+
+    // trace ID only specified by trace request parameter
+    response =
+        adminRestSession.putWithHeader(
+            "/projects/new7?trace=issue/123", new BasicHeader(RestApiServlet.X_GERRIT_TRACE, null));
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+
+    // same trace ID specified by trace header and trace request parameter
+    response =
+        adminRestSession.putWithHeader(
+            "/projects/new8?trace=issue/123",
+            new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/123"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
+    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+
+    // different trace IDs specified by trace header and trace request parameter
+    response =
+        adminRestSession.putWithHeader(
+            "/projects/new9?trace=issue/123",
+            new BasicHeader(RestApiServlet.X_GERRIT_TRACE, "issue/456"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+    assertThat(response.getHeaders(RestApiServlet.X_GERRIT_TRACE))
+        .containsExactly("issue/123", "issue/456");
+    assertThat(projectCreationListener.traceIds).containsExactly("issue/123", "issue/456");
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
   }
 
   @Test
@@ -107,7 +174,7 @@
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isFalse();
+    assertThat(commitValidationListener.traceId).isNull();
     assertThat(commitValidationListener.isLoggingForced).isFalse();
   }
 
@@ -117,36 +184,26 @@
     push.setPushOptions(ImmutableList.of("trace"));
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isTrue();
+    assertThat(commitValidationListener.traceId).isNotNull();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void pushWithTraceTrue() throws Exception {
+  public void pushWithTraceAndProvidedTraceId() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace=true"));
+    push.setPushOptions(ImmutableList.of("trace=issue/123"));
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isTrue();
+    assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void pushWithTraceFalse() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace=false"));
-    PushOneCommit.Result r = push.to("refs/heads/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isFalse();
-    assertThat(commitValidationListener.isLoggingForced).isFalse();
-  }
-
-  @Test
   public void pushForReviewWithoutTrace() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isFalse();
+    assertThat(commitValidationListener.traceId).isNull();
     assertThat(commitValidationListener.isLoggingForced).isFalse();
   }
 
@@ -156,50 +213,85 @@
     push.setPushOptions(ImmutableList.of("trace"));
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isTrue();
+    assertThat(commitValidationListener.traceId).isNotNull();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void pushForReviewWithTraceTrue() throws Exception {
+  public void pushForReviewWithTraceAndProvidedTraceId() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace=true"));
+    push.setPushOptions(ImmutableList.of("trace=issue/123"));
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isTrue();
+    assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void pushForReviewWithTraceFalse() throws Exception {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    push.setPushOptions(ImmutableList.of("trace=false"));
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
-    assertThat(commitValidationListener.foundTraceId).isFalse();
-    assertThat(commitValidationListener.isLoggingForced).isFalse();
+  public void workQueueCopyLoggingContext() throws Exception {
+    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+    assertForceLogging(false);
+    try (TraceContext traceContext = TraceContext.open().forceLogging().addTag("foo", "bar")) {
+      SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
+      assertThat(tagMap.keySet()).containsExactly("foo");
+      assertThat(tagMap.get("foo")).containsExactly("bar");
+      assertForceLogging(true);
+
+      workQueue
+          .createQueue(1, "test-queue")
+          .submit(
+              () -> {
+                // Verify that the tags and force logging flag have been propagated to the new
+                // thread.
+                SortedMap<String, SortedSet<Object>> threadTagMap =
+                    LoggingContext.getInstance().getTags().asMap();
+                expect.that(threadTagMap.keySet()).containsExactly("foo");
+                expect.that(threadTagMap.get("foo")).containsExactly("bar");
+                expect
+                    .that(LoggingContext.getInstance().shouldForceLogging(null, null, false))
+                    .isTrue();
+              })
+          .get();
+
+      // Verify that tags and force logging flag in the outer thread are still set.
+      tagMap = LoggingContext.getInstance().getTags().asMap();
+      assertThat(tagMap.keySet()).containsExactly("foo");
+      assertThat(tagMap.get("foo")).containsExactly("bar");
+      assertForceLogging(true);
+    }
+    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+    assertForceLogging(false);
+  }
+
+  private void assertForceLogging(boolean expected) {
+    assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
+        .isEqualTo(expected);
   }
 
   private static class TraceValidatingProjectCreationValidationListener
       implements ProjectCreationValidationListener {
-    Boolean foundTraceId;
+    String traceId;
+    ImmutableSet<String> traceIds;
     Boolean isLoggingForced;
 
     @Override
     public void validateNewProject(CreateProjectArgs args) throws ValidationException {
-      this.foundTraceId = LoggingContext.getInstance().getTagsAsMap().containsKey("TRACE_ID");
+      this.traceId =
+          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);
     }
   }
 
   private static class TraceValidatingCommitValidationListener implements CommitValidationListener {
-    Boolean foundTraceId;
+    String traceId;
     Boolean isLoggingForced;
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
         throws CommitValidationException {
-      this.foundTraceId = LoggingContext.getInstance().getTagsAsMap().containsKey("TRACE_ID");
+      this.traceId =
+          Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
       return ImmutableList.of();
     }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index a9182ef..b74a0d7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -60,7 +60,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
@@ -105,8 +104,6 @@
             .fromJson(
                 response.getReader(), new TypeToken<List<AccountExternalIdInfo>>() {}.getType());
 
-    Collections.sort(expectedIdInfos);
-    Collections.sort(results);
     assertThat(results).containsExactlyElementsIn(expectedIdInfos);
   }
 
@@ -133,8 +130,6 @@
             .fromJson(
                 response.getReader(), new TypeToken<List<AccountExternalIdInfo>>() {}.getType());
 
-    Collections.sort(expectedIdInfos);
-    Collections.sort(results);
     assertThat(results).containsExactlyElementsIn(expectedIdInfos);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 5580279..af11149 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1324,7 +1324,7 @@
 
   protected void addOnSubmitValidationListener(OnSubmitValidationListener listener) {
     assertThat(onSubmitValidatorHandle).isNull();
-    onSubmitValidatorHandle = onSubmitValidationListeners.add(listener);
+    onSubmitValidatorHandle = onSubmitValidationListeners.add("gerrit", listener);
   }
 
   private String getLatestDiff(Repository repo) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 171babd..f45f9dc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -331,7 +331,7 @@
     assertThat(origActions.get("abandon").label).isEqualTo("Abandon");
 
     Visitor v = new Visitor();
-    visitorHandle = actionVisitors.add(v);
+    visitorHandle = actionVisitors.add("gerrit", v);
 
     Map<String, ActionInfo> newActions =
         gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)).actions;
@@ -380,7 +380,7 @@
     assertThat(origActions.get("rebase").label).isEqualTo("Rebase");
 
     Visitor v = new Visitor();
-    visitorHandle = actionVisitors.add(v);
+    visitorHandle = actionVisitors.add("gerrit", v);
 
     // Test different codepaths within ActionJson...
     // ...via revision API.
@@ -443,7 +443,7 @@
     assertThat(origActions.get("description").label).isEqualTo("Edit Description");
 
     Visitor v = new Visitor();
-    visitorHandle = actionVisitors.add(v);
+    visitorHandle = actionVisitors.add("gerrit", v);
 
     // Unlike for the current revision, actions for old revisions are only available via the
     // revision API.
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 8cd1770..2182b2f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -97,6 +97,7 @@
     PushOneCommit.Result change = createChange();
     RegistrationHandle handle =
         changeMessageModifiers.add(
+            "gerrit",
             new ChangeMessageModifier() {
               @Override
               public String onSubmit(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index e8b8fe8..3d8d06e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -87,6 +87,7 @@
 
     RegistrationHandle handle =
         changeMessageModifiers.add(
+            "gerrit",
             new ChangeMessageModifier() {
               @Override
               public String onSubmit(
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 3534959..a64305c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -89,6 +89,7 @@
   public void webLink() throws Exception {
     RegistrationHandle handle =
         fileHistoryWebLinkDynamicSet.add(
+            "gerrit",
             new FileHistoryWebLink() {
               @Override
               public WebLinkInfo getFileHistoryWebLink(
@@ -111,6 +112,7 @@
   public void webLinkNoRefsMetaConfig() throws Exception {
     RegistrationHandle handle =
         fileHistoryWebLinkDynamicSet.add(
+            "gerrit",
             new FileHistoryWebLink() {
               @Override
               public WebLinkInfo getFileHistoryWebLink(
diff --git a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index 14b3858..f4a833f 100644
--- a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -76,6 +76,7 @@
 
     eventListenerRegistration =
         source.add(
+            "gerrit",
             new CommentAddedListener() {
               @Override
               public void onCommentAdded(Event event) {
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index a5d78c6..87c5ace 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -610,7 +610,7 @@
   }
 
   private void addListener(NotesMigrationStateListener listener) {
-    addedListeners.add(listeners.add(listener));
+    addedListeners.add(listeners.add("gerrit", listener));
   }
 
   private ImmutableSortedSet<String> getObjectFiles(Project.NameKey project) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 45b7767..8b0c56b 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -82,6 +82,7 @@
 
     eventListenerRegistration =
         source.add(
+            "gerrit",
             new CommentAddedListener() {
               @Override
               public void onCommentAdded(Event event) {
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
new file mode 100644
index 0000000..d1b05e3
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/rules/IgnoreSelfApprovalRuleIT.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.rules;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.rules.IgnoreSelfApprovalRule;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.Map;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+@NoHttpd
+public class IgnoreSelfApprovalRuleIT extends AbstractDaemonTest {
+  @Inject private IgnoreSelfApprovalRule rule;
+
+  @Test
+  public void blocksWhenUploaderIsOnlyApprover() throws Exception {
+    enableRule("Code-Review", true);
+
+    PushOneCommit.Result r = createChange();
+    approve(r.getChangeId());
+
+    Collection<SubmitRecord> submitRecords =
+        rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
+
+    assertThat(submitRecords).hasSize(1);
+    SubmitRecord result = submitRecords.iterator().next();
+    assertThat(result.status).isEqualTo(SubmitRecord.Status.NOT_READY);
+    assertThat(result.labels).isNotEmpty();
+    assertThat(result.requirements)
+        .containsExactly(
+            SubmitRequirement.builder()
+                .setFallbackText("Approval from non-uploader required")
+                .setType("non_uploader_approval")
+                .build());
+  }
+
+  @Test
+  public void allowsSubmissionWhenChangeHasNonUploaderApproval() throws Exception {
+    enableRule("Code-Review", true);
+
+    // Create change as user
+    TestRepository<InMemoryRepository> userTestRepo = cloneProject(project, user);
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
+    PushOneCommit.Result r = push.to("refs/for/master");
+
+    // Approve as admin
+    approve(r.getChangeId());
+
+    Collection<SubmitRecord> submitRecords =
+        rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
+    assertThat(submitRecords).isEmpty();
+  }
+
+  @Test
+  public void doesNothingByDefault() throws Exception {
+    enableRule("Code-Review", false);
+
+    PushOneCommit.Result r = createChange();
+    approve(r.getChangeId());
+
+    Collection<SubmitRecord> submitRecords =
+        rule.evaluate(r.getChange(), SubmitRuleOptions.defaults());
+    assertThat(submitRecords).isEmpty();
+  }
+
+  private void enableRule(String labelName, boolean newState) throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      Map<String, LabelType> localLabelSections = u.getConfig().getLabelSections();
+      if (localLabelSections.isEmpty()) {
+        localLabelSections.putAll(projectCache.getAllProjects().getConfig().getLabelSections());
+      }
+      localLabelSections.get(labelName).setIgnoreSelfApproval(newState);
+      u.save();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
index 25bb7a6..ed3cdbc 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -50,7 +50,7 @@
   @Before
   public void addChangeIndexedCounter() {
     changeIndexedCounter = new ChangeIndexedCounter();
-    changeIndexedCounterHandle = changeIndexedListeners.add(changeIndexedCounter);
+    changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
   }
 
   @After
diff --git a/javatests/com/google/gerrit/acceptance/ssh/BUILD b/javatests/com/google/gerrit/acceptance/ssh/BUILD
index b195ecc..a01cd3e 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/BUILD
+++ b/javatests/com/google/gerrit/acceptance/ssh/BUILD
@@ -17,6 +17,7 @@
     vm_args = ["-Xmx512m"],
     deps = [
         ":util",
+        "//java/com/google/gerrit/server/logging",
         "//lib/commons:compress",
     ],
 )
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 95da5a6..9d69955 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -46,13 +46,8 @@
   }
 
   @ConfigSuite.Config
-  public static Config elasticsearchV6_2() {
-    return getConfig(ElasticVersion.V6_2);
-  }
-
-  @ConfigSuite.Config
-  public static Config elasticsearchV6_3() {
-    return getConfig(ElasticVersion.V6_3);
+  public static Config elasticsearchV6() {
+    return getConfig(ElasticVersion.V6_4);
   }
 
   @Override
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 126b0ee..e603413 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.acceptance.ssh;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -28,7 +30,6 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.sshd.Commands;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import org.junit.Test;
@@ -82,16 +83,9 @@
   private static final ImmutableMap<String, List<String>> MASTER_COMMANDS =
       ImmutableMap.of(
           Commands.ROOT,
-          ImmutableList.copyOf(
-              new ArrayList<String>() {
-                private static final long serialVersionUID = 1L;
-
-                {
-                  addAll(COMMON_ROOT_COMMANDS);
-                  addAll(MASTER_ONLY_ROOT_COMMANDS);
-                  Collections.sort(this);
-                }
-              }),
+          Streams.concat(COMMON_ROOT_COMMANDS.stream(), MASTER_ONLY_ROOT_COMMANDS.stream())
+              .sorted()
+              .collect(toImmutableList()),
           "index",
           ImmutableList.of(
               "changes", "changes-in-project"), // "activate" and "start" are not included
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index b01c432..899b0cf 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -2,6 +2,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+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;
@@ -27,7 +28,7 @@
   public void setup() {
     projectCreationListener = new TraceValidatingProjectCreationValidationListener();
     projectCreationListenerRegistrationHandle =
-        projectCreationValidationListeners.add(projectCreationListener);
+        projectCreationValidationListeners.add("gerrit", projectCreationListener);
   }
 
   @After
@@ -39,6 +40,7 @@
   public void sshCallWithoutTrace() throws Exception {
     adminSshSession.exec("gerrit create-project new1");
     adminSshSession.assertSuccess();
+    assertThat(projectCreationListener.traceId).isNull();
     assertThat(projectCreationListener.foundTraceId).isFalse();
     assertThat(projectCreationListener.isLoggingForced).isFalse();
   }
@@ -50,18 +52,40 @@
     // The trace ID is written to stderr.
     adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
 
+    assertThat(projectCreationListener.traceId).isNotNull();
     assertThat(projectCreationListener.foundTraceId).isTrue();
     assertThat(projectCreationListener.isLoggingForced).isTrue();
   }
 
+  @Test
+  public void sshCallWithTraceAndProvidedTraceId() throws Exception {
+    adminSshSession.exec("gerrit create-project --trace --trace-id issue/123 new3");
+
+    // The trace ID is written to stderr.
+    adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
+
+    assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
+    assertThat(projectCreationListener.foundTraceId).isTrue();
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+  }
+
+  @Test
+  public void sshCallWithTraceIdAndWithoutTraceFails() throws Exception {
+    adminSshSession.exec("gerrit create-project --trace-id issue/123 new3");
+    adminSshSession.assertFailure("A trace ID can only be set if --trace was specified.");
+  }
+
   private static class TraceValidatingProjectCreationValidationListener
       implements ProjectCreationValidationListener {
+    String traceId;
     Boolean foundTraceId;
     Boolean isLoggingForced;
 
     @Override
     public void validateNewProject(CreateProjectArgs args) throws ValidationException {
-      this.foundTraceId = LoggingContext.getInstance().getTagsAsMap().containsKey("TRACE_ID");
+      this.traceId =
+          Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
+      this.foundTraceId = traceId != null;
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
new file mode 100644
index 0000000..954b0e6
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -0,0 +1,656 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.group;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.truth.Correspondence;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class GroupOperationsImplTest extends AbstractDaemonTest {
+
+  @Rule public ExpectedException expectedException = ExpectedException.none();
+
+  @Inject private AccountOperations accountOperations;
+
+  @Inject private GroupOperationsImpl groupOperations;
+
+  private int uniqueGroupNameIndex;
+
+  @Test
+  public void groupCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.id).isEqualTo(groupUuid.get());
+    assertThat(foundGroup.name).isNotEmpty();
+  }
+
+  @Test
+  public void twoGroupsWithoutAnyParametersDoNotClash() throws Exception {
+    AccountGroup.UUID groupUuid1 = groupOperations.newGroup().create();
+    AccountGroup.UUID groupUuid2 = groupOperations.newGroup().create();
+
+    TestGroup group1 = groupOperations.group(groupUuid1).get();
+    TestGroup group2 = groupOperations.group(groupUuid2).get();
+    assertThat(group1.groupUuid()).isNotEqualTo(group2.groupUuid());
+  }
+
+  @Test
+  public void groupCreatedByTestApiCanBeRetrievedViaOfficialApi() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().name("unique group created via test API").create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.id).isEqualTo(groupUuid.get());
+    assertThat(foundGroup.name).isEqualTo("unique group created via test API");
+  }
+
+  @Test
+  public void specifiedNameIsRespectedForGroupCreation() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().name("XYZ-123-this-name-must-be-unique").create();
+
+    GroupInfo group = getGroupFromServer(groupUuid);
+    assertThat(group.name).isEqualTo("XYZ-123-this-name-must-be-unique");
+  }
+
+  @Test
+  public void specifiedDescriptionIsRespectedForGroupCreation() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().description("All authenticated users").create();
+
+    GroupInfo group = getGroupFromServer(groupUuid);
+    assertThat(group.description).isEqualTo("All authenticated users");
+  }
+
+  @Test
+  public void requestingNoDescriptionIsPossibleForGroupCreation() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearDescription().create();
+
+    GroupInfo group = getGroupFromServer(groupUuid);
+    assertThat(group.description).isNull();
+  }
+
+  @Test
+  public void specifiedOwnerIsRespectedForGroupCreation() throws Exception {
+    AccountGroup.UUID ownerGroupUuid = groupOperations.newGroup().create();
+
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().ownerGroupUuid(ownerGroupUuid).create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.ownerId).isEqualTo(ownerGroupUuid.get());
+  }
+
+  @Test
+  public void specifiedVisibilityIsRespectedForGroupCreation() throws Exception {
+    AccountGroup.UUID group1Uuid = groupOperations.newGroup().visibleToAll(true).create();
+    AccountGroup.UUID group2Uuid = groupOperations.newGroup().visibleToAll(false).create();
+
+    GroupInfo foundGroup1 = getGroupFromServer(group1Uuid);
+    GroupInfo foundGroup2 = getGroupFromServer(group2Uuid);
+    assertThat(foundGroup1.options.visibleToAll).isTrue();
+    // False == null
+    assertThat(foundGroup2.options.visibleToAll).isNull();
+  }
+
+  @Test
+  public void specifiedMembersAreRespectedForGroupCreation() throws Exception {
+    Account.Id account1Id = accountOperations.newAccount().create();
+    Account.Id account2Id = accountOperations.newAccount().create();
+    Account.Id account3Id = accountOperations.newAccount().create();
+    Account.Id account4Id = accountOperations.newAccount().create();
+
+    AccountGroup.UUID groupUuid =
+        groupOperations
+            .newGroup()
+            .members(account1Id, account2Id)
+            .addMember(account3Id)
+            .addMember(account4Id)
+            .create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.members)
+        .comparingElementsUsing(getAccountToIdCorrespondence())
+        .containsExactly(account1Id, account2Id, account3Id, account4Id);
+  }
+
+  @Test
+  public void directlyAddingMembersIsPossibleForGroupCreation() throws Exception {
+    Account.Id account1Id = accountOperations.newAccount().create();
+    Account.Id account2Id = accountOperations.newAccount().create();
+
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().addMember(account1Id).addMember(account2Id).create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.members)
+        .comparingElementsUsing(getAccountToIdCorrespondence())
+        .containsExactly(account1Id, account2Id);
+  }
+
+  @Test
+  public void requestingNoMembersIsPossibleForGroupCreation() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearMembers().create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.members).isEmpty();
+  }
+
+  @Test
+  public void specifiedSubgroupsAreRespectedForGroupCreation() throws Exception {
+    AccountGroup.UUID group1Uuid = groupOperations.newGroup().create();
+    AccountGroup.UUID group2Uuid = groupOperations.newGroup().create();
+    AccountGroup.UUID group3Uuid = groupOperations.newGroup().create();
+    AccountGroup.UUID group4Uuid = groupOperations.newGroup().create();
+
+    AccountGroup.UUID groupUuid =
+        groupOperations
+            .newGroup()
+            .subgroups(group1Uuid, group2Uuid)
+            .addSubgroup(group3Uuid)
+            .addSubgroup(group4Uuid)
+            .create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.includes)
+        .comparingElementsUsing(getGroupToUuidCorrespondence())
+        .containsExactly(group1Uuid, group2Uuid, group3Uuid, group4Uuid);
+  }
+
+  @Test
+  public void directlyAddingSubgroupsIsPossibleForGroupCreation() throws Exception {
+    AccountGroup.UUID group1Uuid = groupOperations.newGroup().create();
+    AccountGroup.UUID group2Uuid = groupOperations.newGroup().create();
+
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().addSubgroup(group1Uuid).addSubgroup(group2Uuid).create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.includes)
+        .comparingElementsUsing(getGroupToUuidCorrespondence())
+        .containsExactly(group1Uuid, group2Uuid);
+  }
+
+  @Test
+  public void requestingNoSubgroupsIsPossibleForGroupCreation() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearSubgroups().create();
+
+    GroupInfo foundGroup = getGroupFromServer(groupUuid);
+    assertThat(foundGroup.includes).isEmpty();
+  }
+
+  @Test
+  public void existingGroupCanBeCheckedForExistence() throws Exception {
+    AccountGroup.UUID groupUuid = createGroupInServer(createArbitraryGroupInput());
+
+    boolean exists = groupOperations.group(groupUuid).exists();
+
+    assertThat(exists).isTrue();
+  }
+
+  @Test
+  public void notExistingGroupCanBeCheckedForExistence() throws Exception {
+    AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
+
+    boolean exists = groupOperations.group(notExistingGroupUuid).exists();
+
+    assertThat(exists).isFalse();
+  }
+
+  @Test
+  public void retrievingNotExistingGroupFails() throws Exception {
+    AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
+
+    expectedException.expect(IllegalStateException.class);
+    groupOperations.group(notExistingGroupUuid).get();
+  }
+
+  @Test
+  public void groupNotCreatedByTestApiCanBeRetrieved() throws Exception {
+    GroupInput input = createArbitraryGroupInput();
+    input.name = "unique group not created via test API";
+    AccountGroup.UUID groupUuid = createGroupInServer(input);
+
+    TestGroup foundGroup = groupOperations.group(groupUuid).get();
+
+    assertThat(foundGroup.groupUuid()).isEqualTo(groupUuid);
+    assertThat(foundGroup.name()).isEqualTo("unique group not created via test API");
+  }
+
+  @Test
+  public void uuidOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().create();
+
+    AccountGroup.UUID foundGroupUuid = groupOperations.group(groupUuid).get().groupUuid();
+
+    assertThat(foundGroupUuid).isEqualTo(groupUuid);
+  }
+
+  @Test
+  public void nameOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().name("ABC-789-this-name-must-be-unique").create();
+
+    String groupName = groupOperations.group(groupUuid).get().name();
+
+    assertThat(groupName).isEqualTo("ABC-789-this-name-must-be-unique");
+  }
+
+  @Test
+  public void nameKeyOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().name("ABC-789-this-name-must-be-unique").create();
+
+    AccountGroup.NameKey groupName = groupOperations.group(groupUuid).get().nameKey();
+
+    assertThat(groupName).isEqualTo(new AccountGroup.NameKey("ABC-789-this-name-must-be-unique"));
+  }
+
+  @Test
+  public void descriptionOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations
+            .newGroup()
+            .description("This is a very detailed description of this group.")
+            .create();
+
+    Optional<String> description = groupOperations.group(groupUuid).get().description();
+
+    assertThat(description).hasValue("This is a very detailed description of this group.");
+  }
+
+  @Test
+  public void emptyDescriptionOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearDescription().create();
+
+    Optional<String> description = groupOperations.group(groupUuid).get().description();
+
+    assertThat(description).isEmpty();
+  }
+
+  @Test
+  public void ownerGroupUuidOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID originalOwnerGroupUuid = new AccountGroup.UUID("owner group");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().ownerGroupUuid(originalOwnerGroupUuid).create();
+
+    AccountGroup.UUID ownerGroupUuid = groupOperations.group(groupUuid).get().ownerGroupUuid();
+
+    assertThat(ownerGroupUuid).isEqualTo(originalOwnerGroupUuid);
+  }
+
+  @Test
+  public void visibilityOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID visibleGroupUuid = groupOperations.newGroup().visibleToAll(true).create();
+    AccountGroup.UUID invisibleGroupUuid = groupOperations.newGroup().visibleToAll(false).create();
+
+    TestGroup visibleGroup = groupOperations.group(visibleGroupUuid).get();
+    TestGroup invisibleGroup = groupOperations.group(invisibleGroupUuid).get();
+
+    assertThat(visibleGroup.visibleToAll()).named("visibility of visible group").isTrue();
+    assertThat(invisibleGroup.visibleToAll()).named("visibility of invisible group").isFalse();
+  }
+
+  @Test
+  public void createdOnOfExistingGroupCanBeRetrieved() throws Exception {
+    GroupInfo group = gApi.groups().create(createArbitraryGroupInput()).detail();
+    AccountGroup.UUID groupUuid = new AccountGroup.UUID(group.id);
+
+    Timestamp createdOn = groupOperations.group(groupUuid).get().createdOn();
+
+    assertThat(createdOn).isEqualTo(group.createdOn);
+  }
+
+  @Test
+  public void membersOfExistingGroupCanBeRetrieved() throws Exception {
+    Account.Id memberId1 = new Account.Id(1000);
+    Account.Id memberId2 = new Account.Id(2000);
+    Account.Id memberId3 = new Account.Id(3000);
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().members(memberId1, memberId2, memberId3).create();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+
+    assertThat(members).containsExactly(memberId1, memberId2, memberId3);
+  }
+
+  @Test
+  public void emptyMembersOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearMembers().create();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+
+    assertThat(members).isEmpty();
+  }
+
+  @Test
+  public void subgroupsOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID subgroupUuid3 = new AccountGroup.UUID("subgroup 3");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2, subgroupUuid3).create();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+
+    assertThat(subgroups).containsExactly(subgroupUuid1, subgroupUuid2, subgroupUuid3);
+  }
+
+  @Test
+  public void emptySubgroupsOfExistingGroupCanBeRetrieved() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearSubgroups().create();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+
+    assertThat(subgroups).isEmpty();
+  }
+
+  @Test
+  public void updateWithoutAnyParametersIsANoop() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().create();
+    TestGroup originalGroup = groupOperations.group(groupUuid).get();
+
+    groupOperations.group(groupUuid).forUpdate().update();
+
+    TestGroup updatedGroup = groupOperations.group(groupUuid).get();
+    assertThat(updatedGroup).isEqualTo(originalGroup);
+  }
+
+  @Test
+  public void updateWritesToInternalGroupSystem() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().description("original description").create();
+
+    groupOperations.group(groupUuid).forUpdate().description("updated description").update();
+
+    String currentDescription = getGroupFromServer(groupUuid).description;
+    assertThat(currentDescription).isEqualTo("updated description");
+  }
+
+  @Test
+  public void nameCanBeUpdated() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().name("original name").create();
+
+    groupOperations.group(groupUuid).forUpdate().name("updated name").update();
+
+    String currentName = groupOperations.group(groupUuid).get().name();
+    assertThat(currentName).isEqualTo("updated name");
+  }
+
+  @Test
+  public void descriptionCanBeUpdated() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().description("original description").create();
+
+    groupOperations.group(groupUuid).forUpdate().description("updated description").update();
+
+    Optional<String> currentDescription = groupOperations.group(groupUuid).get().description();
+    assertThat(currentDescription).hasValue("updated description");
+  }
+
+  @Test
+  public void descriptionCanBeCleared() throws Exception {
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().description("original description").create();
+
+    groupOperations.group(groupUuid).forUpdate().clearDescription().update();
+
+    Optional<String> currentDescription = groupOperations.group(groupUuid).get().description();
+    assertThat(currentDescription).isEmpty();
+  }
+
+  @Test
+  public void ownerGroupUuidCanBeUpdated() throws Exception {
+    AccountGroup.UUID originalOwnerGroupUuid = new AccountGroup.UUID("original owner");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().ownerGroupUuid(originalOwnerGroupUuid).create();
+
+    AccountGroup.UUID updatedOwnerGroupUuid = new AccountGroup.UUID("updated owner");
+    groupOperations.group(groupUuid).forUpdate().ownerGroupUuid(updatedOwnerGroupUuid).update();
+
+    AccountGroup.UUID currentOwnerGroupUuid =
+        groupOperations.group(groupUuid).get().ownerGroupUuid();
+    assertThat(currentOwnerGroupUuid).isEqualTo(updatedOwnerGroupUuid);
+  }
+
+  @Test
+  public void visibilityCanBeUpdated() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().visibleToAll(true).create();
+
+    groupOperations.group(groupUuid).forUpdate().visibleToAll(false).update();
+
+    boolean visibleToAll = groupOperations.group(groupUuid).get().visibleToAll();
+    assertThat(visibleToAll).isFalse();
+  }
+
+  @Test
+  public void membersCanBeAdded() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearMembers().create();
+
+    Account.Id memberId1 = new Account.Id(1000);
+    Account.Id memberId2 = new Account.Id(2000);
+    groupOperations.group(groupUuid).forUpdate().addMember(memberId1).addMember(memberId2).update();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+    assertThat(members).containsExactly(memberId1, memberId2);
+  }
+
+  @Test
+  public void membersCanBeRemoved() throws Exception {
+    Account.Id memberId1 = new Account.Id(1000);
+    Account.Id memberId2 = new Account.Id(2000);
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
+
+    groupOperations.group(groupUuid).forUpdate().removeMember(memberId2).update();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+    assertThat(members).containsExactly(memberId1);
+  }
+
+  @Test
+  public void memberAdditionAndRemovalCanBeMixed() throws Exception {
+    Account.Id memberId1 = new Account.Id(1000);
+    Account.Id memberId2 = new Account.Id(2000);
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
+
+    Account.Id memberId3 = new Account.Id(3000);
+    groupOperations
+        .group(groupUuid)
+        .forUpdate()
+        .removeMember(memberId1)
+        .addMember(memberId3)
+        .update();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+    assertThat(members).containsExactly(memberId2, memberId3);
+  }
+
+  @Test
+  public void membersCanBeCleared() throws Exception {
+    Account.Id memberId1 = new Account.Id(1000);
+    Account.Id memberId2 = new Account.Id(2000);
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
+
+    groupOperations.group(groupUuid).forUpdate().clearMembers().update();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+    assertThat(members).isEmpty();
+  }
+
+  @Test
+  public void furtherMembersCanBeAddedAfterClearingAll() throws Exception {
+    Account.Id memberId1 = new Account.Id(1000);
+    Account.Id memberId2 = new Account.Id(2000);
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().members(memberId1, memberId2).create();
+
+    Account.Id memberId3 = new Account.Id(3000);
+    groupOperations.group(groupUuid).forUpdate().clearMembers().addMember(memberId3).update();
+
+    ImmutableSet<Account.Id> members = groupOperations.group(groupUuid).get().members();
+    assertThat(members).containsExactly(memberId3);
+  }
+
+  @Test
+  public void subgroupsCanBeAdded() throws Exception {
+    AccountGroup.UUID groupUuid = groupOperations.newGroup().clearSubgroups().create();
+
+    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    groupOperations
+        .group(groupUuid)
+        .forUpdate()
+        .addSubgroup(subgroupUuid1)
+        .addSubgroup(subgroupUuid2)
+        .update();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+    assertThat(subgroups).containsExactly(subgroupUuid1, subgroupUuid2);
+  }
+
+  @Test
+  public void subgroupsCanBeRemoved() throws Exception {
+    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
+
+    groupOperations.group(groupUuid).forUpdate().removeSubgroup(subgroupUuid2).update();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+    assertThat(subgroups).containsExactly(subgroupUuid1);
+  }
+
+  @Test
+  public void subgroupAdditionAndRemovalCanBeMixed() throws Exception {
+    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
+
+    AccountGroup.UUID subgroupUuid3 = new AccountGroup.UUID("subgroup 3");
+    groupOperations
+        .group(groupUuid)
+        .forUpdate()
+        .removeSubgroup(subgroupUuid1)
+        .addSubgroup(subgroupUuid3)
+        .update();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+    assertThat(subgroups).containsExactly(subgroupUuid2, subgroupUuid3);
+  }
+
+  @Test
+  public void subgroupsCanBeCleared() throws Exception {
+    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
+
+    groupOperations.group(groupUuid).forUpdate().clearSubgroups().update();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+    assertThat(subgroups).isEmpty();
+  }
+
+  @Test
+  public void furtherSubgroupsCanBeAddedAfterClearingAll() throws Exception {
+    AccountGroup.UUID subgroupUuid1 = new AccountGroup.UUID("subgroup 1");
+    AccountGroup.UUID subgroupUuid2 = new AccountGroup.UUID("subgroup 2");
+    AccountGroup.UUID groupUuid =
+        groupOperations.newGroup().subgroups(subgroupUuid1, subgroupUuid2).create();
+
+    AccountGroup.UUID subgroupUuid3 = new AccountGroup.UUID("subgroup 3");
+    groupOperations
+        .group(groupUuid)
+        .forUpdate()
+        .clearSubgroups()
+        .addSubgroup(subgroupUuid3)
+        .update();
+
+    ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(groupUuid).get().subgroups();
+    assertThat(subgroups).containsExactly(subgroupUuid3);
+  }
+
+  private GroupInput createArbitraryGroupInput() {
+    GroupInput groupInput = new GroupInput();
+    groupInput.name = name("verifiers-" + uniqueGroupNameIndex++);
+    return groupInput;
+  }
+
+  private GroupInfo getGroupFromServer(AccountGroup.UUID groupUuid) throws RestApiException {
+    return gApi.groups().id(groupUuid.get()).detail();
+  }
+
+  private AccountGroup.UUID createGroupInServer(GroupInput input) throws RestApiException {
+    GroupInfo group = gApi.groups().create(input).detail();
+    return new AccountGroup.UUID(group.id);
+  }
+
+  private static Correspondence<AccountInfo, Account.Id> getAccountToIdCorrespondence() {
+    return new Correspondence<AccountInfo, Account.Id>() {
+      @Override
+      public boolean compare(AccountInfo actualAccount, Account.Id expectedId) {
+        Account.Id accountId =
+            Optional.ofNullable(actualAccount)
+                .map(account -> account._accountId)
+                .map(Account.Id::new)
+                .orElse(null);
+        return Objects.equals(accountId, expectedId);
+      }
+
+      @Override
+      public String toString() {
+        return "has ID";
+      }
+    };
+  }
+
+  private static Correspondence<GroupInfo, AccountGroup.UUID> getGroupToUuidCorrespondence() {
+    return new Correspondence<GroupInfo, AccountGroup.UUID>() {
+      @Override
+      public boolean compare(GroupInfo actualGroup, AccountGroup.UUID expectedUuid) {
+        AccountGroup.UUID groupUuid =
+            Optional.ofNullable(actualGroup)
+                .map(group -> group.id)
+                .map(AccountGroup.UUID::new)
+                .orElse(null);
+        return Objects.equals(groupUuid, expectedUuid);
+      }
+
+      @Override
+      public String toString() {
+        return "has UUID";
+      }
+    };
+  }
+}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index f5614b4..8b3c08f 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -45,11 +45,13 @@
       case V2_4:
         return "elasticsearch:2.4.6-alpine";
       case V5_6:
-        return "docker.elastic.co/elasticsearch/elasticsearch:5.6.10";
+        return "docker.elastic.co/elasticsearch/elasticsearch:5.6.11";
       case V6_2:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4";
       case V6_3:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.3.2";
+      case V6_4:
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.0";
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index 1d17b5b..b8154ce 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_4);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 7c5d2e2..3445b36 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_4);
     nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index 15b58e0..851b27d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -41,7 +41,7 @@
       return;
     }
 
-    container = ElasticContainer.createAndStart(ElasticVersion.V6_3);
+    container = ElasticContainer.createAndStart(ElasticVersion.V6_4);
     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 860dca6..b598a0a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -29,21 +29,31 @@
     assertThat(ElasticVersion.forVersion("2.4.6")).isEqualTo(ElasticVersion.V2_4);
 
     assertThat(ElasticVersion.forVersion("5.6.0")).isEqualTo(ElasticVersion.V5_6);
-    assertThat(ElasticVersion.forVersion("5.6.9")).isEqualTo(ElasticVersion.V5_6);
-    assertThat(ElasticVersion.forVersion("5.6.10")).isEqualTo(ElasticVersion.V5_6);
+    assertThat(ElasticVersion.forVersion("5.6.11")).isEqualTo(ElasticVersion.V5_6);
 
     assertThat(ElasticVersion.forVersion("6.2.0")).isEqualTo(ElasticVersion.V6_2);
     assertThat(ElasticVersion.forVersion("6.2.4")).isEqualTo(ElasticVersion.V6_2);
 
     assertThat(ElasticVersion.forVersion("6.3.0")).isEqualTo(ElasticVersion.V6_3);
-    assertThat(ElasticVersion.forVersion("6.3.1")).isEqualTo(ElasticVersion.V6_3);
+    assertThat(ElasticVersion.forVersion("6.3.2")).isEqualTo(ElasticVersion.V6_3);
+
+    assertThat(ElasticVersion.forVersion("6.4.0")).isEqualTo(ElasticVersion.V6_4);
+    assertThat(ElasticVersion.forVersion("6.4.1")).isEqualTo(ElasticVersion.V6_4);
   }
 
   @Test
   public void unsupportedVersion() throws Exception {
-    exception.expect(ElasticVersion.InvalidVersion.class);
+    exception.expect(ElasticVersion.UnsupportedVersion.class);
     exception.expectMessage(
-        "Invalid version: [4.0.0]. Supported versions: " + ElasticVersion.supportedVersions());
+        "Unsupported version: [4.0.0]. Supported versions: " + ElasticVersion.supportedVersions());
     ElasticVersion.forVersion("4.0.0");
   }
+
+  @Test
+  public void version6() throws Exception {
+    assertThat(ElasticVersion.V6_2.isV6()).isTrue();
+    assertThat(ElasticVersion.V6_3.isV6()).isTrue();
+    assertThat(ElasticVersion.V6_4.isV6()).isTrue();
+    assertThat(ElasticVersion.V5_6.isV6()).isFalse();
+  }
 }
diff --git a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
index 117e474..c86160f 100644
--- a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
+++ b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.extensions.registration;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toSet;
 
 import com.google.inject.Key;
+import com.google.inject.Provider;
 import com.google.inject.util.Providers;
+import java.util.Iterator;
 import org.junit.Test;
 
 public class DynamicSetTest {
@@ -40,7 +43,7 @@
   @Test
   public void containsTrueWithSingleElement() throws Exception {
     DynamicSet<Integer> ds = new DynamicSet<>();
-    ds.add(2);
+    ds.add("gerrit", 2);
 
     assertThat(ds.contains(2)).isTrue(); // See above comment about ds.contains
   }
@@ -48,7 +51,7 @@
   @Test
   public void containsFalseWithSingleElement() throws Exception {
     DynamicSet<Integer> ds = new DynamicSet<>();
-    ds.add(2);
+    ds.add("gerrit", 2);
 
     assertThat(ds.contains(3)).isFalse(); // See above comment about ds.contains
   }
@@ -56,8 +59,8 @@
   @Test
   public void containsTrueWithTwoElements() throws Exception {
     DynamicSet<Integer> ds = new DynamicSet<>();
-    ds.add(2);
-    ds.add(4);
+    ds.add("gerrit", 2);
+    ds.add("gerrit", 4);
 
     assertThat(ds.contains(4)).isTrue(); // See above comment about ds.contains
   }
@@ -65,8 +68,8 @@
   @Test
   public void containsFalseWithTwoElements() throws Exception {
     DynamicSet<Integer> ds = new DynamicSet<>();
-    ds.add(2);
-    ds.add(4);
+    ds.add("gerrit", 2);
+    ds.add("gerrit", 4);
 
     assertThat(ds.contains(3)).isFalse(); // See above comment about ds.contains
   }
@@ -74,12 +77,12 @@
   @Test
   public void containsDynamic() throws Exception {
     DynamicSet<Integer> ds = new DynamicSet<>();
-    ds.add(2);
+    ds.add("gerrit", 2);
 
     Key<Integer> key = Key.get(Integer.class);
-    ReloadableRegistrationHandle<Integer> handle = ds.add(key, Providers.of(4));
+    ReloadableRegistrationHandle<Integer> handle = ds.add("gerrit", key, Providers.of(4));
 
-    ds.add(6);
+    ds.add("gerrit", 6);
 
     // At first, 4 is contained.
     assertThat(ds.contains(4)).isTrue(); // See above comment about ds.contains
@@ -90,4 +93,49 @@
     // And now 4 should no longer be contained.
     assertThat(ds.contains(4)).isFalse(); // See above comment about ds.contains
   }
+
+  @Test
+  public void plugins() {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add("foo", 1);
+    ds.add("bar", 2);
+    ds.add("bar", 3);
+
+    assertThat(ds.plugins()).containsExactly("bar", "foo").inOrder();
+  }
+
+  @Test
+  public void byPlugin() {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add("foo", 1);
+    ds.add("bar", 2);
+    ds.add("bar", 3);
+
+    assertThat(ds.byPlugin("foo").stream().map(Provider::get).collect(toSet())).containsExactly(1);
+    assertThat(ds.byPlugin("bar").stream().map(Provider::get).collect(toSet()))
+        .containsExactly(2, 3);
+  }
+
+  @Test
+  public void entryIterator() {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add("foo", 1);
+    ds.add("bar", 2);
+    ds.add("bar", 3);
+
+    Iterator<DynamicSet.Entry<Integer>> entryIterator = ds.entries().iterator();
+    DynamicSet.Entry<Integer> next = entryIterator.next();
+    assertThat(next.getPluginName()).isEqualTo("foo");
+    assertThat(next.getProvider().get()).isEqualTo(1);
+
+    next = entryIterator.next();
+    assertThat(next.getPluginName()).isEqualTo("bar");
+    assertThat(next.getProvider().get()).isEqualTo(2);
+
+    next = entryIterator.next();
+    assertThat(next.getPluginName()).isEqualTo("bar");
+    assertThat(next.getProvider().get()).isEqualTo(3);
+
+    assertThat(entryIterator.hasNext()).isFalse();
+  }
 }
diff --git a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index ad8f4311..266f868 100644
--- a/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -184,7 +184,7 @@
     }
 
     String cert = payload + new String(bout.toByteArray(), UTF_8);
-    Reader reader = new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
+    Reader reader = new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)), UTF_8);
     PushCertificateParser parser = new PushCertificateParser(repo, signedPushConfig);
     return parser.parse(reader);
   }
diff --git a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
index 086dcc2..1c6559b0 100644
--- a/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
+++ b/javatests/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -76,7 +76,7 @@
    */
   private ReloadableRegistrationHandle<AllRequestFilter> addFilter(AllRequestFilter filter) {
     Key<AllRequestFilter> key = Key.get(AllRequestFilter.class);
-    return filters.add(key, Providers.of(filter));
+    return filters.add("gerrit", key, Providers.of(filter));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index 07399c6..307a23e 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.template.soy.data.SoyMapData;
 import java.net.URISyntaxException;
@@ -30,7 +31,7 @@
     }
 
     String getIndexSource() {
-      return new String(indexSource);
+      return new String(indexSource, UTF_8);
     }
   }
 
diff --git a/javatests/com/google/gerrit/index/BUILD b/javatests/com/google/gerrit/index/BUILD
index 14a7048..5597ed1 100644
--- a/javatests/com/google/gerrit/index/BUILD
+++ b/javatests/com/google/gerrit/index/BUILD
@@ -6,9 +6,9 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//antlr3:query_parser",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
-        "//java/com/google/gerrit/index:query_parser",
         "//lib:guava",
         "//lib:junit",
         "//lib/antlr:java-runtime",
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 29e9a0b..7be1827 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -49,6 +49,7 @@
         "//java/com/google/gerrit/server/cache/testing",
         "//java/com/google/gerrit/server/group/testing",
         "//java/com/google/gerrit/server/ioutil",
+        "//java/com/google/gerrit/server/logging",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index 91cc2b7..6dd0f3e 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -55,7 +55,7 @@
     user = createNiceMock(IdentifiedUser.class);
     replay(user);
     backends = new DynamicSet<>();
-    backends.add(new SystemGroupBackend(new Config()));
+    backends.add("gerrit", new SystemGroupBackend(new Config()));
     backend = new UniversalGroupBackend(backends);
   }
 
@@ -123,7 +123,7 @@
     replay(member, notMember, backend);
 
     backends = new DynamicSet<>();
-    backends.add(backend);
+    backends.add("gerrit", backend);
     backend = new UniversalGroupBackend(backends);
 
     GroupMembership checker = backend.membershipsOf(member);
diff --git a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
index e91c3b4..dca2dcb 100644
--- a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
 import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -27,7 +26,6 @@
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -55,9 +53,6 @@
   private RevCommit commit_v1_3;
   private RevCommit commit_v2_5;
 
-  private List<String> expTags = new ArrayList<>();
-  private List<String> expBranches = new ArrayList<>();
-
   private RevWalk revWalk;
 
   @Override
@@ -140,12 +135,8 @@
     IncludedInResolver.Result detail = resolve(commit_v2_5);
 
     // Check that only tags and branches which refer the tip are returned
-    expTags.add(TAG_2_5);
-    expTags.add(TAG_2_5_ANNOTATED);
-    expTags.add(TAG_2_5_ANNOTATED_TWICE);
-    assertEquals(expTags, detail.getTags());
-    expBranches.add(BRANCH_2_5);
-    assertEquals(expBranches, detail.getBranches());
+    assertThat(detail.tags()).containsExactly(TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
+    assertThat(detail.branches()).containsExactly(BRANCH_2_5);
   }
 
   @Test
@@ -154,22 +145,18 @@
     IncludedInResolver.Result detail = resolve(commit_initial);
 
     // Check whether all tags and branches are returned
-    expTags.add(TAG_1_0);
-    expTags.add(TAG_1_0_1);
-    expTags.add(TAG_1_3);
-    expTags.add(TAG_2_0);
-    expTags.add(TAG_2_0_1);
-    expTags.add(TAG_2_5);
-    expTags.add(TAG_2_5_ANNOTATED);
-    expTags.add(TAG_2_5_ANNOTATED_TWICE);
-    assertEquals(expTags, detail.getTags());
-
-    expBranches.add(BRANCH_MASTER);
-    expBranches.add(BRANCH_1_0);
-    expBranches.add(BRANCH_1_3);
-    expBranches.add(BRANCH_2_0);
-    expBranches.add(BRANCH_2_5);
-    assertEquals(expBranches, detail.getBranches());
+    assertThat(detail.tags())
+        .containsExactly(
+            TAG_1_0,
+            TAG_1_0_1,
+            TAG_1_3,
+            TAG_2_0,
+            TAG_2_0_1,
+            TAG_2_5,
+            TAG_2_5_ANNOTATED,
+            TAG_2_5_ANNOTATED_TWICE);
+    assertThat(detail.branches())
+        .containsExactly(BRANCH_MASTER, BRANCH_1_0, BRANCH_1_3, BRANCH_2_0, BRANCH_2_5);
   }
 
   @Test
@@ -178,27 +165,15 @@
     IncludedInResolver.Result detail = resolve(commit_v1_3);
 
     // Check whether all succeeding tags and branches are returned
-    expTags.add(TAG_1_3);
-    expTags.add(TAG_2_5);
-    expTags.add(TAG_2_5_ANNOTATED);
-    expTags.add(TAG_2_5_ANNOTATED_TWICE);
-    assertEquals(expTags, detail.getTags());
-
-    expBranches.add(BRANCH_1_3);
-    expBranches.add(BRANCH_2_5);
-    assertEquals(expBranches, detail.getBranches());
+    assertThat(detail.tags())
+        .containsExactly(TAG_1_3, TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
+    assertThat(detail.branches()).containsExactly(BRANCH_1_3, BRANCH_2_5);
   }
 
   private IncludedInResolver.Result resolve(RevCommit commit) throws Exception {
     return IncludedInResolver.resolve(db, revWalk, commit);
   }
 
-  private void assertEquals(List<String> list1, List<String> list2) {
-    Collections.sort(list1);
-    Collections.sort(list2);
-    Assert.assertEquals(list1, list2);
-  }
-
   private void createAndCheckoutBranch(ObjectId objectId, String branchName) throws IOException {
     String fullBranchName = "refs/heads/" + branchName;
     super.createBranch(objectId, fullBranchName);
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
new file mode 100644
index 0000000..5117c01
--- /dev/null
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.Expect;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class LoggingContextAwareExecutorServiceTest {
+  @Rule public final Expect expect = Expect.create();
+
+  @Test
+  public void loggingContextPropagationToBackgroundThread() throws Exception {
+    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+    assertForceLogging(false);
+    try (TraceContext traceContext = TraceContext.open().forceLogging().addTag("foo", "bar")) {
+      SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
+      assertThat(tagMap.keySet()).containsExactly("foo");
+      assertThat(tagMap.get("foo")).containsExactly("bar");
+      assertForceLogging(true);
+
+      ExecutorService executor =
+          new LoggingContextAwareExecutorService(Executors.newFixedThreadPool(1));
+      executor
+          .submit(
+              () -> {
+                // Verify that the tags and force logging flag have been propagated to the new
+                // thread.
+                SortedMap<String, SortedSet<Object>> threadTagMap =
+                    LoggingContext.getInstance().getTags().asMap();
+                expect.that(threadTagMap.keySet()).containsExactly("foo");
+                expect.that(threadTagMap.get("foo")).containsExactly("bar");
+                expect
+                    .that(LoggingContext.getInstance().shouldForceLogging(null, null, false))
+                    .isTrue();
+              })
+          .get();
+
+      // Verify that tags and force logging flag in the outer thread are still set.
+      tagMap = LoggingContext.getInstance().getTags().asMap();
+      assertThat(tagMap.keySet()).containsExactly("foo");
+      assertThat(tagMap.get("foo")).containsExactly("bar");
+      assertForceLogging(true);
+    }
+    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+    assertForceLogging(false);
+  }
+
+  private void assertForceLogging(boolean expected) {
+    assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
+        .isEqualTo(expected);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareThreadFactoryTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareThreadFactoryTest.java
deleted file mode 100644
index 1164e27..0000000
--- a/javatests/com/google/gerrit/server/logging/LoggingContextAwareThreadFactoryTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.truth.Expect;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import org.junit.Rule;
-import org.junit.Test;
-
-public class LoggingContextAwareThreadFactoryTest {
-  @Rule public final Expect expect = Expect.create();
-
-  @Test
-  public void loggingContextPropagationToNewThread() throws Exception {
-    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
-    assertForceLogging(false);
-    try (TraceContext traceContext = TraceContext.open().forceLogging().addTag("foo", "bar")) {
-      SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
-      assertThat(tagMap.keySet()).containsExactly("foo");
-      assertThat(tagMap.get("foo")).containsExactly("bar");
-      assertForceLogging(true);
-
-      Thread thread =
-          new LoggingContextAwareThreadFactory(r -> new Thread(r, "test-thread"))
-              .newThread(
-                  () -> {
-                    // Verify that the tags and force logging flag have been propagated to the new
-                    // thread.
-                    SortedMap<String, SortedSet<Object>> threadTagMap =
-                        LoggingContext.getInstance().getTags().asMap();
-                    expect.that(threadTagMap.keySet()).containsExactly("foo");
-                    expect.that(threadTagMap.get("foo")).containsExactly("bar");
-                    expect
-                        .that(LoggingContext.getInstance().shouldForceLogging(null, null, false))
-                        .isTrue();
-                  });
-
-      // Execute in background.
-      thread.start();
-      thread.join();
-
-      // Verify that tags and force logging flag in the outer thread are still set.
-      tagMap = LoggingContext.getInstance().getTags().asMap();
-      assertThat(tagMap.keySet()).containsExactly("foo");
-      assertThat(tagMap.get("foo")).containsExactly("bar");
-      assertForceLogging(true);
-    }
-    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
-    assertForceLogging(false);
-  }
-
-  @Test
-  public void loggingContextPropagationToSameThread() throws Exception {
-    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
-    assertForceLogging(false);
-    try (TraceContext traceContext = TraceContext.open().forceLogging().addTag("foo", "bar")) {
-      SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
-      assertThat(tagMap.keySet()).containsExactly("foo");
-      assertThat(tagMap.get("foo")).containsExactly("bar");
-      assertForceLogging(true);
-
-      Thread thread =
-          new LoggingContextAwareThreadFactory()
-              .newThread(
-                  () -> {
-                    // Verify that the tags and force logging flag have been propagated to the new
-                    // thread.
-                    SortedMap<String, SortedSet<Object>> threadTagMap =
-                        LoggingContext.getInstance().getTags().asMap();
-                    expect.that(threadTagMap.keySet()).containsExactly("foo");
-                    expect.that(threadTagMap.get("foo")).containsExactly("bar");
-                    expect
-                        .that(LoggingContext.getInstance().shouldForceLogging(null, null, false))
-                        .isTrue();
-                  });
-
-      // Execute in the same thread.
-      thread.run();
-
-      // Verify that tags and force logging flag in the outer thread are still set.
-      tagMap = LoggingContext.getInstance().getTags().asMap();
-      assertThat(tagMap.keySet()).containsExactly("foo");
-      assertThat(tagMap.get("foo")).containsExactly("bar");
-      assertForceLogging(true);
-    }
-    assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
-    assertForceLogging(false);
-  }
-
-  private void assertForceLogging(boolean expected) {
-    assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
-        .isEqualTo(expected);
-  }
-}
diff --git a/javatests/com/google/gerrit/server/logging/TraceContextTest.java b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
index 7dfe14c..044d237 100644
--- a/javatests/com/google/gerrit/server/logging/TraceContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.logging.TraceContext.TraceIdConsumer;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.SortedSet;
@@ -154,6 +155,87 @@
     assertForceLogging(false);
   }
 
+  @Test
+  public void newTrace() {
+    TestTraceIdConsumer traceIdConsumer = new TestTraceIdConsumer();
+    try (TraceContext traceContext = TraceContext.newTrace(true, null, traceIdConsumer)) {
+      assertForceLogging(true);
+      assertThat(LoggingContext.getInstance().getTagsAsMap().keySet())
+          .containsExactly(RequestId.Type.TRACE_ID.name());
+    }
+    assertThat(traceIdConsumer.tagName).isEqualTo(RequestId.Type.TRACE_ID.name());
+    assertThat(traceIdConsumer.traceId).isNotNull();
+  }
+
+  @Test
+  public void newTraceWithProvidedTraceId() {
+    TestTraceIdConsumer traceIdConsumer = new TestTraceIdConsumer();
+    String traceId = "foo";
+    try (TraceContext traceContext = TraceContext.newTrace(true, traceId, traceIdConsumer)) {
+      assertForceLogging(true);
+      assertTags(ImmutableMap.of(RequestId.Type.TRACE_ID.name(), ImmutableSet.of(traceId)));
+    }
+    assertThat(traceIdConsumer.tagName).isEqualTo(RequestId.Type.TRACE_ID.name());
+    assertThat(traceIdConsumer.traceId).isEqualTo(traceId);
+  }
+
+  @Test
+  public void newTraceDisabled() {
+    TestTraceIdConsumer traceIdConsumer = new TestTraceIdConsumer();
+    try (TraceContext traceContext = TraceContext.newTrace(false, null, traceIdConsumer)) {
+      assertForceLogging(false);
+      assertTags(ImmutableMap.of());
+    }
+    assertThat(traceIdConsumer.tagName).isNull();
+    assertThat(traceIdConsumer.traceId).isNull();
+  }
+
+  @Test
+  public void newTraceDisabledWithProvidedTraceId() {
+    TestTraceIdConsumer traceIdConsumer = new TestTraceIdConsumer();
+    try (TraceContext traceContext = TraceContext.newTrace(false, "foo", traceIdConsumer)) {
+      assertForceLogging(false);
+      assertTags(ImmutableMap.of());
+    }
+    assertThat(traceIdConsumer.tagName).isNull();
+    assertThat(traceIdConsumer.traceId).isNull();
+  }
+
+  @Test
+  public void onlyOneTraceId() {
+    TestTraceIdConsumer traceIdConsumer1 = new TestTraceIdConsumer();
+    try (TraceContext traceContext1 = TraceContext.newTrace(true, null, traceIdConsumer1)) {
+      String expectedTraceId = traceIdConsumer1.traceId;
+      assertThat(expectedTraceId).isNotNull();
+
+      TestTraceIdConsumer traceIdConsumer2 = new TestTraceIdConsumer();
+      try (TraceContext traceContext2 = TraceContext.newTrace(true, null, traceIdConsumer2)) {
+        assertForceLogging(true);
+        assertTags(
+            ImmutableMap.of(RequestId.Type.TRACE_ID.name(), ImmutableSet.of(expectedTraceId)));
+      }
+      assertThat(traceIdConsumer2.tagName).isEqualTo(RequestId.Type.TRACE_ID.name());
+      assertThat(traceIdConsumer2.traceId).isEqualTo(expectedTraceId);
+    }
+  }
+
+  @Test
+  public void multipleTraceIdsIfTraceIdProvided() {
+    String traceId1 = "foo";
+    try (TraceContext traceContext1 =
+        TraceContext.newTrace(true, traceId1, (tagName, traceId) -> {})) {
+      TestTraceIdConsumer traceIdConsumer = new TestTraceIdConsumer();
+      String traceId2 = "bar";
+      try (TraceContext traceContext2 = TraceContext.newTrace(true, traceId2, traceIdConsumer)) {
+        assertForceLogging(true);
+        assertTags(
+            ImmutableMap.of(RequestId.Type.TRACE_ID.name(), ImmutableSet.of(traceId1, traceId2)));
+      }
+      assertThat(traceIdConsumer.tagName).isEqualTo(RequestId.Type.TRACE_ID.name());
+      assertThat(traceIdConsumer.traceId).isEqualTo(traceId2);
+    }
+  }
+
   private void assertTags(ImmutableMap<String, ImmutableSet<String>> expectedTagMap) {
     SortedMap<String, SortedSet<Object>> actualTagMap =
         LoggingContext.getInstance().getTags().asMap();
@@ -168,4 +250,15 @@
     assertThat(LoggingContext.getInstance().shouldForceLogging(null, null, false))
         .isEqualTo(expected);
   }
+
+  private static class TestTraceIdConsumer implements TraceIdConsumer {
+    String tagName;
+    String traceId;
+
+    @Override
+    public void accept(String tagName, String traceId) {
+      this.tagName = tagName;
+      this.traceId = traceId;
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index c774fc5..8cd2753 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -426,7 +426,7 @@
   @Test
   public void approvalsPostSubmit() throws Exception {
     Change c = newChange();
-    RequestId submissionId = RequestId.forChange(c);
+    RequestId submissionId = submissionId(c);
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.putApproval("Code-Review", (short) 1);
     update.putApproval("Verified", (short) 1);
@@ -461,7 +461,7 @@
   @Test
   public void approvalsDuringSubmit() throws Exception {
     Change c = newChange();
-    RequestId submissionId = RequestId.forChange(c);
+    RequestId submissionId = submissionId(c);
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.putApproval("Code-Review", (short) 1);
     update.putApproval("Verified", (short) 1);
@@ -598,7 +598,7 @@
   @Test
   public void submitRecords() throws Exception {
     Change c = newChange();
-    RequestId submissionId = RequestId.forChange(c);
+    RequestId submissionId = submissionId(c);
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
 
@@ -640,7 +640,7 @@
   @Test
   public void latestSubmitRecordsOnly() throws Exception {
     Change c = newChange();
-    RequestId submissionId = RequestId.forChange(c);
+    RequestId submissionId = submissionId(c);
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
     update.merge(
@@ -941,7 +941,7 @@
     // Finish off by merging the change.
     update = newUpdate(c, changeOwner);
     update.merge(
-        RequestId.forChange(c),
+        submissionId(c),
         ImmutableList.of(
             submitRecord(
                 "NOT_READY",
@@ -3141,4 +3141,8 @@
     update.commit();
     return tr.parseBody(commit);
   }
+
+  private RequestId submissionId(Change c) {
+    return new RequestId(c.getId().toString());
+  }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 43e2602..8daf67f 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -151,7 +151,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
 
-    RequestId submissionId = RequestId.forChange(c);
+    RequestId submissionId = submissionId(c);
     update.merge(
         submissionId,
         ImmutableList.of(
@@ -220,7 +220,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setSubjectForCommit("Submit patch set 1");
 
-    RequestId submissionId = RequestId.forChange(c);
+    RequestId submissionId = submissionId(c);
     update.merge(
         submissionId, ImmutableList.of(submitRecord("RULE_ERROR", "Problem with patch set:\n1")));
     update.commit();
@@ -424,4 +424,8 @@
     RevCommit commit = parseCommit(commitId);
     assertThat(commit.getFullMessage()).isEqualTo(expected);
   }
+
+  private RequestId submissionId(Change c) {
+    return new RequestId(c.getId().toString());
+  }
 }
diff --git a/javatests/com/google/gerrit/server/rules/BUILD b/javatests/com/google/gerrit/server/rules/BUILD
index 42452df..8f4c90d 100644
--- a/javatests/com/google/gerrit/server/rules/BUILD
+++ b/javatests/com/google/gerrit/server/rules/BUILD
@@ -7,9 +7,11 @@
     resources = ["//prologtests:gerrit_common_test"],
     deps = [
         "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/project/testing:project-test-util",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
         "//lib/guice",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/prolog:runtime",
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
new file mode 100644
index 0000000..27f4423
--- /dev/null
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2018 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.rules;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import org.junit.Test;
+
+public class IgnoreSelfApprovalRuleTest {
+  private static final Change.Id CHANGE_ID = new Change.Id(100);
+  private static final PatchSet.Id PS_ID = new PatchSet.Id(CHANGE_ID, 1);
+  private static final LabelType VERIFIED = makeLabel("Verified");
+  private static final Account.Id USER1 = makeAccount(100001);
+
+  @Test
+  public void filtersByLabel() {
+    LabelType codeReview = makeLabel("Code-Review");
+    PatchSetApproval approvalVerified = makeApproval(VERIFIED.getLabelId(), USER1, 2);
+    PatchSetApproval approvalCr = makeApproval(codeReview.getLabelId(), USER1, 2);
+
+    Collection<PatchSetApproval> filteredApprovals =
+        IgnoreSelfApprovalRule.filterApprovalsByLabel(
+            ImmutableList.of(approvalVerified, approvalCr), VERIFIED);
+
+    assertThat(filteredApprovals).containsExactly(approvalVerified);
+  }
+
+  @Test
+  public void filtersVotesFromUser() {
+    PatchSetApproval approvalM2 = makeApproval(VERIFIED.getLabelId(), USER1, -2);
+    PatchSetApproval approvalM1 = makeApproval(VERIFIED.getLabelId(), USER1, -1);
+
+    ImmutableList<PatchSetApproval> approvals =
+        ImmutableList.of(
+            approvalM2,
+            approvalM1,
+            makeApproval(VERIFIED.getLabelId(), USER1, 0),
+            makeApproval(VERIFIED.getLabelId(), USER1, +1),
+            makeApproval(VERIFIED.getLabelId(), USER1, +2));
+
+    Collection<PatchSetApproval> filteredApprovals =
+        IgnoreSelfApprovalRule.filterOutPositiveApprovalsOfUser(approvals, USER1);
+
+    assertThat(filteredApprovals).containsExactly(approvalM1, approvalM2);
+  }
+
+  private static LabelType makeLabel(String labelName) {
+    List<LabelValue> values = new ArrayList<>();
+    // The label text is irrelevant here, only the numerical value is used
+    values.add(new LabelValue((short) -2, "-2"));
+    values.add(new LabelValue((short) -1, "-1"));
+    values.add(new LabelValue((short) 0, "No vote."));
+    values.add(new LabelValue((short) 1, "+1"));
+    values.add(new LabelValue((short) 2, "+2"));
+    return new LabelType(labelName, values);
+  }
+
+  private static PatchSetApproval makeApproval(LabelId labelId, Account.Id accountId, int value) {
+    PatchSetApproval.Key key = makeKey(PS_ID, accountId, labelId);
+    return new PatchSetApproval(key, (short) value, Date.from(Instant.now()));
+  }
+
+  private static PatchSetApproval.Key makeKey(
+      PatchSet.Id psId, Account.Id accountId, LabelId labelId) {
+    return new PatchSetApproval.Key(psId, accountId, labelId);
+  }
+
+  private static Account.Id makeAccount(int account) {
+    return new Account.Id(account);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
index 9256ee4..75f9307 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
@@ -586,7 +586,7 @@
     AccountGroup group = createInReviewDb("group");
 
     TestGroupBackend testGroupBackend = new TestGroupBackend();
-    backends.add(testGroupBackend);
+    backends.add("gerrit", testGroupBackend);
     AccountGroup.UUID subgroupUuid = testGroupBackend.create("test").getGroupUUID();
     assertThat(groupBackend.handles(subgroupUuid)).isTrue();
     addSubgroupsInReviewDb(group.getId(), subgroupUuid);
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
index 2b1a07e..f6b3e30 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
@@ -25,6 +25,7 @@
 import com.google.common.net.HttpHeaders;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.nio.charset.Charset;
 import java.util.Collection;
@@ -106,7 +107,7 @@
   public synchronized PrintWriter getWriter() {
     checkState(outputStream == null, "getOutputStream() already called");
     if (writer == null) {
-      writer = new PrintWriter(actualBody);
+      writer = new PrintWriter(new OutputStreamWriter(actualBody, UTF_8));
     }
     return writer;
   }
diff --git a/lib/LICENSE-Apache1.1 b/lib/LICENSE-Apache1.1
deleted file mode 100644
index 8eda4fc..0000000
--- a/lib/LICENSE-Apache1.1
+++ /dev/null
@@ -1,51 +0,0 @@
-The Apache Software License, Version 1.1
-
-Copyright (c) 2000-2002 The Apache Software Foundation.  All rights
-reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-1. Redistributions of source code must retain the above copyright
-   notice, this list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright
-   notice, this list of conditions and the following disclaimer in
-   the documentation and/or other materials provided with the
-   distribution.
-
-3. The end-user documentation included with the redistribution,
-   if any, must include the following acknowledgment:
-      "This product includes software developed by the
-       Apache Software Foundation (http://www.apache.org/)."
-   Alternately, this acknowledgment may appear in the software itself,
-   if and wherever such third-party acknowledgments normally appear.
-
-4. The names "Apache" and "Apache Software Foundation", "Jakarta-Oro"
-   must not be used to endorse or promote products derived from this
-   software without prior written permission. For written
-   permission, please contact apache@apache.org.
-
-5. Products derived from this software may not be called "Apache"
-   or "Jakarta-Oro", nor may "Apache" or "Jakarta-Oro" appear in their
-   name, without prior written permission of the Apache Software Foundation.
-
-THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
-WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
-ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
-USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
-OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGE.
-====================================================================
-
-This software consists of voluntary contributions made by many
-individuals on behalf of the Apache Software Foundation.  For more
-information on the Apache Software Foundation, please see
-<http://www.apache.org/>.
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 6ada5bd..7191901 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@
 load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_CENTRAL", "MAVEN_LOCAL", "maven_jar")
 
-_JGIT_VERS = "5.0.2.201807311906-r"
+_JGIT_VERS = "5.0.3.201809091024-r"
 
 _DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
@@ -40,28 +40,28 @@
         name = "jgit-lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "a81d7c8d153a8a744b6be1d9c6d698270beec1c0",
-        src_sha1 = "c89f8f38cebaf75d13f9b2f7a1da71206d8c38f7",
+        sha1 = "0afec2df3ff8835bc4d5c279d14fad0daae6dd93",
+        src_sha1 = "e2c978064e2a46b260bbda0d8c393ed741046420",
         unsign = True,
     )
     maven_jar(
         name = "jgit-servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "ab3d0c85bc2008da513c1127ab4acf3df8ef414e",
+        sha1 = "8fb0f9b6c38ac6fce60f2ead740e03dd79c3c288",
         unsign = True,
     )
     maven_jar(
         name = "jgit-archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "ba6e0aaf3f733f2f460e227145526e1737ca160f",
+        sha1 = "72a157ce261f3eb938d9e0ee83d7c9700aa7d736",
     )
     maven_jar(
         name = "jgit-junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "fe28963520e19c918eb26747e678ec9772ba800f",
+        sha1 = "eb430358d96dedd923e4075cd54a7db4cab51ca2",
         unsign = True,
     )
 
diff --git a/plugins/BUILD b/plugins/BUILD
index ef18e01..a7622b2 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -27,17 +27,19 @@
 ]
 
 EXPORTS = [
+    "//antlr3:query_parser",
     "//java/com/google/gerrit/common:annotations",
     "//java/com/google/gerrit/common:server",
     "//java/com/google/gerrit/extensions:api",
     "//java/com/google/gerrit/index",
     "//java/com/google/gerrit/index:query_exception",
-    "//java/com/google/gerrit/index:query_parser",
     "//java/com/google/gerrit/lifecycle",
     "//java/com/google/gerrit/metrics",
     "//java/com/google/gerrit/metrics/dropwizard",
     "//java/com/google/gerrit/reviewdb:server",
     "//java/com/google/gerrit/server/audit",
+    "//java/com/google/gerrit/server/logging",
+    "//java/com/google/gerrit/server/schema",
     "//java/com/google/gerrit/util/http",
     "//lib/commons:compress",
     "//lib/commons:dbcp",
@@ -100,13 +102,13 @@
     main_class = "Dummy",
     visibility = ["//visibility:public"],
     runtime_deps = [
+        "//antlr3:libquery_parser-src.jar",
         "//java/com/google/gerrit/common:libannotations-src.jar",
         "//java/com/google/gerrit/common:libserver-src.jar",
         "//java/com/google/gerrit/extensions:libapi-src.jar",
         "//java/com/google/gerrit/httpd:libhttpd-src.jar",
         "//java/com/google/gerrit/index:libindex-src.jar",
         "//java/com/google/gerrit/index:libquery_exception-src.jar",
-        "//java/com/google/gerrit/index:libquery_parser-src.jar",
         "//java/com/google/gerrit/pgm/init/api:libapi-src.jar",
         "//java/com/google/gerrit/reviewdb:libserver-src.jar",
         "//java/com/google/gerrit/server:libserver-src.jar",
@@ -121,9 +123,9 @@
 java_doc(
     name = "plugin-api-javadoc",
     libs = PLUGIN_API + [
+        "//antlr3:query_parser",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
-        "//java/com/google/gerrit/index:query_parser",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 4ebf98c..22342a6 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 4ebf98c77086477a5fa63e339a539b47d4e8d202
+Subproject commit 22342a6da26c75b14bc629331c339d1b820b4d39
diff --git a/plugins/hooks b/plugins/hooks
index 07672f3..cc74144 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 07672f31880ba80300b38492df9d0acfcd6ee00a
+Subproject commit cc74144db755a18c5a63764a336b93ab3d1be1fe
diff --git a/plugins/replication b/plugins/replication
index b62f006..d557ccc 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit b62f006b1350180de0af02c82fb18fb290a2548f
+Subproject commit d557ccc642c59a55750f560ce0d98870e1550d65
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index e4024e9..cc636d7 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit e4024e9d8d8139fc4c658c3af1a5e11e19b2d476
+Subproject commit cc636d7e36afb62455a9f045b125d246fd84afd0
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index e905e038..ebd72ad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -122,7 +122,7 @@
       _changeComments: Object,
       _canStartReview: {
         type: Boolean,
-        computed: '_computeCanStartReview(_loggedIn, _change, _account)',
+        computed: '_computeCanStartReview(_change)',
       },
       _comments: Object,
       /** @type {?} */
@@ -1341,9 +1341,9 @@
       });
     },
 
-    _computeCanStartReview(loggedIn, change, account) {
-      return !!(loggedIn && change.work_in_progress &&
-          change.owner._account_id === account._account_id);
+    _computeCanStartReview(change) {
+      return !!(change.actions && change.actions.ready &&
+          change.actions.ready.enabled);
     },
 
     _computeReplyDisabled() { return false; },
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index b5b8cd9..4e6cb6e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -1411,18 +1411,24 @@
       });
 
       test('canStartReview computation', () => {
-        const account1 = {_account_id: 1};
-        const account2 = {_account_id: 2};
-        const change = {
-          owner: {_account_id: 1},
+        const change1 = {};
+        const change2 = {
+          actions: {
+            ready: {
+              enabled: true,
+            },
+          },
         };
-        assert.isFalse(element._computeCanStartReview(true, change, account1));
-        change.work_in_progress = false;
-        assert.isFalse(element._computeCanStartReview(true, change, account1));
-        change.work_in_progress = true;
-        assert.isTrue(element._computeCanStartReview(true, change, account1));
-        assert.isFalse(element._computeCanStartReview(false, change, account1));
-        assert.isFalse(element._computeCanStartReview(true, change, account2));
+        const change3 = {
+          actions: {
+            ready: {
+              label: 'Ready for Review',
+            },
+          },
+        };
+        assert.isFalse(element._computeCanStartReview(change1));
+        assert.isTrue(element._computeCanStartReview(change2));
+        assert.isFalse(element._computeCanStartReview(change3));
       });
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 8ca40e7..9996abc 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -40,7 +40,7 @@
     setup(() => {
       element = fixture('basic');
       sandbox = sinon.sandbox.create();
-      sandbox.stub(Gerrit.Nav, 'overrideCommentlinks', x => x);
+      sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
     });
 
     teardown(() => { sandbox.restore(); });
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 5608dec..00157ab 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -178,34 +178,20 @@
           <gr-account-label
               account="[[author]]"
               hide-avatar></gr-account-label>
-          <template is="dom-if" if="[[_successfulParse]]">
-            <template is="dom-if" if="[[_parsedVotes.length]]">voted</template>
-            <template is="dom-repeat" items="[[_parsedVotes]]" as="score">
-              <span class$="score [[_computeScoreClass(score, labelExtremes)]]">
-                [[score.label]] [[score.value]]
-              </span>
-            </template>
-            [[_computeConversationalString(_parsedVotes, _parsedPatchNum, _parsedCommentCount)]]
+          <template is="dom-repeat" items="[[_getScores(message)]]" as="score">
+            <span class$="score [[_computeScoreClass(score, labelExtremes)]]">
+              [[score.label]] [[score.value]]
+            </span>
           </template>
         </div>
         <template is="dom-if" if="[[message.message]]">
           <div class="content">
-            <template is="dom-if" if="[[_successfulParse]]">
-              <div class="message hideOnOpen">[[_parsedChangeMessage]]</div>
-              <gr-formatted-text
-                  no-trailing-margin
-                  class="message hideOnCollapsed"
-                  content="[[_parsedChangeMessage]]"
-                  config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
-            </template>
-            <template is="dom-if" if="[[!_successfulParse]]">
-              <div class="message hideOnOpen">[[message.message]]</div>
-                <gr-formatted-text
-                    no-trailing-margin
-                    class="message hideOnCollapsed"
-                    content="[[message.message]]"
-                    config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
-            </template>
+            <div class="message hideOnOpen">[[message.message]]</div>
+            <gr-formatted-text
+                no-trailing-margin
+                class="message hideOnCollapsed"
+                content="[[message.message]]"
+                config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
             <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
               <gr-button link small on-tap="_handleReplyTap">Reply</gr-button>
             </div>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index addd660..0590c73 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -17,8 +17,7 @@
 (function() {
   'use strict';
 
-  const PATCH_SET_PREFIX_PATTERN = /^Patch Set (\d)+:[ ]?/;
-  const COMMENTS_COUNT_PATTERN = /^\((\d+)( inline)? comments?\)$/;
+  const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+: /;
   const LABEL_TITLE_SCORE_PATTERN = /^([A-Za-z0-9-]+)([+-]\d+)$/;
 
   Polymer({
@@ -102,17 +101,10 @@
         type: Boolean,
         value: false,
       },
-
-      _parsedPatchNum: String,
-      _parsedCommentCount: String,
-      _parsedVotes: Array,
-      _parsedChangeMessage: String,
-      _successfulParse: Boolean,
     },
 
     observers: [
       '_updateExpandedClass(message.expanded)',
-      '_consumeMessage(message.message)',
     ],
 
     ready() {
@@ -192,6 +184,19 @@
       return event.type === 'REVIEWER_UPDATE';
     },
 
+    _getScores(message) {
+      if (!message.message) { return []; }
+      const line = message.message.split('\n', 1)[0];
+      const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+      if (!line.match(patchSetPrefix)) { return []; }
+      const scoresRaw = line.split(patchSetPrefix)[1];
+      if (!scoresRaw) { return []; }
+      return scoresRaw.split(' ')
+          .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+          .filter(ms => ms && ms.length === 3)
+          .map(ms => ({label: ms[1], value: ms[2]}));
+    },
+
     _computeScoreClass(score, labelExtremes) {
       const classes = [];
       if (score.value > 0) {
@@ -255,73 +260,5 @@
       e.stopPropagation();
       this.set('message.expanded', !this.message.expanded);
     },
-
-    /**
-     * Attempts to consume a change message to create a shorter and more legible
-     * format. If the function encounters unexpected characters at any point, it
-     * sets the _successfulParse flag to false and terminates, causing the UI to
-     * fall back to displaying the entirety of the change message.
-     *
-     * A successful parse results in a one-liner that reads:
-     * `${AVATAR} voted ${VOTES} and left ${NUM} comment(s) on ${PATCHSET}`
-     *
-     * @param {string} text
-     */
-    _consumeMessage(text) {
-      this._parsedPatchNum = '';
-      this._parsedCommentCount = '';
-      this._parsedChangeMessage = '';
-      this._parsedVotes = [];
-      if (!text) {
-        // No message body means nothing to parse.
-        this._successfulParse = false;
-        return;
-      }
-      const lines = text.split('\n');
-      const messageLines = lines.shift().split(PATCH_SET_PREFIX_PATTERN);
-      if (!messageLines[1]) {
-        // Message is in an unexpected format.
-        this._successfulParse = false;
-        return;
-      }
-      this._parsedPatchNum = messageLines[1];
-      if (messageLines[2]) {
-        // Content after the colon is always vote information. If it is in the
-        // most up to date schema, parse it. Otherwise, cancel the parsing
-        // completely.
-        let match;
-        for (const score of messageLines[2].split(' ')) {
-          match = score.match(LABEL_TITLE_SCORE_PATTERN);
-          if (!match || match.length !== 3) {
-            this._successfulParse = false;
-            return;
-          }
-          this._parsedVotes.push({label: match[1], value: match[2]});
-        }
-      }
-      // Remove empty line.
-      lines.shift();
-      if (lines.length) {
-        const commentMatch = lines[0].match(COMMENTS_COUNT_PATTERN);
-        if (commentMatch) {
-          this._parsedCommentCount = commentMatch[1];
-          // Remove comment line and the following empty line.
-          lines.splice(0, 2);
-        }
-        this._parsedChangeMessage = lines.join('\n');
-      }
-      this._successfulParse = true;
-    },
-
-    _computeConversationalString(votes, patchNum, commentCount) {
-      let clause = ' on Patch Set ' + patchNum;
-      if (commentCount) {
-        let commentStr = ' comment';
-        if (parseInt(commentCount, 10) > 1) { commentStr += 's'; }
-        clause = ' left ' + commentCount + commentStr + clause;
-        if (votes.length) { clause = ' and' + clause; }
-      }
-      return clause;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 6bb4618..870f366 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -169,9 +169,7 @@
         author: {},
         expanded: false,
         message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Ready+1',
-        _revision_number: 1,
       };
-      element.comments = {};
       element.labelExtremes = {
         'Verified': {max: 1, min: -1},
         'Code-Review': {max: 2, min: -2},
@@ -201,145 +199,5 @@
       const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
       assert.equal(scoreChips.length, 0);
     });
-
-    suite('_consumeMessage', () => {
-      const assertConsumeFailed = str => {
-        element._consumeMessage(str);
-        assert.isFalse(element._successfulParse);
-      };
-
-      test('no message body', () => {
-        assertConsumeFailed('');
-      });
-
-      test('known old schema', () => {
-        const str = 'Patch Set 1: Looks good to me, approved; Verified';
-        assertConsumeFailed(str);
-      });
-
-      test('known old schema 2', () => {
-        const str = [
-          'Patch Set 2: Looks good to me',
-          '',
-          'Patch set 2 compiles, and runs as expected.',
-        ].join('\n');
-        assertConsumeFailed(str);
-      });
-
-      test('known old schema 3', () => {
-        const str = 'Patch Set 2: (1 inline comment)';
-        assertConsumeFailed(str);
-      });
-
-      test('known old schema 4', () => {
-        const str = [
-          'Patch Set 2: Looks good to me',
-          '',
-          '(1 inline comment)',
-        ].join('\n');
-        assertConsumeFailed(str);
-      });
-
-      test('just change message', () => {
-        const str = [
-          'Patch Set 2:',
-          '',
-          'I think you should reconsider this approach.',
-          'It really makes no sense.',
-        ].join('\n');
-
-        element._consumeMessage(str);
-
-        assert.isTrue(element._successfulParse);
-        assert.equal(element._parsedPatchNum, '2');
-        assert.equal(element._parsedCommentCount, '');
-        assert.deepEqual(element._parsedVotes, []);
-        assert.equal(element._parsedChangeMessage, [
-          'I think you should reconsider this approach.',
-          'It really makes no sense.',
-        ].join('\n'));
-      });
-
-      test('just votes', () => {
-        element._consumeMessage('Patch Set 2: Code-Review-Label+1 Verified+1');
-
-        assert.isTrue(element._successfulParse);
-        assert.equal(element._parsedPatchNum, '2');
-        assert.equal(element._parsedCommentCount, '');
-        assert.deepEqual(element._parsedVotes, [
-          {label: 'Code-Review-Label', value: '+1'},
-          {label: 'Verified', value: '+1'},
-        ]);
-        assert.equal(element._parsedChangeMessage, '');
-      });
-
-      test('just comments', () => {
-        const str = [
-          'Patch Set 2:',
-          '',
-          '(8 comments)',
-        ].join('\n');
-
-        element._consumeMessage(str);
-
-        assert.isTrue(element._successfulParse);
-        assert.equal(element._parsedPatchNum, '2');
-        assert.equal(element._parsedCommentCount, '8');
-        assert.deepEqual(element._parsedVotes, []);
-        assert.equal(element._parsedChangeMessage, '');
-      });
-
-      test('vote with comments and change message', () => {
-        const str = [
-          'Patch Set 2: Code-Review-Label+1 Verified+1',
-          '',
-          '(1 comment)',
-          '',
-          'LGTM',
-          '',
-          'Nice work, just one nit.',
-        ].join('\n');
-
-        element._consumeMessage(str);
-
-        assert.isTrue(element._successfulParse);
-        assert.equal(element._parsedPatchNum, '2');
-        assert.equal(element._parsedCommentCount, '1');
-        assert.deepEqual(element._parsedVotes, [
-          {label: 'Code-Review-Label', value: '+1'},
-          {label: 'Verified', value: '+1'},
-        ]);
-        assert.equal(element._parsedChangeMessage, [
-          'LGTM',
-          '',
-          'Nice work, just one nit.',
-        ].join('\n'));
-      });
-
-      test('vote with change message', () => {
-        const str = [
-          'Patch Set 2: Code-Review-Label+2 Verified+1',
-          '',
-          'LGTM',
-          '',
-          'Nice work.',
-        ].join('\n');
-
-        element._consumeMessage(str);
-
-        assert.isTrue(element._successfulParse);
-        assert.equal(element._parsedPatchNum, '2');
-        assert.equal(element._parsedCommentCount, '');
-        assert.deepEqual(element._parsedVotes, [
-          {label: 'Code-Review-Label', value: '+2'},
-          {label: 'Verified', value: '+1'},
-        ]);
-        assert.equal(element._parsedChangeMessage, [
-          'LGTM',
-          '',
-          'Nice work.',
-        ].join('\n'));
-      });
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index aa04194..c5e32cc 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -138,7 +138,7 @@
       _generateWeblinks: uninitialized,
 
       /** @type {Function} */
-      overrideCommentlinks: uninitialized,
+      mapCommentlinks: uninitialized,
 
       /**
        * @param {number=} patchNum
@@ -152,23 +152,38 @@
 
       /**
        * Setup router implementation.
-       * @param {Function} navigate
-       * @param {Function} generateUrl
-       * @param {Function} generateWeblinks
-       * @param {Function} overrideCommentlinks
+       * @param {function(!string)} navigate the router-abstracted equivalent of
+       *     `window.location.href = ...`. Takes a string.
+       * @param {function(!Object): string} generateUrl generates a URL given
+       *     navigation parameters, detailed in the file header.
+       * @param {function(!Object): string} generateWeblinks weblinks generator
+       *     function takes single payload parameter with type property that
+       *  determines which
+       *     part of the UI is the consumer of the weblinks. type property can
+       *     be one of file, change, or patchset.
+       *     - For file type, payload will also contain string properties: repo,
+       *         commit, file.
+       *     - For patchset type, payload will also contain string properties:
+       *         repo, commit.
+       *     - For change type, payload will also contain string properties:
+       *         repo, commit. If server provides weblinks, those will be passed
+       *         as options.weblinks property on the main payload object.
+       * @param {function(!Object): Object} mapCommentlinks provides an escape
+       *     hatch to modify the commentlinks object, e.g. if it contains any
+       *     relative URLs.
        */
-      setup(navigate, generateUrl, generateWeblinks, overrideCommentlinks) {
+      setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
         this._navigate = navigate;
         this._generateUrl = generateUrl;
         this._generateWeblinks = generateWeblinks;
-        this.overrideCommentlinks = overrideCommentlinks;
+        this.mapCommentlinks = mapCommentlinks;
       },
 
       destroy() {
         this._navigate = uninitialized;
         this._generateUrl = uninitialized;
         this._generateWeblinks = uninitialized;
-        this.overrideCommentlinks = uninitialized;
+        this.mapCommentlinks = uninitialized;
       },
 
       /**
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 4f0a73b..9f1b412 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -96,7 +96,10 @@
         reflectToAttribute: true,
       },
       noRenderOnPrefsChange: Boolean,
-      comments: Object,
+      comments: {
+        type: Object,
+        value: {left: [], right: []},
+      },
       lineWrapping: {
         type: Boolean,
         value: false,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 091cb75..530da02 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -60,7 +60,7 @@
      *     commentLink patterns
      */
     _contentOrConfigChanged(content, config) {
-      config = Gerrit.Nav.overrideCommentlinks(config);
+      config = Gerrit.Nav.mapCommentlinks(config);
       const output = Polymer.dom(this.$.output);
       output.textContent = '';
       const parser = new GrLinkTextParser(config,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index fc76da5..23c1442 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -44,7 +44,7 @@
     setup(() => {
       element = fixture('basic');
       sandbox = sinon.sandbox.create();
-      sandbox.stub(Gerrit.Nav, 'overrideCommentlinks', x => x);
+      sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
       element.config = {
         ph: {
           match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 84c1803..af10b8371 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1498,11 +1498,28 @@
      */
     getRepos(filter, reposPerPage, opt_offset) {
       const defaultFilter = 'state:active OR state:read-only';
+      const namePartDelimiters = /[@.\-\s\/_]/g;
       const offset = opt_offset || 0;
 
+      if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
+        // The query language specifies hyphens as operators. Split the string
+        // by hyphens and 'AND' the parts together as 'inname:' queries.
+        // If the filter includes a semicolon, the user is using a more complex
+        // query so we trust them and don't do any magic under the hood.
+        const originalFilter = filter;
+        filter = '';
+        originalFilter.split(namePartDelimiters).forEach(part => {
+          if (part) {
+            filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
+          }
+        });
+      }
+      // Check if filter is now empty which could be either because the user did
+      // not provide it or because the user provided only a split character.
       if (!filter) {
         filter = defaultFilter;
       }
+
       filter = filter.trim();
       const encodedFilter = encodeURIComponent(filter);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index b7466ef..d9656e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -951,16 +951,46 @@
             '/projects/?n=26&S=25&query=test');
       });
 
-      test('with filter', () => {
-        element.getRepos('test/test/test', 25);
+      test('with blank', () => {
+        element.getRepos('test/test', 25);
         assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
-            '/projects/?n=26&S=0&query=test%2Ftest%2Ftest');
+            '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
       });
 
-      test('with regex filter', () => {
-        element.getRepos('^test.*', 25);
+      test('with hyphen', () => {
+        element.getRepos('foo-bar', 25);
         assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
-            '/projects/?n=26&S=0&query=%5Etest.*');
+            '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+      });
+
+      test('with leading hyphen', () => {
+        element.getRepos('-bar', 25);
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+            '/projects/?n=26&S=0&query=inname%3Abar');
+      });
+
+      test('with trailing hyphen', () => {
+        element.getRepos('foo-bar-', 25);
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+            '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+      });
+
+      test('with underscore', () => {
+        element.getRepos('foo_bar', 25);
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+            '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+      });
+
+      test('with underscore', () => {
+        element.getRepos('foo_bar', 25);
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+            '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+      });
+
+      test('hyphen only', () => {
+        element.getRepos('-', 25);
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
+            `/projects/?n=26&S=0&query=${defaultQuery}`);
       });
     });
 
diff --git a/tools/BUILD b/tools/BUILD
index 53f441a..73ecfb9 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -68,7 +68,7 @@
         "-Xep:CannotMockFinalClass:WARN",
         "-Xep:ClassCanBeStatic:WARN",
         "-Xep:ClassNewInstance:WARN",
-        "-Xep:DefaultCharset:WARN",
+        "-Xep:DefaultCharset:ERROR",
         "-Xep:DoubleCheckedLocking:WARN",
         "-Xep:ElementsCountedInLoop:WARN",
         "-Xep:EqualsHashCode:WARN",
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index 2adb7dd..b185214 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -15,7 +15,7 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
 load("//tools/bzl:java.bzl", "java_library2")
 
-jar_filetype = FileType([".jar"])
+jar_filetype = [".jar"]
 
 BROWSERS = [
     "chrome",
@@ -225,7 +225,7 @@
             default = Label("@bazel_tools//tools/zip:zipper"),
             cfg = "host",
             executable = True,
-            single_file = True,
+            allow_single_file = True,
         ),
     },
     outputs = {
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index d6d0c95..0997bcb 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -426,7 +426,7 @@
 
 def bundle_assets(*args, **kwargs):
     """Combine html, js, css files and optionally split into js and html bundles."""
-    _bundle_rule(*args, pkg = PACKAGE_NAME, **kwargs)
+    _bundle_rule(*args, pkg = native.package_name(), **kwargs)
 
 def polygerrit_plugin(name, app, srcs = [], assets = None, **kwargs):
     """Bundles plugin dependencies for deployment.
@@ -447,7 +447,7 @@
         name = name + "_combined",
         app = app,
         srcs = srcs if app in srcs else srcs + [app],
-        pkg = PACKAGE_NAME,
+        pkg = native.package_name(),
         **kwargs
     )
 
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index 476ccb9..ebe57f2 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -35,7 +35,7 @@
             continue
 
         handled_rules.append(rule_name)
-        for c in child.getchildren():
+        for c in list(child):
             if c.tag != "rule-input":
                 continue
 
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index f011446..d059216 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -39,7 +39,7 @@
     if target[0] not in ":/":
         target = ":" + target
     if target[0] != "/":
-        target = "//" + PACKAGE_NAME + target
+        target = "//" + native.package_name() + target
 
     forbidden = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
     native.genquery(
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 1a376e9..40dd769 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -14,7 +14,7 @@
 
 # War packaging.
 
-jar_filetype = FileType([".jar"])
+jar_filetype = [".jar"]
 
 LIBS = [
     "//java/com/google/gerrit/common:version",
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index b99c04e..64d837a 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -52,10 +52,12 @@
                 action='store', default='gerrit', dest='project_name')
 opts.add_option('-b', '--batch', action='store_true',
                 dest='batch', help='Bazel batch option')
+opts.add_option('-j', '--java', action='store',
+                dest='java', help='Post Java 8 support (9|10|11|...)')
 args, _ = opts.parse_args()
 
 batch_option = '--batch' if args.batch else None
-
+custom_java = args.java
 
 def _build_bazel_cmd(*args):
     cmd = ['bazel']
@@ -63,6 +65,9 @@
         cmd.append('--batch')
     for arg in args:
         cmd.append(arg)
+    if custom_java:
+        cmd.append('--host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java%s' % custom_java)
+        cmd.append('--java_toolchain=@bazel_tools//tools/jdk:toolchain_java%s' % custom_java)
     return cmd
 
 
@@ -70,9 +75,10 @@
     return check_output(_build_bazel_cmd('info', 'output_base')).strip()
 
 
-def gen_bazel_path():
+def gen_bazel_path(ext_location):
     bazel = check_output(['which', 'bazel']).strip().decode('UTF-8')
     with open(path.join(ROOT, ".bazel_path"), 'w') as fd:
+        fd.write("output_base=%s\n" % ext_location)
         fd.write("bazel=%s\n" % bazel)
         fd.write("PATH=%s\n" % environ["PATH"])
 
@@ -301,7 +307,7 @@
     gen_project(args.project_name)
     gen_classpath(ext_location)
     gen_factorypath(ext_location)
-    gen_bazel_path()
+    gen_bazel_path(ext_location)
 
     # TODO(davido): Remove this when GWT gone
     gwt_working_dir = ".gwt_work_dir"