Merge "Add support for tracing the execution time of operations"
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/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.txt b/Documentation/rest-api.txt
index 97cca38..8f6a47b 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -210,6 +210,15 @@
   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 the trace ID. The trace ID is returned with
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index ac09d3c..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
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/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/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/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/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/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/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/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/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/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index df3560a..6b3a49b 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -168,7 +168,7 @@
 
   @Override
   public Iterator<T> iterator() {
-    Iterator<Entry<T>> entryIterator = entryIterator();
+    Iterator<Entry<T>> entryIterator = entries().iterator();
     return new Iterator<T>() {
       @Override
       public boolean hasNext() {
@@ -183,39 +183,44 @@
     };
   }
 
-  public Iterator<Entry<T>> entryIterator() {
+  public Iterable<Entry<T>> entries() {
     final Iterator<AtomicReference<NamedProvider<T>>> itr = items.iterator();
-    return new Iterator<Entry<T>>() {
-      private Entry<T> next;
-
+    return new Iterable<Entry<T>>() {
       @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.
+      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;
           }
-        }
-        return next != null;
-      }
 
-      @Override
-      public Entry<T> next() {
-        if (hasNext()) {
-          Entry<T> result = next;
-          next = null;
-          return result;
-        }
-        throw new NoSuchElementException();
-      }
+          @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();
+          @Override
+          public void remove() {
+            throw new UnsupportedOperationException();
+          }
+        };
       }
     };
   }
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/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 38fe0e2..10f2638 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -110,6 +110,7 @@
 import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.LockFailureException;
+import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -1322,11 +1323,42 @@
   }
 
   private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
-    String traceValue = req.getParameter(ParameterParser.TRACE_PARAMETER);
-    return TraceContext.newTrace(
-        traceValue != null,
-        traceValue,
-        (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId.toString()));
+    // 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;
+    }
+
+    // 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) {
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/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/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/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/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/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/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/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/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/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/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/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 6107ecd..438438c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -582,7 +582,12 @@
             tracePushOption.isPresent(),
             tracePushOption.orElse(null),
             (tagName, traceId) -> addMessage(tagName + ": " + traceId))) {
-      traceContext.addTag(RequestId.Type.RECEIVE_ID, RequestId.forProject(project.getNameKey()));
+      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());
+
       try {
         if (!projectState.getProject().getState().permitsWrite()) {
           for (ReceiveCommand cmd : commands) {
@@ -1805,7 +1810,7 @@
       return;
     }
 
-    if (validateConnected(magicBranch.dest, tip)) {
+    if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
       this.magicBranch = magicBranch;
     }
   }
@@ -1814,7 +1819,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());
@@ -1837,7 +1842,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 {
@@ -1845,7 +1850,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;
     }
@@ -1906,13 +1911,15 @@
     }
 
     try {
+      NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, receivePack.getRevWalk());
       if (validCommit(
           receivePack.getRevWalk().getObjectReader(),
           changeEnt.getDest(),
           cmd,
           newCommit,
           false,
-          changeEnt)) {
+          changeEnt,
+          rejectCommits)) {
         logger.atFine().log("Replacing change %s", changeEnt.getId());
         requestReplace(cmd, true, changeEnt, newCommit);
       }
@@ -1961,6 +1968,10 @@
     }
   }
 
+  private NoteMap loadRejectCommits() throws IOException {
+    return BanCommit.loadRejectCommitsMap(repo, receivePack.getRevWalk());
+  }
+
   private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress) {
     logger.atFine().log("Finding new and replaced changes");
     List<CreateRequest> newChanges = new ArrayList<>();
@@ -2074,7 +2085,8 @@
             magicBranch.cmd,
             c,
             magicBranch.merged,
-            null)) {
+            null,
+            loadRejectCommits())) {
           // Not a change the user can propose? Abort as early as possible.
           logger.atFine().log("Aborting early due to invalid commit");
           return Collections.emptyList();
@@ -3038,6 +3050,8 @@
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
+      NoteMap rejectCommits = loadRejectCommits();
+
       RevObject parsedObject = walk.parseAny(cmd.getNewId());
       if (!(parsedObject instanceof RevCommit)) {
         return;
@@ -3060,7 +3074,7 @@
           continue;
         }
 
-        if (!validCommit(walk.getObjectReader(), branch, cmd, c, false, null)) {
+        if (!validCommit(walk.getObjectReader(), branch, cmd, c, false, null, rejectCommits)) {
           break;
         }
 
@@ -3097,9 +3111,9 @@
       ReceiveCommand cmd,
       RevCommit commit,
       boolean isMerged,
-      @Nullable Change change)
+      @Nullable Change change,
+      NoteMap rejectCommits)
       throws IOException {
-    PermissionBackend.ForRef perm = permissions.ref(branch.get());
 
     ValidCommitKey key = new AutoValue_ReceiveCommits_ValidCommitKey(commit.copy(), branch);
     if (validCommits.contains(key)) {
@@ -3108,18 +3122,21 @@
 
     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);
+      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(
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 3e382fb..bcb910a 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;
@@ -123,15 +120,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 +154,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 +185,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 +200,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/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..17e152e3
--- /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 3e7bab4..977baa5 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -47,8 +47,9 @@
  * </pre>
  *
  * <p>The logging tags and the force logging flag are stored in the {@link LoggingContext}. {@link
- * LoggingContextAwareThreadFactory} ensures that the logging context is automatically copied to
- * background threads.
+ * 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.
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..c48a2f8 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 {
@@ -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());
   }
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/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/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/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/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/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/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/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/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/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/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/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/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/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b362b2b..7126354 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -526,9 +526,9 @@
 
   @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(
@@ -554,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();
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index fe55115..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;
@@ -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
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 8f1bdbc..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));
   }
 
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/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/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 b48d47e..137dc21 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -18,7 +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;
@@ -27,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;
@@ -71,29 +82,90 @@
     assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
     assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
     assertThat(projectCreationListener.traceId).isNull();
-    assertThat(projectCreationListener.foundTraceId).isFalse();
     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.traceId).isNotNull();
-    assertThat(projectCreationListener.foundTraceId).isTrue();
     assertThat(projectCreationListener.isLoggingForced).isTrue();
   }
 
   @Test
-  public void restCallWithTraceAndProvidedTraceId() throws Exception {
+  public void restCallWithTraceRequestParamAndProvidedTraceId() throws Exception {
     RestResponse response =
         adminRestSession.put("/projects/new3?" + ParameterParser.TRACE_PARAMETER + "=issue/123");
     assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
-    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
+    assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isEqualTo("issue/123");
     assertThat(projectCreationListener.traceId).isEqualTo("issue/123");
-    assertThat(projectCreationListener.foundTraceId).isTrue();
+    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.traceId).isNotNull();
+    assertThat(projectCreationListener.isLoggingForced).isTrue();
+  }
+
+  @Test
+  public void restCallWithTraceHeaderAndProvidedTraceId() throws Exception {
+    RestResponse response =
+        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)).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();
   }
 
@@ -103,7 +175,6 @@
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
-    assertThat(commitValidationListener.foundTraceId).isFalse();
     assertThat(commitValidationListener.isLoggingForced).isFalse();
   }
 
@@ -114,7 +185,6 @@
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNotNull();
-    assertThat(commitValidationListener.foundTraceId).isTrue();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
@@ -125,7 +195,6 @@
     PushOneCommit.Result r = push.to("refs/heads/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
-    assertThat(commitValidationListener.foundTraceId).isTrue();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
@@ -135,7 +204,6 @@
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNull();
-    assertThat(commitValidationListener.foundTraceId).isFalse();
     assertThat(commitValidationListener.isLoggingForced).isFalse();
   }
 
@@ -146,7 +214,6 @@
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isNotNull();
-    assertThat(commitValidationListener.foundTraceId).isTrue();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
@@ -157,28 +224,67 @@
     PushOneCommit.Result r = push.to("refs/for/master");
     r.assertOkStatus();
     assertThat(commitValidationListener.traceId).isEqualTo("issue/123");
-    assertThat(commitValidationListener.foundTraceId).isTrue();
     assertThat(commitValidationListener.isLoggingForced).isTrue();
   }
 
+  @Test
+  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 {
     String traceId;
-    Boolean foundTraceId;
+    ImmutableSet<String> traceIds;
     Boolean isLoggingForced;
 
     @Override
     public void validateNewProject(CreateProjectArgs args) throws ValidationException {
       this.traceId =
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
-      this.foundTraceId = traceId != null;
+      this.traceIds = LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID");
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
     }
   }
 
   private static class TraceValidatingCommitValidationListener implements CommitValidationListener {
     String traceId;
-    Boolean foundTraceId;
     Boolean isLoggingForced;
 
     @Override
@@ -186,7 +292,6 @@
         throws CommitValidationException {
       this.traceId =
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
-      this.foundTraceId = traceId != 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/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/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/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/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/extensions/registration/DynamicSetTest.java b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
index bbcee2d..c86160f 100644
--- a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
+++ b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -123,7 +123,7 @@
     ds.add("bar", 2);
     ds.add("bar", 3);
 
-    Iterator<DynamicSet.Entry<Integer>> entryIterator = ds.entryIterator();
+    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);
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/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/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/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/plugins/BUILD b/plugins/BUILD
index b7364be..a7622b2 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -38,6 +38,8 @@
     "//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",
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/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/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"