Merge changes I8ab306e8,Ib1abbb96

* changes:
  Add AutoValue classes for Preferences
  Rename Preferences to StoredPreferences
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 1a4a75d..158e528 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -43,8 +43,8 @@
   $ bazel build \
     --define=ABSOLUTE_JAVABASE=<path-to-java-12> \
     --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \
-    --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
-    --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
+    --host_java_toolchain=//tools:toolchain_vanilla \
+    --java_toolchain=//tools:toolchain_vanilla \
     :release
 ```
 
@@ -56,8 +56,8 @@
     --define=ABSOLUTE_JAVABASE=<path-to-java-12> \
     --javabase=@bazel_tools//tools/jdk:absolute_javabase \
     --host_javabase=@bazel_tools//tools/jdk:absolute_javabase \
-    --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
-    --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla \
+    --host_java_toolchain=//tools:toolchain_vanilla \
+    --java_toolchain=//tools:toolchain_vanilla \
     //...
 ```
 
@@ -69,8 +69,8 @@
 > build --define=ABSOLUTE_JAVABASE=<path-to-java-12>
 > build --javabase=@bazel_tools//tools/jdk:absolute_javabase
 > build --host_javabase=@bazel_tools//tools/jdk:absolute_javabase
-> build --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla
-> build --java_toolchain=@bazel_tools//tools/jdk:toolchain_vanilla
+> build --host_java_toolchain=//tools:toolchain_vanilla
+> build --java_toolchain=//tools:toolchain_vanilla
 > EOF
 ```
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 77ef60d..74ed725 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2738,6 +2738,20 @@
 }
 ----
 
+[[exception-hook]]
+== ExceptionHook
+
+An `ExceptionHook` allows implementors to control how certain
+exceptions should be handled.
+
+This interface is intended to be implemented for multi-master setups to
+control the behavior for handling exceptions that are thrown by a lower
+layer that handles the consensus and synchronization between different
+server nodes. E.g. if an operation fails because consensus for a Git
+update could not be achieved (e.g. due to slow responding server nodes)
+this interface can be used to retry the request instead of failing it
+immediately.
+
 [[quota-enforcer]]
 == Quota Enforcer
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 3eb259b..f03507a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6019,7 +6019,9 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name         ||Description
-|`message`          ||Commit message for the cherry-pick change
+|`message`          |optional|
+Commit message for the cherry-pick change. If not set, the commit message of
+the cherry-picked commit is used.
 |`destination`      ||Destination branch
 |`base`             |optional|
 40-hex digit SHA-1 of the commit which will be the parent commit of the newly created change.
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index 1b9a6e5..d422dd9 100644
--- a/Documentation/user-request-tracing.txt
+++ b/Documentation/user-request-tracing.txt
@@ -72,7 +72,8 @@
 exception as recoverable.
 
 The trace IDs for auto-retries are generated and start with
-`retry-on-failure-`.
+`retry-on-failure-`. For REST requests they are returned to the client
+as `X-Gerrit-Trace` header.
 
 The best way to search for auto-retries in logs is to do a grep by
 `AutoRetry`. For each auto-retry that happened this should match 1 or 2
diff --git a/WORKSPACE b/WORKSPACE
index a21fe31..75a7eed 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -56,29 +56,6 @@
 
 check_bazel_version()
 
-# Protobuf rules support
-http_archive(
-    name = "rules_proto",
-    sha256 = "602e7161d9195e50246177e7c55b2f39950a9cf7366f74ed5f22fd45750cd208",
-    strip_prefix = "rules_proto-97d8af4dc474595af3900dd85cb3a29ad28cc313",
-    urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_proto/archive/97d8af4dc474595af3900dd85cb3a29ad28cc313.tar.gz",
-        "https://github.com/bazelbuild/rules_proto/archive/97d8af4dc474595af3900dd85cb3a29ad28cc313.tar.gz",
-    ],
-)
-
-# Rules Python
-http_archive(
-    name = "rules_python",
-    sha256 = "b5bab4c47e863e0fbb77df4a40c45ca85f98f5a2826939181585644c9f31b97b",
-    strip_prefix = "rules_python-9d68f24659e8ce8b736590ba1e4418af06ec2552",
-    urls = ["https://github.com/bazelbuild/rules_python/archive/9d68f24659e8ce8b736590ba1e4418af06ec2552.tar.gz"],
-)
-
-load("@rules_python//python:repositories.bzl", "py_repositories")
-
-py_repositories()
-
 load("@io_bazel_rules_closure//closure:repositories.bzl", "closure_repositories")
 
 # Prevent redundant loading of dependencies.
@@ -118,17 +95,6 @@
 
 gazelle_dependencies()
 
-# Protobuf rules support
-http_archive(
-    name = "rules_proto",
-    sha256 = "602e7161d9195e50246177e7c55b2f39950a9cf7366f74ed5f22fd45750cd208",
-    strip_prefix = "rules_proto-97d8af4dc474595af3900dd85cb3a29ad28cc313",
-    urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_proto/archive/97d8af4dc474595af3900dd85cb3a29ad28cc313.tar.gz",
-        "https://github.com/bazelbuild/rules_proto/archive/97d8af4dc474595af3900dd85cb3a29ad28cc313.tar.gz",
-    ],
-)
-
 # Dependencies for PolyGerrit local dev server.
 go_repository(
     name = "com_github_howeyc_fsnotify",
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index fa9299a..97394c7 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -128,6 +128,7 @@
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gerrit.testing.SshMode;
+import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -143,6 +144,8 @@
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -285,6 +288,7 @@
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
+  private String systemTimeZone;
 
   @Before
   public void clearSender() {
@@ -435,6 +439,37 @@
     if (!classDesc.skipProjectClone()) {
       testRepo = cloneProject(project, getCloneAsAccount(description));
     }
+
+    // Set the clock step last, so that the test setup isn't consuming any timestamps after the
+    // clock has been set.
+    setTimeSettings(classDesc.useSystemTime(), classDesc.useClockStep(), classDesc.useTimezone());
+    setTimeSettings(
+        methodDesc.useSystemTime(), methodDesc.useClockStep(), methodDesc.useTimezone());
+  }
+
+  private void setTimeSettings(
+      boolean useSystemTime,
+      @Nullable UseClockStep useClockStep,
+      @Nullable UseTimezone useTimezone) {
+    if (useSystemTime) {
+      TestTimeUtil.useSystemTime();
+    } else if (useClockStep != null) {
+      TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
+      if (useClockStep.startAtEpoch()) {
+        TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+      }
+    }
+    if (useTimezone != null) {
+      systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
+    }
+  }
+
+  private void resetTimeSettings() {
+    TestTimeUtil.useSystemTime();
+    if (systemTimeZone != null) {
+      System.setProperty("user.timezone", systemTimeZone);
+      systemTimeZone = null;
+    }
   }
 
   /** Override to bind an additional Guice module */
@@ -562,6 +597,7 @@
       repo.close();
     }
     closeSsh();
+    resetTimeSettings();
     if (server != commonServer) {
       server.close();
       server = null;
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 4279764..adc1f1d 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -110,6 +110,9 @@
           has(Sandboxed.class, testDesc.getTestClass()),
           has(SkipProjectClone.class, testDesc.getTestClass()),
           has(UseSsh.class, testDesc.getTestClass()),
+          false, // @UseSystemTime is only valid on methods.
+          get(UseClockStep.class, testDesc.getTestClass()),
+          get(UseTimezone.class, testDesc.getTestClass()),
           null, // @GerritConfig is only valid on methods.
           null, // @GerritConfigs is only valid on methods.
           null, // @GlobalPluginConfig is only valid on methods.
@@ -119,6 +122,15 @@
 
     public static Description forTestMethod(
         org.junit.runner.Description testDesc, String configName) {
+      UseClockStep useClockStep = testDesc.getAnnotation(UseClockStep.class);
+      if (testDesc.getAnnotation(UseSystemTime.class) == null && useClockStep == null) {
+        // Only read the UseClockStep from the class if on method level neither @UseSystemTime nor
+        // @UseClockStep have been used.
+        // If the method defines @UseSystemTime or @UseClockStep it should overwrite @UseClockStep
+        // on class level.
+        useClockStep = get(UseClockStep.class, testDesc.getTestClass());
+      }
+
       return new AutoValue_GerritServer_Description(
           testDesc,
           configName,
@@ -133,6 +145,11 @@
               || has(SkipProjectClone.class, testDesc.getTestClass()),
           testDesc.getAnnotation(UseSsh.class) != null
               || has(UseSsh.class, testDesc.getTestClass()),
+          testDesc.getAnnotation(UseSystemTime.class) != null,
+          useClockStep,
+          testDesc.getAnnotation(UseTimezone.class) != null
+              ? testDesc.getAnnotation(UseTimezone.class)
+              : get(UseTimezone.class, testDesc.getTestClass()),
           testDesc.getAnnotation(GerritConfig.class),
           testDesc.getAnnotation(GerritConfigs.class),
           testDesc.getAnnotation(GlobalPluginConfig.class),
@@ -149,6 +166,16 @@
       return false;
     }
 
+    @Nullable
+    private static <T extends Annotation> T get(Class<T> annotation, Class<?> clazz) {
+      for (; clazz != null; clazz = clazz.getSuperclass()) {
+        if (clazz.getAnnotation(annotation) != null) {
+          return clazz.getAnnotation(annotation);
+        }
+      }
+      return null;
+    }
+
     private static Level getLogLevelThresholdAnnotation(org.junit.runner.Description testDesc) {
       LogThreshold logLevelThreshold = testDesc.getTestClass().getAnnotation(LogThreshold.class);
       if (logLevelThreshold == null) {
@@ -176,6 +203,14 @@
       return useSshAnnotation() && SshMode.useSsh();
     }
 
+    abstract boolean useSystemTime();
+
+    @Nullable
+    abstract UseClockStep useClockStep();
+
+    @Nullable
+    abstract UseTimezone useTimezone();
+
     @Nullable
     abstract GerritConfig config();
 
@@ -191,12 +226,15 @@
     abstract Level logLevelThreshold();
 
     private void checkValidAnnotations() {
+      if (useClockStep() != null && useSystemTime()) {
+        throw new IllegalStateException("Use either @UseClockStep or @UseSystemTime, not both");
+      }
       if (configs() != null && config() != null) {
-        throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig not both");
+        throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig, not both");
       }
       if (pluginConfigs() != null && pluginConfig() != null) {
         throw new IllegalStateException(
-            "Use either @GlobalPluginConfig or @GlobalPluginConfigs not both");
+            "Use either @GlobalPluginConfig or @GlobalPluginConfigs, not both");
       }
       if ((pluginConfigs() != null || pluginConfig() != null) && memory()) {
         throw new IllegalStateException("Must use @UseLocalDisk with @GlobalPluginConfig(s)");
@@ -412,7 +450,7 @@
       @Nullable InMemoryRepositoryManager inMemoryRepoManager)
       throws Exception {
     Config cfg = desc.buildConfig(baseConfig);
-    daemon.setSlave(isSlave(baseConfig) || cfg.getBoolean("container", "slave", false));
+    daemon.setSlave(isSlave(baseConfig) || isSlave(cfg));
     mergeTestConfig(cfg);
     // Set the log4j configuration to an invalid one to prevent system logs
     // from getting configured and creating log files.
diff --git a/java/com/google/gerrit/acceptance/UseClockStep.java b/java/com/google/gerrit/acceptance/UseClockStep.java
new file mode 100644
index 0000000..10a93fe
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/UseClockStep.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Annotation to use a clock step for the execution of acceptance tests (the test class must inherit
+ * from {@link AbstractDaemonTest}).
+ *
+ * <p>Annotations on method level override annotations on class level.
+ */
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface UseClockStep {
+  /** Amount to increment clock by on each lookup. */
+  long clockStep() default 1L;
+
+  /** Time unit for {@link #clockStep()}. */
+  TimeUnit clockStepUnit() default TimeUnit.SECONDS;
+
+  /** Whether the clock should initially be set to {@link java.time.Instant#EPOCH}. */
+  boolean startAtEpoch() default false;
+}
diff --git a/java/com/google/gerrit/acceptance/UseSystemTime.java b/java/com/google/gerrit/acceptance/UseSystemTime.java
new file mode 100644
index 0000000..e9cbd47
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/UseSystemTime.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to use the system time for the execution of acceptance tests (the test class must
+ * inherit from {@link AbstractDaemonTest}).
+ *
+ * <p>Can only be applied on method level, since using system time on class level is the default if
+ * {@link UseClockStep} is not used.
+ *
+ * <p>Intended to be used to use system time for single tests when the test class is annotated with
+ * {@link UseClockStep}.
+ */
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface UseSystemTime {}
diff --git a/java/com/google/gerrit/acceptance/UseTimezone.java b/java/com/google/gerrit/acceptance/UseTimezone.java
new file mode 100644
index 0000000..7412030
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/UseTimezone.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to set a timezone for the execution of acceptance tests (the test class must inherit
+ * from {@link AbstractDaemonTest}).
+ *
+ * <p>Annotations on method level override annotations on class level.
+ */
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface UseTimezone {
+  /** The timezone that should be used for the test, e.g. "US/Eastern". */
+  String timezone();
+}
diff --git a/java/com/google/gerrit/git/BUILD b/java/com/google/gerrit/git/BUILD
index 5ece37a..1edba38 100644
--- a/java/com/google/gerrit/git/BUILD
+++ b/java/com/google/gerrit/git/BUILD
@@ -7,6 +7,8 @@
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
         "//lib/jgit/org.eclipse.jgit:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/git/GitUpdateFailureException.java b/java/com/google/gerrit/git/GitUpdateFailureException.java
new file mode 100644
index 0000000..76ef217
--- /dev/null
+++ b/java/com/google/gerrit/git/GitUpdateFailureException.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.git;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Thrown when updating a ref in Git fails. */
+public class GitUpdateFailureException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  private final ImmutableList<GitUpdateFailure> failures;
+
+  public GitUpdateFailureException(String message, RefUpdate refUpdate) {
+    super(message);
+    this.failures = ImmutableList.of(GitUpdateFailure.create(refUpdate));
+  }
+
+  public GitUpdateFailureException(String message, BatchRefUpdate batchRefUpdate) {
+    super(message);
+    this.failures =
+        batchRefUpdate.getCommands().stream()
+            .filter(c -> c.getResult() != ReceiveCommand.Result.OK)
+            .map(GitUpdateFailure::create)
+            .collect(toImmutableList());
+  }
+
+  /** @return the names of the refs for which the update failed. */
+  public ImmutableList<String> getFailedRefs() {
+    return failures.stream().map(GitUpdateFailure::ref).collect(toImmutableList());
+  }
+
+  /** @return the failures that caused this exception. */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public ImmutableList<GitUpdateFailure> getFailures() {
+    return failures;
+  }
+
+  @AutoValue
+  public abstract static class GitUpdateFailure {
+    private static GitUpdateFailure create(RefUpdate refUpdate) {
+      return builder().ref(refUpdate.getName()).result(refUpdate.getResult().name()).build();
+    }
+
+    private static GitUpdateFailure create(ReceiveCommand receiveCommand) {
+      return builder()
+          .ref(receiveCommand.getRefName())
+          .result(receiveCommand.getResult().name())
+          .message(receiveCommand.getMessage())
+          .build();
+    }
+
+    public abstract String ref();
+
+    public abstract String result();
+
+    public abstract Optional<String> message();
+
+    public static GitUpdateFailure.Builder builder() {
+      return new AutoValue_GitUpdateFailureException_GitUpdateFailure.Builder();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder ref(String ref);
+
+      abstract Builder result(String result);
+
+      abstract Builder message(@Nullable String message);
+
+      abstract GitUpdateFailure build();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/git/LockFailureException.java b/java/com/google/gerrit/git/LockFailureException.java
index 9e67d70..371488d 100644
--- a/java/com/google/gerrit/git/LockFailureException.java
+++ b/java/com/google/gerrit/git/LockFailureException.java
@@ -14,36 +14,18 @@
 
 package com.google.gerrit.git;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
-
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** Thrown when updating a ref in Git fails with LOCK_FAILURE. */
-public class LockFailureException extends IOException {
+public class LockFailureException extends GitUpdateFailureException {
   private static final long serialVersionUID = 1L;
 
-  private final ImmutableList<String> refs;
-
   public LockFailureException(String message, RefUpdate refUpdate) {
-    super(message);
-    refs = ImmutableList.of(refUpdate.getName());
+    super(message, refUpdate);
   }
 
   public LockFailureException(String message, BatchRefUpdate batchRefUpdate) {
-    super(message);
-    refs =
-        batchRefUpdate.getCommands().stream()
-            .filter(c -> c.getResult() == ReceiveCommand.Result.LOCK_FAILURE)
-            .map(ReceiveCommand::getRefName)
-            .collect(toImmutableList());
-  }
-
-  /** Subset of ref names that caused the lock failure. */
-  public ImmutableList<String> getFailedRefs() {
-    return refs;
+    super(message, batchRefUpdate);
   }
 }
diff --git a/java/com/google/gerrit/git/RefUpdateUtil.java b/java/com/google/gerrit/git/RefUpdateUtil.java
index 520d0f2..fa7b98f 100644
--- a/java/com/google/gerrit/git/RefUpdateUtil.java
+++ b/java/com/google/gerrit/git/RefUpdateUtil.java
@@ -99,7 +99,7 @@
     if (lockFailure + aborted == bru.getCommands().size()) {
       throw new LockFailureException("Update aborted with one or more lock failures: " + bru, bru);
     } else if (failure > 0) {
-      throw new IOException("Update failed: " + bru);
+      throw new GitUpdateFailureException("Update failed: " + bru, bru);
     }
   }
 
@@ -130,7 +130,8 @@
       case REJECTED_CURRENT_BRANCH:
       case REJECTED_MISSING_OBJECT:
       case REJECTED_OTHER_REASON:
-        throw new IOException("Failed to update " + ru.getName() + ": " + ru.getResult());
+        throw new GitUpdateFailureException(
+            "Failed to update " + ru.getName() + ": " + ru.getResult(), ru);
     }
   }
 
@@ -174,7 +175,8 @@
       case REJECTED_MISSING_OBJECT:
       case REJECTED_OTHER_REASON:
       default:
-        throw new IOException("Failed to delete " + refName + ": " + ru.getResult());
+        throw new GitUpdateFailureException(
+            "Failed to delete " + refName + ": " + ru.getResult(), ru);
     }
   }
 
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 33e4bdd..407a4bc 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -166,6 +166,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
@@ -280,6 +281,7 @@
 
   private final Globals globals;
   private final Provider<RestCollection<RestResource, RestResource>> members;
+  private Optional<String> traceId;
 
   public RestApiServlet(
       Globals globals, RestCollection<? extends RestResource, ? extends RestResource> members) {
@@ -328,10 +330,9 @@
           logger.atFinest().log(
               "Received REST request: %s %s (parameters: %s)",
               req.getMethod(), req.getRequestURI(), getParameterNames(req));
+          logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
           logger.atFinest().log(
-              "Calling user: %s (groups = %s)",
-              globals.currentUser.get().getLoggableName(),
-              globals.currentUser.get().getEffectiveGroups().getKnownGroups());
+              "Groups: %s", globals.currentUser.get().getEffectiveGroups().getKnownGroups());
 
           if (isCorsPreflight(req)) {
             doCorsPreflight(req, res);
@@ -552,7 +553,8 @@
             throw new ResourceNotFoundException();
           }
 
-          response.traceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+          traceId = response.traceId();
+          traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
 
           if (response instanceof Response.Redirect) {
             CacheHeaders.setNotCacheable(res);
@@ -1486,11 +1488,12 @@
     }
   }
 
-  private static long handleException(
-      Throwable err, HttpServletRequest req, HttpServletResponse res) throws IOException {
+  private long handleException(Throwable err, HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
     logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
     if (!res.isCommitted()) {
       res.reset();
+      traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
       return replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
     }
     return 0;
diff --git a/java/com/google/gerrit/reviewdb/client/Change.java b/java/com/google/gerrit/reviewdb/client/Change.java
index 9a3672f..f2e0596 100644
--- a/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/java/com/google/gerrit/reviewdb/client/Change.java
@@ -16,13 +16,19 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeStatus;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 import java.sql.Timestamp;
 import java.util.Arrays;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 
 /**
  * A change proposed to be merged into a branch.
@@ -94,6 +100,16 @@
  * notice of a replacement patch set is sent, or when notice of the change submission occurs.
  */
 public final class Change {
+  private static final SecureRandom rng;
+
+  static {
+    try {
+      rng = SecureRandom.getInstance("SHA1PRNG");
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("Cannot create RNG for Change-Id generator", e);
+    }
+  }
+
   public static Id id(int id) {
     return new AutoValue_Change_Id(id);
   }
@@ -248,6 +264,20 @@
     }
   }
 
+  public static ObjectId generateChangeId() {
+    byte[] rand = new byte[Constants.OBJECT_ID_STRING_LENGTH];
+    rng.nextBytes(rand);
+    String randomString = new String(rand, UTF_8);
+
+    try (ObjectInserter f = new ObjectInserter.Formatter()) {
+      return f.idFor(Constants.OBJ_COMMIT, Constants.encode(randomString));
+    }
+  }
+
+  public static Key generateKey() {
+    return key("I" + generateChangeId().name());
+  }
+
   public static Key key(String key) {
     return new AutoValue_Change_Key(key);
   }
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
new file mode 100644
index 0000000..ea76330
--- /dev/null
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Allows implementors to control how certain exceptions should be handled.
+ *
+ * <p>This interface is intended to be implemented for multi-master setups to control the behavior
+ * for handling exceptions that are thrown by a lower layer that handles the consensus and
+ * synchronization between different server nodes. E.g. if an operation fails because consensus for
+ * a Git update could not be achieved (e.g. due to slow responding server nodes) this interface can
+ * be used to retry the request instead of failing it immediately.
+ */
+@ExtensionPoint
+public interface ExceptionHook {
+  /**
+   * Whether an operation should be retried if it failed with the given throwable.
+   *
+   * <p>Only affects operations that are executed with {@link
+   * com.google.gerrit.server.update.RetryHelper}.
+   *
+   * @param throwable throwable that was thrown while executing the operation
+   * @return whether the operation should be retried
+   */
+  default boolean shouldRetry(Throwable throwable) {
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 6a23084..1b1fab6 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -32,6 +32,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.GitUpdateFailureException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -268,7 +269,7 @@
           if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
             throw new LockFailureException(message, batchUpdate);
           }
-          throw new IOException(message);
+          throw new GitUpdateFailureException(message, batchUpdate);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 77f3e73..04ac477 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -83,7 +83,6 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.ChangeIdUtil;
 
 public class ChangeInserter implements InsertChangeOp {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -203,18 +202,9 @@
     if (!idList.isEmpty()) {
       return Change.key(idList.get(idList.size() - 1).trim());
     }
-    // A Change-Id is computed for the review, but not appended to the commit message.
+    // A Change-Id is generated for the review, but not appended to the commit message.
     // This can happen if requireChangeId is false.
-    ObjectId changeId =
-        ChangeIdUtil.computeChangeId(
-            commit.getTree(),
-            commit,
-            commit.getAuthorIdent(),
-            commit.getCommitterIdent(),
-            commit.getShortMessage());
-    StringBuilder changeIdStr = new StringBuilder();
-    changeIdStr.append("I").append(ObjectId.toString(changeId));
-    return Change.key(changeIdStr.toString());
+    return Change.generateKey();
   }
 
   public PatchSet.Id getPatchSetId() {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 45b70b2..3794f04 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -78,6 +78,7 @@
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.TraceRequestListener;
@@ -390,6 +391,7 @@
     DynamicSet.setOf(binder(), RequestListener.class);
     DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
     DynamicSet.setOf(binder(), ChangeETagComputation.class);
+    DynamicSet.setOf(binder(), ExceptionHook.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index f2180d7..aa735f9 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -18,8 +18,10 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.git.GitUpdateFailureException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
@@ -327,14 +329,7 @@
         }
 
         if (update.insertChangeId()) {
-          ObjectId id =
-              ChangeIdUtil.computeChangeId(
-                  res,
-                  getRevision(),
-                  commit.getAuthor(),
-                  commit.getCommitter(),
-                  commit.getMessage());
-          commit.setMessage(ChangeIdUtil.insertId(commit.getMessage(), id));
+          commit.setMessage(ChangeIdUtil.insertId(commit.getMessage(), Change.generateChangeId()));
         }
 
         src = rw.parseCommit(inserter.insert(commit));
@@ -439,13 +434,14 @@
           case REJECTED_MISSING_OBJECT:
           case REJECTED_OTHER_REASON:
           default:
-            throw new IOException(
+            throw new GitUpdateFailureException(
                 "Cannot update "
                     + ru.getName()
                     + " in "
                     + db.getDirectory()
                     + ": "
-                    + ru.getResult());
+                    + ru.getResult(),
+                ru);
         }
       }
     };
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 13ec5a8..3dcb695 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -585,9 +585,8 @@
   // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
   private void processCommandsUnsafe(
       Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
-    logger.atFine().log(
-        "Calling user: %s (groups = %s)",
-        user.getLoggableName(), user.getEffectiveGroups().getKnownGroups());
+    logger.atFine().log("Calling user: %s", user.getLoggableName());
+    logger.atFine().log("Groups: %s", user.getEffectiveGroups().getKnownGroups());
 
     if (!projectState.getProject().getState().permitsWrite()) {
       for (ReceiveCommand cmd : commands) {
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index fb58577..77e0e0c 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.notedb.NoteDbUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -51,12 +50,10 @@
 public class AuditLogReader {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  private final String serverId;
   private final AllUsersName allUsersName;
 
   @Inject
-  public AuditLogReader(@GerritServerId String serverId, AllUsersName allUsersName) {
-    this.serverId = serverId;
+  public AuditLogReader(AllUsersName allUsersName) {
     this.allUsersName = allUsersName;
   }
 
@@ -143,7 +140,7 @@
   }
 
   private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
-    Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent(), serverId);
+    Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent());
     if (!authorId.isPresent()) {
       // Only report audit events from identified users, since this was a non-nullable field in
       // ReviewDb. May be revisited.
@@ -179,7 +176,7 @@
   private Optional<Account.Id> parseAccount(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
     Optional<Account.Id> result =
         Optional.ofNullable(RawParseUtils.parsePersonIdent(line.getValue()))
-            .flatMap(ident -> NoteDbUtil.parseIdent(ident, serverId));
+            .flatMap(ident -> NoteDbUtil.parseIdent(ident));
     if (!result.isPresent()) {
       logInvalid(uuid, c, line);
     }
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 15a3532..30c5250 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -179,38 +179,43 @@
   public static class TraceTimer implements AutoCloseable {
     private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-    private final Consumer<Long> logFn;
+    private final Consumer<Long> doneLogFn;
     private final Stopwatch stopwatch;
 
     private TraceTimer(String operation) {
       this(
+          () -> logger.atFine().log("Starting timer for %s", operation),
           elapsedMs -> {
             LoggingContext.getInstance()
                 .addPerformanceLogRecord(() -> PerformanceLogRecord.create(operation, elapsedMs));
-            logger.atFine().log("%s (%d ms)", operation, elapsedMs);
+            logger.atFine().log("%s done (%d ms)", operation, elapsedMs);
           });
     }
 
     private TraceTimer(String operation, Metadata metadata) {
       this(
+          () ->
+              logger.atFine().log(
+                  "Starting timer for %s (%s)", operation, metadata.toStringForLogging()),
           elapsedMs -> {
             LoggingContext.getInstance()
                 .addPerformanceLogRecord(
                     () -> PerformanceLogRecord.create(operation, elapsedMs, metadata));
             logger.atFine().log(
-                "%s (%s) (%d ms)", operation, metadata.toStringForLogging(), elapsedMs);
+                "%s (%s) done (%d ms)", operation, metadata.toStringForLogging(), elapsedMs);
           });
     }
 
-    private TraceTimer(Consumer<Long> logFn) {
-      this.logFn = logFn;
+    private TraceTimer(Runnable startLogFn, Consumer<Long> doneLogFn) {
+      startLogFn.run();
+      this.doneLogFn = doneLogFn;
       this.stopwatch = Stopwatch.createStarted();
     }
 
     @Override
     public void close() {
       stopwatch.stop();
-      logFn.accept(stopwatch.elapsed(TimeUnit.MILLISECONDS));
+      doneLogFn.accept(stopwatch.elapsed(TimeUnit.MILLISECONDS));
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 9cc1c84..10bae48 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -51,6 +52,7 @@
     public final AllUsersName allUsers;
     public final LegacyChangeNoteRead legacyChangeNoteRead;
     public final NoteDbMetrics metrics;
+    public final String serverId;
 
     // Providers required to avoid dependency cycles.
 
@@ -64,7 +66,8 @@
         ChangeNoteJson changeNoteJson,
         LegacyChangeNoteRead legacyChangeNoteRead,
         NoteDbMetrics metrics,
-        Provider<ChangeNotesCache> cache) {
+        Provider<ChangeNotesCache> cache,
+        @GerritServerId String serverId) {
       this.failOnLoadForTest = new AtomicBoolean();
       this.repoManager = repoManager;
       this.allUsers = allUsers;
@@ -72,6 +75,7 @@
       this.changeNoteJson = changeNoteJson;
       this.metrics = metrics;
       this.cache = cache;
+      this.serverId = serverId;
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index e1217c2..929974d 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -22,6 +22,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
@@ -503,6 +504,19 @@
     ChangeNotesCache.Value v =
         args.cache.get().get(getProjectName(), getChangeId(), rev, handle::walk);
     state = v.state();
+
+    String stateServerId = state.serverId();
+    /**
+     * In earlier Gerrit versions serverId wasn't part of the change notes cache. That's why the
+     * earlier cached entries don't have the serverId attribute. That's fine because in earlier
+     * gerrit version serverId was already validated. Another approach to simplify the check would
+     * be to bump the cache version, but that would invalidate all persistent cache entries, what we
+     * rather try to avoid.
+     */
+    checkState(
+        Strings.isNullOrEmpty(stateServerId) || args.serverId.equals(stateServerId),
+        String.format("invalid server id, expected %s: actual: %s", args.serverId, stateServerId));
+
     state.copyColumnsTo(change);
     revisionNoteMap = v.revisionNoteMap();
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 27c2aa6..b5087ff 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -135,6 +135,7 @@
   private Timestamp createdOn;
   private Timestamp lastUpdatedOn;
   private Account.Id ownerId;
+  private String serverId;
   private String changeId;
   private String subject;
   private String originalSubject;
@@ -223,6 +224,7 @@
         createdOn,
         lastUpdatedOn,
         ownerId,
+        serverId,
         branch,
         buildCurrentPatchSetId(),
         subject,
@@ -334,6 +336,10 @@
     Account.Id accountId = parseIdent(commit);
     if (accountId != null) {
       ownerId = accountId;
+      PersonIdent personIdent = commit.getAuthorIdent();
+      serverId = NoteDbUtil.extractHostPartFromPersonIdent(personIdent);
+    } else {
+      serverId = "UNKNOWN_SERVER_ID";
     }
     Account.Id realAccountId = parseRealAccountId(commit, accountId);
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 2728516..5adc196 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -96,6 +96,7 @@
       Timestamp createdOn,
       Timestamp lastUpdatedOn,
       Account.Id owner,
+      String serverId,
       String branch,
       @Nullable PatchSet.Id currentPatchSetId,
       String subject,
@@ -154,6 +155,7 @@
                 .build())
         .pastAssignees(pastAssignees)
         .hashtags(hashtags)
+        .serverId(serverId)
         .patchSets(patchSets.entrySet())
         .approvals(approvals.entries())
         .reviewers(reviewers)
@@ -276,6 +278,9 @@
 
   abstract ImmutableSet<String> hashtags();
 
+  @Nullable
+  abstract String serverId();
+
   abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets();
 
   abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals();
@@ -376,6 +381,8 @@
 
     abstract Builder columns(ChangeColumns columns);
 
+    abstract Builder serverId(String serverId);
+
     abstract Builder pastAssignees(Set<Account.Id> pastAssignees);
 
     abstract Builder hashtags(Iterable<String> hashtags);
@@ -427,6 +434,10 @@
           .setChangeId(object.changeId().get())
           .setColumns(toChangeColumnsProto(object.columns()));
 
+      if (object.serverId() != null) {
+        b.setServerId(object.serverId());
+        b.setHasServerId(true);
+      }
       object.pastAssignees().forEach(a -> b.addPastAssignee(a.get()));
       object.hashtags().forEach(b::addHashtag);
       object
@@ -549,6 +560,7 @@
               .metaId(ObjectIdConverter.create().fromByteString(proto.getMetaId()))
               .changeId(changeId)
               .columns(toChangeColumns(changeId, proto.getColumns()))
+              .serverId(proto.getHasServerId() ? proto.getServerId() : null)
               .pastAssignees(
                   proto.getPastAssigneeList().stream().map(Account::id).collect(toImmutableSet()))
               .hashtags(proto.getHashtagList())
diff --git a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
index 36bfe47..7e301f7 100644
--- a/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
+++ b/java/com/google/gerrit/server/notedb/LegacyChangeNoteRead.java
@@ -50,14 +50,11 @@
 
   public Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
       throws ConfigInvalidException {
-    return NoteDbUtil.parseIdent(ident, serverId)
+    return NoteDbUtil.parseIdent(ident)
         .orElseThrow(
             () ->
                 parseException(
-                    changeId,
-                    "invalid identity, expected <id>@%s: %s",
-                    serverId,
-                    ident.getEmailAddress()));
+                    changeId, "cannot retrieve account id: %s", ident.getEmailAddress()));
   }
 
   private static boolean match(byte[] note, MutableInteger p, byte[] expected) {
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index c53f4b9..bfbdd18 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -37,25 +37,28 @@
       ImmutableSet.of(
           "com.google.gerrit.httpd.restapi.RestApiServlet", RetryingRestModifyView.class.getName());
 
-  /**
-   * Returns an AccountId for the given email address. Returns empty if the address isn't on this
-   * server.
-   */
-  public static Optional<Account.Id> parseIdent(PersonIdent ident, String serverId) {
+  /** Returns an AccountId for the given email address. */
+  public static Optional<Account.Id> parseIdent(PersonIdent ident) {
     String email = ident.getEmailAddress();
     int at = email.indexOf('@');
     if (at >= 0) {
-      String host = email.substring(at + 1);
-      if (host.equals(serverId)) {
-        Integer id = Ints.tryParse(email.substring(0, at));
-        if (id != null) {
-          return Optional.of(Account.id(id));
-        }
+      Integer id = Ints.tryParse(email.substring(0, at));
+      if (id != null) {
+        return Optional.of(Account.id(id));
       }
     }
     return Optional.empty();
   }
 
+  public static String extractHostPartFromPersonIdent(PersonIdent ident) {
+    String email = ident.getEmailAddress();
+    int at = email.indexOf('@');
+    if (at >= 0) {
+      return email.substring(at + 1);
+    }
+    throw new IllegalArgumentException("No host part found: " + email);
+  }
+
   public static String formatTime(PersonIdent ident, Timestamp t) {
     GitDateFormatter dateFormatter = new GitDateFormatter(Format.DEFAULT);
     // TODO(dborowitz): Use a ThreadLocal or use Joda.
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 858edf2..b8054cd 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -134,9 +134,8 @@
     logger.atFinest().log(
         "Filter refs for repository %s by visibility (options = %s, refs = %s)",
         projectState.getNameKey(), opts, refs);
-    logger.atFinest().log(
-        "Calling user: %s (groups = %s)",
-        user.getLoggableName(), user.getEffectiveGroups().getKnownGroups());
+    logger.atFinest().log("Calling user: %s", user.getLoggableName());
+    logger.atFinest().log("Groups: %s", user.getEffectiveGroups().getKnownGroups());
     logger.atFinest().log(
         "auth.skipFullRefEvaluationIfAllRefsAreVisible = %s",
         skipFullRefEvaluationIfAllRefsAreVisible);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPick.java b/java/com/google/gerrit/server/restapi/change/CherryPick.java
index bcbee4d..9dd6b86 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPick.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPick.java
@@ -81,9 +81,7 @@
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
     input.parent = input.parent == null ? 1 : input.parent;
-    if (input.message == null || input.message.trim().isEmpty()) {
-      throw new BadRequestException("message must be non-empty");
-    } else if (input.destination == null || input.destination.trim().isEmpty()) {
+    if (input.destination == null || input.destination.trim().isEmpty()) {
       throw new BadRequestException("destination must be non-empty");
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index afd9d8e..bf920e1 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -182,17 +182,14 @@
                 input.parent, commitToCherryPick.getParentCount()));
       }
 
+      String message = Strings.nullToEmpty(input.message).trim();
+      message = message.isEmpty() ? commitToCherryPick.getFullMessage() : message;
+
       Timestamp now = TimeUtil.nowTs();
       PersonIdent committerIdent = identifiedUser.newCommitterIdent(now, serverTimeZone);
 
-      final ObjectId computedChangeId =
-          ChangeIdUtil.computeChangeId(
-              commitToCherryPick.getTree(),
-              baseCommit,
-              commitToCherryPick.getAuthorIdent(),
-              committerIdent,
-              input.message);
-      String commitMessage = ChangeIdUtil.insertId(input.message, computedChangeId).trim() + '\n';
+      final ObjectId generatedChangeId = Change.generateChangeId();
+      String commitMessage = ChangeIdUtil.insertId(message, generatedChangeId).trim() + '\n';
 
       CodeReviewCommit cherryPickCommit;
       ProjectState projectState = projectCache.checkedGet(dest.project());
@@ -227,7 +224,7 @@
           final String idStr = idList.get(idList.size() - 1).trim();
           changeKey = Change.key(idStr);
         } else {
-          changeKey = Change.key("I" + computedChangeId.name());
+          changeKey = Change.key("I" + generatedChangeId.name());
         }
 
         BranchNameKey newDest = BranchNameKey.create(project, destRef.getName());
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
index 25cd924..0216a90 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickCommit.java
@@ -43,7 +43,6 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.revwalk.RevCommit;
 
 @Singleton
 public class CherryPickCommit
@@ -75,9 +74,6 @@
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
       throws IOException, UpdateException, RestApiException, PermissionBackendException,
           ConfigInvalidException, NoSuchProjectException {
-    RevCommit commit = rsrc.getCommit();
-    String message = Strings.nullToEmpty(input.message).trim();
-    input.message = message.isEmpty() ? commit.getFullMessage() : message;
     String destination = Strings.nullToEmpty(input.destination).trim();
     input.parent = input.parent == null ? 1 : input.parent;
     Project.NameKey projectName = rsrc.getProjectState().getNameKey();
@@ -101,7 +97,7 @@
               updateFactory,
               null,
               projectName,
-              commit,
+              rsrc.getCommit(),
               input,
               BranchNameKey.create(rsrc.getProjectState().getNameKey(), refName));
       CherryPickChangeInfo changeInfo =
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 445279f..c6c9595 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.reviewdb.client.BranchNameKey;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -64,6 +65,7 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.restapi.project.CommitsCollection;
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -78,11 +80,13 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -109,6 +113,7 @@
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson.Factory jsonFactory;
   private final ChangeFinder changeFinder;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final PatchSetUtil psUtil;
   private final MergeUtil.Factory mergeUtilFactory;
   private final SubmitType submitType;
@@ -129,6 +134,7 @@
       ChangeInserter.Factory changeInserterFactory,
       ChangeJson.Factory json,
       ChangeFinder changeFinder,
+      Provider<InternalChangeQuery> queryProvider,
       RetryHelper retryHelper,
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
@@ -147,6 +153,7 @@
     this.changeInserterFactory = changeInserterFactory;
     this.jsonFactory = json;
     this.changeFinder = changeFinder;
+    this.queryProvider = queryProvider;
     this.psUtil = psUtil;
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
@@ -208,6 +215,20 @@
     }
     input.subject = subject;
 
+    Optional<String> changeId = getChangeIdFromMessage(input.subject);
+    if (changeId.isPresent()) {
+      if (!queryProvider
+          .get()
+          .setLimit(1)
+          .byBranchKey(
+              BranchNameKey.create(input.project, input.branch), Change.key(changeId.get()))
+          .isEmpty()) {
+        throw new ResourceConflictException(
+            String.format(
+                "A change with Change-Id %s already exists for this branch.", changeId.get()));
+      }
+    }
+
     if (input.topic != null) {
       input.topic = Strings.emptyToNull(input.topic.trim());
     }
@@ -289,7 +310,7 @@
 
       Timestamp now = TimeUtil.nowTs();
       PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
-      String commitMessage = getCommitMessage(input.subject, me, oi, mergeTip, author);
+      String commitMessage = getCommitMessage(input.subject, me);
 
       RevCommit c;
       if (input.merge != null) {
@@ -386,18 +407,22 @@
     return parentCommit;
   }
 
-  private String getCommitMessage(
-      String subject,
-      IdentifiedUser me,
-      ObjectInserter objectInserter,
-      RevCommit mergeTip,
-      PersonIdent author)
-      throws IOException {
+  private Optional<String> getChangeIdFromMessage(String subject) {
+    int indexOfChangeId = ChangeIdUtil.indexOfChangeId(subject, "\n");
+    if (indexOfChangeId == -1) {
+      return Optional.empty();
+    }
+    return Optional.of(
+        subject.substring(
+            indexOfChangeId + 11 /* "Change-Id: "*/,
+            indexOfChangeId + 12 /* "Change-Id: I" */ + Constants.OBJECT_ID_STRING_LENGTH));
+  }
+
+  private String getCommitMessage(String subject, IdentifiedUser me) {
     // Add a Change-Id line if there isn't already one
     String commitMessage = subject;
     if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
-      ObjectId treeId = mergeTip == null ? emptyTreeId(objectInserter) : mergeTip.getTree();
-      ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, commitMessage);
+      ObjectId id = Change.generateChangeId();
       commitMessage = ChangeIdUtil.insertId(commitMessage, id);
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index 413d780..c69a348 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -201,14 +201,8 @@
                 patch.commitId().name());
       }
 
-      ObjectId computedChangeId =
-          ChangeIdUtil.computeChangeId(
-              parentToCommitToRevert.getTree(),
-              commitToRevert,
-              authorIdent,
-              committerIdent,
-              message);
-      revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, computedChangeId, true));
+      ObjectId generatedChangeId = Change.generateChangeId();
+      revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, generatedChangeId, true));
 
       Change.Id changeId = Change.id(seq.nextChangeId());
       ObjectId id = oi.insert(revertCommitBuilder);
@@ -240,7 +234,7 @@
         bu.setNotify(notify);
         bu.insertChange(ins);
         bu.addOp(changeId, new NotifyOp(changeToRevert, ins));
-        bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId));
+        bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(generatedChangeId));
         bu.execute();
       }
       return changeId;
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 9216eec..fdca897 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -19,10 +19,12 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -182,12 +184,13 @@
     }
     projectCache.checkedGet(rsrc.getProject()).checkStatePermitsWrite();
 
-    return Response.ok(new Output(mergeChange(rsrc, submitter, input)));
+    return mergeChange(rsrc, submitter, input);
   }
 
-  public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws RestApiException, IOException, UpdateException, ConfigInvalidException,
-          PermissionBackendException {
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public Response<Output> mergeChange(
+      RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
+      throws RestApiException, IOException {
     Change change = rsrc.getChange();
     if (!change.isNew()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -202,10 +205,17 @@
     }
 
     try (MergeOp op = mergeOpProvider.get()) {
-      Change updatedChange = op.merge(change, submitter, true, input, false);
+      Change updatedChange;
+
+      try {
+        updatedChange = op.merge(change, submitter, true, input, false);
+      } catch (Exception e) {
+        Throwables.throwIfInstanceOf(e, RestApiException.class);
+        return Response.<Output>internalServerError(e).traceId(op.getTraceId().orElse(null));
+      }
 
       if (updatedChange.isMerged()) {
-        return change;
+        return Response.ok(new Output(change));
       }
 
       String msg =
@@ -460,8 +470,14 @@
         throw new ResourceConflictException("current revision is missing");
       }
 
-      Output out = submit.apply(new RevisionResource(rsrc, ps), input).value();
-      return Response.ok(json.noOptions().format(out.change));
+      Response<Output> response = submit.apply(new RevisionResource(rsrc, ps), input);
+      if (response instanceof Response.InternalServerError) {
+        Response.InternalServerError<?> ise = (Response.InternalServerError<?>) response;
+        return Response.<ChangeInfo>internalServerError(ise.cause())
+            .traceId(ise.traceId().orElse(null));
+      }
+
+      return Response.ok(json.noOptions().format(response.value().change));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 024880f..813f9ab 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -91,6 +91,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -247,6 +248,7 @@
   private Set<Project.NameKey> allProjects;
   private boolean dryrun;
   private TopicMetrics topicMetrics;
+  private String traceId;
 
   @Inject
   MergeOp(
@@ -518,6 +520,7 @@
                         .multipliedBy(cs.projects().size()))
                 .caller(getClass())
                 .retryWithTrace(t -> !(t instanceof RestApiException))
+                .onAutoTrace(traceId -> this.traceId = traceId)
                 .build());
 
         if (projects > 1) {
@@ -538,6 +541,10 @@
     }
   }
 
+  public Optional<String> getTraceId() {
+    return Optional.ofNullable(traceId);
+  }
+
   private void openRepoManager() {
     if (orm != null) {
       orm.close();
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index b120379..bea3867 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -39,10 +39,12 @@
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.RequestId;
 import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.time.Duration;
@@ -182,14 +184,19 @@
 
   private final Metrics metrics;
   private final BatchUpdate.Factory updateFactory;
+  private final PluginSetContext<ExceptionHook> exceptionHooks;
   private final Map<ActionType, Duration> defaultTimeouts;
   private final WaitStrategy waitStrategy;
   @Nullable private final Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup;
   private final boolean retryWithTraceOnFailure;
 
   @Inject
-  RetryHelper(@GerritServerConfig Config cfg, Metrics metrics, BatchUpdate.Factory updateFactory) {
-    this(cfg, metrics, updateFactory, null);
+  RetryHelper(
+      @GerritServerConfig Config cfg,
+      Metrics metrics,
+      PluginSetContext<ExceptionHook> exceptionHooks,
+      BatchUpdate.Factory updateFactory) {
+    this(cfg, metrics, updateFactory, exceptionHooks, null);
   }
 
   @VisibleForTesting
@@ -197,9 +204,11 @@
       @GerritServerConfig Config cfg,
       Metrics metrics,
       BatchUpdate.Factory updateFactory,
+      PluginSetContext<ExceptionHook> exceptionHooks,
       @Nullable Consumer<RetryerBuilder<?>> overwriteDefaultRetryerStrategySetup) {
     this.metrics = metrics;
     this.updateFactory = updateFactory;
+    this.exceptionHooks = exceptionHooks;
 
     Duration defaultTimeout =
         Duration.ofMillis(
@@ -308,6 +317,11 @@
                   return true;
                 }
 
+                // Exception hooks may identify additional exceptions for retry.
+                if (exceptionHooks.stream().anyMatch(h -> h.shouldRetry(t))) {
+                  return true;
+                }
+
                 // A non-recoverable failure occurred. Check if we should retry to capture a trace
                 // of the failure. If a trace was already done there is no need to retry.
                 if (retryWithTraceOnFailure
diff --git a/javatests/com/google/gerrit/acceptance/annotation/BUILD b/javatests/com/google/gerrit/acceptance/annotation/BUILD
index 5476bb6..8dd99c9 100644
--- a/javatests/com/google/gerrit/acceptance/annotation/BUILD
+++ b/javatests/com/google/gerrit/acceptance/annotation/BUILD
@@ -4,4 +4,5 @@
     srcs = glob(["*.java"]),
     group = "annotation",
     labels = ["annotation"],
+    deps = ["//java/com/google/gerrit/server/util/time"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
new file mode 100644
index 0000000..ecfe3f5
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.annotation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+public class UseClockStepTest extends AbstractDaemonTest {
+  @Test
+  @UseClockStep
+  public void useClockStepWithDefaults() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isEqualTo(1000);
+  }
+
+  @Test
+  @UseClockStep(clockStepUnit = TimeUnit.MINUTES)
+  public void useClockStepWithTimeUnit() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isEqualTo(60 * 1000);
+  }
+
+  @Test
+  @UseClockStep(clockStep = 5)
+  public void useClockStepWithClockStep() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isEqualTo(5 * 1000);
+  }
+
+  @Test
+  @UseClockStep(startAtEpoch = true)
+  public void useClockStepWithStartAtEpoch() {
+    assertThat(TimeUtil.nowTs()).isEqualTo(Timestamp.from(Instant.EPOCH));
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseSystemTimeTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseSystemTimeTest.java
new file mode 100644
index 0000000..7480200
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseSystemTimeTest.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.annotation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseSystemTime;
+import com.google.gerrit.server.util.time.TimeUtil;
+import org.junit.Test;
+
+@UseClockStep
+public class UseSystemTimeTest extends AbstractDaemonTest {
+  @Test
+  @UseSystemTime
+  public void useSystemTimeAlthoughClassIsAnnotatedWithUseClockStep() {
+    long firstTimestamp = TimeUtil.nowMs();
+    long secondTimestamp = TimeUtil.nowMs();
+    assertThat(secondTimestamp - firstTimestamp).isLessThan(1000);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseTimezoneTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseTimezoneTest.java
new file mode 100644
index 0000000..abf5eda
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseTimezoneTest.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.annotation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseTimezone;
+import org.junit.Test;
+
+public class UseTimezoneTest extends AbstractDaemonTest {
+  @Test
+  @UseTimezone(timezone = "US/Eastern")
+  public void usEastern() {
+    assertThat(System.getProperty("user.timezone")).isEqualTo("US/Eastern");
+  }
+
+  @Test
+  @UseTimezone(timezone = "UTC")
+  public void utc() {
+    assertThat(System.getProperty("user.timezone")).isEqualTo("UTC");
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 9fb6d26..eb59669 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -61,6 +61,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -133,6 +134,8 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -143,7 +146,6 @@
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -419,36 +421,32 @@
   }
 
   @Test
+  @UseClockStep
   public void createAtomically() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
-      Account.Id accountId = Account.id(seq.nextAccountId());
-      String fullName = "Foo";
-      ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
-      AccountState accountState =
-          accountsUpdateProvider
-              .get()
-              .insert(
-                  "Create Account Atomically",
-                  accountId,
-                  u -> u.setFullName(fullName).addExternalId(extId));
-      assertThat(accountState.getAccount().fullName()).isEqualTo(fullName);
+    Account.Id accountId = Account.id(seq.nextAccountId());
+    String fullName = "Foo";
+    ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
+    AccountState accountState =
+        accountsUpdateProvider
+            .get()
+            .insert(
+                "Create Account Atomically",
+                accountId,
+                u -> u.setFullName(fullName).addExternalId(extId));
+    assertThat(accountState.getAccount().fullName()).isEqualTo(fullName);
 
-      AccountInfo info = gApi.accounts().id(accountId.get()).get();
-      assertThat(info.name).isEqualTo(fullName);
+    AccountInfo info = gApi.accounts().id(accountId.get()).get();
+    assertThat(info.name).isEqualTo(fullName);
 
-      List<EmailInfo> emails = gApi.accounts().id(accountId.get()).getEmails();
-      assertThat(emails.stream().map(e -> e.email).collect(toSet())).containsExactly(extId.email());
+    List<EmailInfo> emails = gApi.accounts().id(accountId.get()).getEmails();
+    assertThat(emails.stream().map(e -> e.email).collect(toSet())).containsExactly(extId.email());
 
-      RevCommit commitUserBranch =
-          projectOperations.project(allUsers).getHead(RefNames.refsUsers(accountId));
-      RevCommit commitRefsMetaExternalIds =
-          projectOperations.project(allUsers).getHead(RefNames.REFS_EXTERNAL_IDS);
-      assertThat(commitUserBranch.getCommitTime())
-          .isEqualTo(commitRefsMetaExternalIds.getCommitTime());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    RevCommit commitUserBranch =
+        projectOperations.project(allUsers).getHead(RefNames.refsUsers(accountId));
+    RevCommit commitRefsMetaExternalIds =
+        projectOperations.project(allUsers).getHead(RefNames.REFS_EXTERNAL_IDS);
+    assertThat(commitUserBranch.getCommitTime())
+        .isEqualTo(commitRefsMetaExternalIds.getCommitTime());
   }
 
   @Test
@@ -2593,7 +2591,11 @@
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg,
+                retryMetrics,
+                null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2646,6 +2648,7 @@
                 cfg,
                 retryMetrics,
                 null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
                 r ->
                     r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
                         .withBlockStrategy(noSleepBlockStrategy)),
@@ -2700,7 +2703,11 @@
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg,
+                retryMetrics,
+                null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2769,7 +2776,11 @@
             externalIds,
             metaDataUpdateInternalFactory,
             new RetryHelper(
-                cfg, retryMetrics, null, r -> r.withBlockStrategy(noSleepBlockStrategy)),
+                cfg,
+                retryMetrics,
+                null,
+                new PluginSetContext<>(DynamicSet.emptySet(), PluginMetrics.DISABLED_INSTANCE),
+                r -> r.withBlockStrategy(noSleepBlockStrategy)),
             extIdNotesFactory,
             ident,
             ident,
@@ -2906,9 +2917,9 @@
   }
 
   @Test
+  @UseClockStep
   public void deleteAllDraftComments() throws Exception {
     try {
-      TestTimeUtil.resetWithClockStep(1, SECONDS);
       Project.NameKey project2 = projectOperations.newProject().create();
       PushOneCommit.Result r1 = createChange();
 
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 6dc8fc5..e1af2e4 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -18,12 +18,12 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
@@ -51,15 +51,13 @@
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
+@UseClockStep
 public class AgreementsIT extends AbstractDaemonTest {
   private ContributorAgreement caAutoVerify;
   private ContributorAgreement caNoAutoVerify;
@@ -112,16 +110,6 @@
     return cfg;
   }
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Before
   public void setUp() throws Exception {
     caAutoVerify = configureContributorAgreement(true);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index 87f9a2d5..668dcfd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -28,6 +27,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
@@ -113,10 +113,9 @@
   }
 
   @Test
+  @UseClockStep
   @GerritConfig(name = "changeCleanup.abandonAfter", value = "1w")
   public void abandonInactiveOpenChanges() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-
     // create 2 changes which will be abandoned ...
     int id1 = createChange().getChange().getId().get();
     int id2 = createChange().getChange().getId().get();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index cf72874..61e7582 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -58,7 +58,6 @@
 import static com.google.gerrit.truth.CacheStatsSubject.assertThat;
 import static com.google.gerrit.truth.CacheStatsSubject.cloneStats;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
@@ -78,6 +77,8 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -182,7 +183,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
@@ -213,8 +213,8 @@
 import org.junit.Test;
 
 @NoHttpd
+@UseTimezone(timezone = "US/Eastern")
 public class ChangeIT extends AbstractDaemonTest {
-  private String systemTimeZone;
 
   @Inject private AccountOperations accountOperations;
   @Inject private ChangeIndexCollection changeIndexCollection;
@@ -242,17 +242,6 @@
   private RegistrationHandle changeIndexedCounterHandle;
 
   @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
-  @Before
   public void addChangeIndexedCounter() {
     changeIndexedCounter = new ChangeIndexedCounter();
     changeIndexedCounterHandle = changeIndexedListeners.add("gerrit", changeIndexedCounter);
@@ -1896,6 +1885,7 @@
   }
 
   @Test
+  @UseClockStep
   public void addReviewer() throws Exception {
     testAddReviewerViaPostReview(
         (changeId, reviewer) -> {
@@ -1906,6 +1896,7 @@
   }
 
   @Test
+  @UseClockStep
   public void addReviewerViaPostReview() throws Exception {
     testAddReviewerViaPostReview(
         (changeId, reviewer) -> {
@@ -1918,7 +1909,6 @@
   }
 
   private void testAddReviewerViaPostReview(AddReviewerCaller addReviewer) throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -2028,8 +2018,8 @@
   }
 
   @Test
+  @UseClockStep
   public void addReviewerThatIsNotPerfectMatch() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -2077,8 +2067,8 @@
   }
 
   @Test
+  @UseClockStep
   public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
@@ -2138,8 +2128,8 @@
   }
 
   @Test
+  @UseClockStep
   public void addSelfAsReviewer() throws Exception {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 216a2ec..7156c8d 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -16,6 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -40,11 +44,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import java.sql.Timestamp;
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 
 /** Tests for comment validation in {@link PostReview}. */
 public class PostReviewIT extends AbstractDaemonTest {
@@ -53,14 +56,14 @@
 
   private static final String COMMENT_TEXT = "The comment text";
 
-  private Capture<ImmutableList<CommentForValidation>> capture = new Capture<>();
+  @Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> capture;
 
   @Override
   public Module createModule() {
     return new FactoryModule() {
       @Override
       public void configure() {
-        CommentValidator mockCommentValidator = EasyMock.createMock(CommentValidator.class);
+        CommentValidator mockCommentValidator = mock(CommentValidator.class);
         bind(CommentValidator.class)
             .annotatedWith(Exports.named(mockCommentValidator.getClass()))
             .toInstance(mockCommentValidator);
@@ -71,23 +74,17 @@
 
   @Before
   public void resetMock() {
-    EasyMock.reset(mockCommentValidator);
-  }
-
-  @After
-  public void verifyMock() {
-    EasyMock.verify(mockCommentValidator);
+    initMocks(this);
+    clearInvocations(mockCommentValidator);
   }
 
   @Test
   public void validateCommentsInInput_commentOK() throws Exception {
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(
-                        CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of());
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
 
     PushOneCommit.Result r = createChange();
 
@@ -106,12 +103,9 @@
   public void validateCommentsInInput_commentRejected() throws Exception {
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT);
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
 
     PushOneCommit.Result r = createChange();
 
@@ -139,8 +133,6 @@
 
   @Test
   public void validateCommentsInInput_commentCleanedUp() throws Exception {
-    EasyMock.replay(mockCommentValidator);
-
     PushOneCommit.Result r = createChange();
     assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
 
@@ -159,13 +151,11 @@
 
   @Test
   public void validateDrafts_draftOK() throws Exception {
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(
-                        CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of());
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
 
     PushOneCommit.Result r = createChange();
 
@@ -186,13 +176,11 @@
   public void validateDrafts_draftRejected() throws Exception {
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.INLINE_COMMENT, COMMENT_TEXT);
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(
-                        CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.INLINE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
     PushOneCommit.Result r = createChange();
 
     DraftInput draft =
@@ -230,16 +218,14 @@
     testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draftFile);
     assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty();
 
-    EasyMock.expect(mockCommentValidator.validateComments(EasyMock.capture(capture)))
-        .andReturn(ImmutableList.of());
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(capture.capture())).thenReturn(ImmutableList.of());
 
     ReviewInput input = new ReviewInput();
     input.drafts = DraftHandling.PUBLISH;
     gApi.changes().id(r.getChangeId()).current().review(input);
     assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(2);
 
-    assertThat(capture.getValues()).hasSize(1);
+    assertThat(capture.getAllValues()).hasSize(1);
     assertThat(capture.getValue())
         .containsExactly(
             CommentForValidation.create(
@@ -250,12 +236,10 @@
 
   @Test
   public void validateCommentsInChangeMessage_messageOK() throws Exception {
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of());
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
     PushOneCommit.Result r = createChange();
 
     ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
@@ -271,12 +255,10 @@
   public void validateCommentsInChangeMessage_messageRejected() throws Exception {
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT);
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(CommentType.CHANGE_MESSAGE, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
     PushOneCommit.Result r = createChange();
 
     ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index d908ee5f..599a2b9 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -93,7 +94,6 @@
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -104,7 +104,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -121,10 +120,10 @@
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
 public class GroupsIT extends AbstractDaemonTest {
   @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
   @Inject private AccountOperations accountOperations;
@@ -140,16 +139,6 @@
   @Inject private Sequences seq;
   @Inject private StalenessChecker stalenessChecker;
 
-  @Before
-  public void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @After
   public void consistencyCheck() throws Exception {
     if (description.getAnnotation(IgnoreGroupInconsistencies.class) == null) {
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index 71ca289..f7902c4 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -96,10 +96,30 @@
   }
 
   @Test
+  public void cherryPickWithoutMessage() throws Exception {
+    String branch = "foo";
+
+    // Create change to cherry-pick
+    RevCommit revCommit = createChange().getCommit();
+
+    // Create target branch to cherry-pick to.
+    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+
+    // Cherry-pick without message.
+    CherryPickInput input = new CherryPickInput();
+    input.destination = branch;
+    String changeId =
+        gApi.projects().name(project.get()).commit(revCommit.name()).cherryPick(input).get().id;
+
+    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
+  }
+
+  @Test
   public void cherryPickCommitWithoutChangeId() throws Exception {
-    // This test is a little superfluous, since the current cherry-pick code ignores
-    // the commit message of the to-be-cherry-picked change, using the one in
-    // CherryPickInput instead.
     CherryPickInput input = new CherryPickInput();
     input.destination = "foo";
     input.message = "it goes to foo branch";
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 22b8051..2087f939 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -341,6 +341,34 @@
   }
 
   @Test
+  public void cherryPickWithoutMessage() throws Exception {
+    String branch = "foo";
+
+    // Create change to cherry-pick
+    PushOneCommit.Result change = createChange();
+    RevCommit revCommit = change.getCommit();
+
+    // Create target branch to cherry-pick to.
+    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+
+    // Cherry-pick without message.
+    CherryPickInput input = new CherryPickInput();
+    input.destination = branch;
+    String changeId =
+        gApi.changes()
+            .id(change.getChangeId())
+            .revision(revCommit.name())
+            .cherryPickAsInfo(input)
+            .id;
+
+    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
+  }
+
+  @Test
   public void cherryPickSetChangeId() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master");
     CherryPickInput in = new CherryPickInput();
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index df4b6d6..2e331c4 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -27,7 +27,6 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -37,6 +36,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
@@ -65,7 +65,6 @@
 import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage;
 import com.google.gerrit.server.restapi.change.ChangeEdits.Post;
 import com.google.gerrit.server.restapi.change.ChangeEdits.Put;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 import com.google.inject.Inject;
@@ -81,11 +80,10 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
+@UseClockStep
 public class ChangeEditIT extends AbstractDaemonTest {
 
   private static final String FILE_NAME = "foo";
@@ -103,16 +101,6 @@
   private String changeId2;
   private PatchSet ps;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Before
   public void setUp() throws Exception {
     changeId = newChange(admin.newIdent());
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 27cc241..564a29b 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -41,7 +41,6 @@
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static java.util.Comparator.comparing;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 
@@ -58,6 +57,7 @@
 import com.google.gerrit.acceptance.SkipProjectClone;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -105,7 +105,6 @@
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -134,12 +133,11 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 @SkipProjectClone
+@UseClockStep
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
   protected enum Protocol {
     // Only test protocols which are actually served by the Gerrit server, since each separate test
@@ -160,16 +158,6 @@
 
   @Inject private DynamicSet<CommitValidationListener> commitValidators;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Before
   public void setUpPatchSetLock() throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index e7501e7..8a0238a 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -16,16 +16,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -480,127 +479,107 @@
   }
 
   @Test
+  @UseClockStep
   public void superRepoCommitHasSameAuthorAsSubmoduleCommit() throws Exception {
     // Make sure that the commit is created at an earlier timestamp than the submit timestamp.
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
-      allowMatchingSubmoduleSubscription(
-          subKey, "refs/heads/master", superKey, "refs/heads/master");
-      createSubmoduleSubscription(superRepo, "master", subKey, "master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    createSubmoduleSubscription(superRepo, "master", subKey, "master");
 
-      PushOneCommit.Result pushResult =
-          createChange(subRepo, "refs/heads/master", "Change", "a.txt", "some content", null);
-      approve(pushResult.getChangeId());
-      gApi.changes().id(pushResult.getChangeId()).current().submit();
+    PushOneCommit.Result pushResult =
+        createChange(subRepo, "refs/heads/master", "Change", "a.txt", "some content", null);
+    approve(pushResult.getChangeId());
+    gApi.changes().id(pushResult.getChangeId()).current().submit();
 
-      // Expect that the author name/email is preserved for the superRepo commit, but a new author
-      // timestamp is used.
-      PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    // Expect that the author name/email is preserved for the superRepo commit, but a new author
+    // timestamp is used.
+    PersonIdent authorIdent = getAuthor(superRepo, "master");
+    assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
+    assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult.getCommit().getAuthorIdent().getWhen());
   }
 
   @Test
+  @UseClockStep
   public void superRepoCommitHasSameAuthorAsSubmoduleCommits() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    // Make sure that the commits are created at different timestamps and that the submit timestamp
-    // is afterwards.
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
+    Project.NameKey proj2 = createProjectForPush(getSubmitType());
 
-      Project.NameKey proj2 = createProjectForPush(getSubmitType());
+    TestRepository<?> subRepo2 = cloneProject(proj2);
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
-      TestRepository<?> subRepo2 = cloneProject(proj2);
-      allowMatchingSubmoduleSubscription(
-          subKey, "refs/heads/master", superKey, "refs/heads/master");
-      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+    prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
+    pushSubmoduleConfig(superRepo, "master", config);
 
-      Config config = new Config();
-      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
-      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
-      pushSubmoduleConfig(superRepo, "master", config);
+    String topic = "foo";
 
-      String topic = "foo";
+    PushOneCommit.Result pushResult1 =
+        createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
+    approve(pushResult1.getChangeId());
 
-      PushOneCommit.Result pushResult1 =
-          createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
-      approve(pushResult1.getChangeId());
+    PushOneCommit.Result pushResult2 =
+        createChange(subRepo2, "refs/heads/master", "Change 2", "b.txt", "other content", topic);
+    approve(pushResult2.getChangeId());
 
-      PushOneCommit.Result pushResult2 =
-          createChange(subRepo2, "refs/heads/master", "Change 2", "b.txt", "other content", topic);
-      approve(pushResult2.getChangeId());
+    // Submit the topic, 2 changes with the same author.
+    gApi.changes().id(pushResult1.getChangeId()).current().submit();
 
-      // Submit the topic, 2 changes with the same author.
-      gApi.changes().id(pushResult1.getChangeId()).current().submit();
-
-      // Expect that the author name/email is preserved for the superRepo commit, but a new author
-      // timestamp is used.
-      PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    // Expect that the author name/email is preserved for the superRepo commit, but a new author
+    // timestamp is used.
+    PersonIdent authorIdent = getAuthor(superRepo, "master");
+    assertThat(authorIdent.getName()).isEqualTo(admin.fullName());
+    assertThat(authorIdent.getEmailAddress()).isEqualTo(admin.email());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
   }
 
   @Test
+  @UseClockStep
   public void superRepoCommitHasGerritAsAuthorIfAuthorsOfSubmoduleCommitsDiffer() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
-    // Make sure that the commits are created at different timestamps and that the submit timestamp
-    // is afterwards.
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-    try {
-      Project.NameKey proj2 = createProjectForPush(getSubmitType());
-      TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
+    Project.NameKey proj2 = createProjectForPush(getSubmitType());
+    TestRepository<InMemoryRepository> repo2 = cloneProject(proj2, user);
 
-      allowMatchingSubmoduleSubscription(
-          subKey, "refs/heads/master", superKey, "refs/heads/master");
-      allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
+    allowMatchingSubmoduleSubscription(proj2, "refs/heads/master", superKey, "refs/heads/master");
 
-      Config config = new Config();
-      prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
-      prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
-      pushSubmoduleConfig(superRepo, "master", config);
+    Config config = new Config();
+    prepareSubmoduleConfigEntry(config, subKey, subKey, "master");
+    prepareSubmoduleConfigEntry(config, proj2, proj2, "master");
+    pushSubmoduleConfig(superRepo, "master", config);
 
-      String topic = "foo";
+    String topic = "foo";
 
-      // Create change as admin.
-      PushOneCommit.Result pushResult1 =
-          createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
-      approve(pushResult1.getChangeId());
+    // Create change as admin.
+    PushOneCommit.Result pushResult1 =
+        createChange(subRepo, "refs/heads/master", "Change 1", "a.txt", "some content", topic);
+    approve(pushResult1.getChangeId());
 
-      // Create change as user.
-      PushOneCommit push =
-          pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
-      PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
-      approve(pushResult2.getChangeId());
+    // Create change as user.
+    PushOneCommit push =
+        pushFactory.create(user.newIdent(), repo2, "Change 2", "b.txt", "other content");
+    PushOneCommit.Result pushResult2 = push.to("refs/for/master/" + name(topic));
+    approve(pushResult2.getChangeId());
 
-      // Submit the topic, 2 changes with the different author.
-      gApi.changes().id(pushResult1.getChangeId()).current().submit();
+    // Submit the topic, 2 changes with the different author.
+    gApi.changes().id(pushResult1.getChangeId()).current().submit();
 
-      // Expect that the Gerrit server identity is chosen as author for the superRepo commit and a
-      // new author timestamp is used.
-      PersonIdent authorIdent = getAuthor(superRepo, "master");
-      assertThat(authorIdent.getName()).isEqualTo(serverIdent.get().getName());
-      assertThat(authorIdent.getEmailAddress()).isEqualTo(serverIdent.get().getEmailAddress());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
-      assertThat(authorIdent.getWhen())
-          .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
-    } finally {
-      TestTimeUtil.useSystemTime();
-    }
+    // Expect that the Gerrit server identity is chosen as author for the superRepo commit and a
+    // new author timestamp is used.
+    PersonIdent authorIdent = getAuthor(superRepo, "master");
+    assertThat(authorIdent.getName()).isEqualTo(serverIdent.get().getName());
+    assertThat(authorIdent.getEmailAddress()).isEqualTo(serverIdent.get().getEmailAddress());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult1.getCommit().getAuthorIdent().getWhen());
+    assertThat(authorIdent.getWhen())
+        .isGreaterThan(pushResult2.getCommit().getAuthorIdent().getWhen());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index af4a22e..0e51208 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.httpd.restapi.ParameterParser;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.validators.CommitValidationException;
@@ -85,6 +86,7 @@
   @Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
   @Inject private DynamicSet<PerformanceLogger> performanceLoggers;
   @Inject private DynamicSet<SubmitRule> submitRules;
+  @Inject private DynamicSet<ExceptionHook> exceptionHooks;
   @Inject private WorkQueue workQueue;
 
   private TraceValidatingProjectCreationValidationListener projectCreationListener;
@@ -585,17 +587,18 @@
     assertThat(projectCreationListener.tags.get("project")).containsExactly("new24");
   }
 
+  @Test
   @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
   public void autoRetryWithTrace() throws Exception {
     String changeId = createChange().getChangeId();
     approve(changeId);
 
     TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
-    traceSubmitRule.failOnce = true;
+    traceSubmitRule.failAlways = true;
     RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
     try {
       RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
-      assertThat(response.getStatusCode()).isEqualTo(SC_OK);
+      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
       assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).startsWith("retry-on-failure-");
       assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
       assertThat(traceSubmitRule.isLoggingForced).isTrue();
@@ -605,6 +608,36 @@
   }
 
   @Test
+  @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+  public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+    String changeId = createChange().getChangeId();
+    approve(changeId);
+
+    TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+    traceSubmitRule.failAlways = true;
+    RegistrationHandle submitRuleRegistrationHandle = submitRules.add("gerrit", traceSubmitRule);
+    RegistrationHandle exceptionHookRegistrationHandle =
+        exceptionHooks.add(
+            "gerrit",
+            new ExceptionHook() {
+              @Override
+              public boolean shouldRetry(Throwable t) {
+                return true;
+              }
+            });
+    try {
+      RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+      assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(traceSubmitRule.traceId).isNull();
+      assertThat(traceSubmitRule.isLoggingForced).isFalse();
+    } finally {
+      submitRuleRegistrationHandle.remove();
+      exceptionHookRegistrationHandle.remove();
+    }
+  }
+
+  @Test
   public void noAutoRetryWithTraceIfDisabled() throws Exception {
     String changeId = createChange().getChangeId();
     approve(changeId);
@@ -617,7 +650,7 @@
       assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
       assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
       assertThat(traceSubmitRule.traceId).isNull();
-      assertThat(traceSubmitRule.isLoggingForced).isNull();
+      assertThat(traceSubmitRule.isLoggingForced).isFalse();
     } finally {
       submitRuleRegistrationHandle.remove();
     }
@@ -677,18 +710,19 @@
     String traceId;
     Boolean isLoggingForced;
     boolean failOnce;
+    boolean failAlways;
 
     @Override
     public Optional<SubmitRecord> evaluate(ChangeData changeData) {
-      if (failOnce) {
-        failOnce = false;
-        throw new IllegalStateException("forced failure from test");
-      }
-
       this.traceId =
           Iterables.getFirst(LoggingContext.getInstance().getTagsAsMap().get("TRACE_ID"), null);
       this.isLoggingForced = LoggingContext.getInstance().shouldForceLogging(null, null, false);
 
+      if (failOnce || failAlways) {
+        failOnce = false;
+        throw new IllegalStateException("forced failure from test");
+      }
+
       SubmitRecord submitRecord = new SubmitRecord();
       submitRecord.status = SubmitRecord.Status.OK;
       return Optional.of(submitRecord);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index a756c97..c0083d2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -29,7 +29,6 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
 import static org.mockito.Mockito.atLeast;
@@ -49,6 +48,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
@@ -95,7 +96,6 @@
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -119,10 +119,11 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.RefSpec;
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 public abstract class AbstractSubmit extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config submitWholeTopicEnabled() {
@@ -138,19 +139,6 @@
   @Inject private Submit submitHandler;
 
   private RegistrationHandle onSubmitValidatorHandle;
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
 
   @After
   public void removeOnSubmitValidator() {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 39eb5cd..094bcd5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -19,12 +19,12 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
@@ -35,30 +35,18 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.Iterator;
 import java.util.List;
 import org.eclipse.jgit.transport.RefSpec;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
 public class AssigneeIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Test
   public void getNoAssignee() throws Exception {
     PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index a55f17e..221e854 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -23,7 +23,6 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
 import static com.google.gerrit.server.restapi.change.DeleteChangeMessage.createNewChangeMessage;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static java.util.stream.Collectors.toSet;
 import static org.eclipse.jgit.util.RawParseUtils.decode;
 
@@ -32,6 +31,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -45,7 +46,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -54,30 +54,16 @@
 import java.util.Optional;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 @RunWith(ConfigSuite.class)
 public class ChangeMessagesIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
   @Test
   public void messagesNotReturnedByDefault() throws Exception {
     String changeId = createChange().getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 54a50ce..a3285be 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
 import com.google.common.base.Strings;
@@ -29,6 +28,8 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseSystemTime;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -51,34 +52,28 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
 import com.google.gerrit.testing.FakeEmailSender.Message;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
+@UseClockStep
 public class CreateChangeIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Test
   public void createEmptyChange_MissingBranch() throws Exception {
     ChangeInput ci = new ChangeInput();
@@ -160,6 +155,31 @@
   }
 
   @Test
+  public void cannotCreateChangeWithChangeIfOfExistingChangeOnSameBranch() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "Subject\n\nChange-Id: " + changeId;
+    assertCreateFails(
+        ci,
+        ResourceConflictException.class,
+        "A change with Change-Id " + changeId + " already exists for this branch.");
+  }
+
+  @Test
+  public void canCreateChangeWithChangeIfOfExistingChangeOnOtherBranch() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    createBranch(BranchNameKey.create(project, "other"));
+
+    ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+    ci.subject = "Subject\n\nChange-Id: " + changeId;
+    ci.branch = "other";
+    ChangeInfo info = assertCreateSucceeds(ci);
+    assertThat(info.changeId).isEqualTo(changeId);
+  }
+
+  @Test
   public void notificationsOnChangeCreation() throws Exception {
     requestScopeOperations.setApiUser(user.id());
     watch(project.get());
@@ -445,6 +465,7 @@
   }
 
   @Test
+  @UseSystemTime
   public void sha1sOfTwoNewChangesDiffer() throws Exception {
     ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
     ChangeInfo info1 = assertCreateSucceeds(changeInput);
@@ -452,6 +473,33 @@
     assertThat(info1.currentRevision).isNotEqualTo(info2.currentRevision);
   }
 
+  @Test
+  @UseSystemTime
+  public void sha1sOfTwoNewChangesDifferIfCreatedConcurrently() throws Exception {
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    try {
+      for (int i = 0; i < 10; i++) {
+        ChangeInput changeInput = newChangeInput(ChangeStatus.NEW);
+
+        CyclicBarrier sync = new CyclicBarrier(2);
+        Callable<ChangeInfo> createChange =
+            () -> {
+              requestScopeOperations.setApiUser(admin.id());
+              sync.await();
+              return assertCreateSucceeds(changeInput);
+            };
+
+        Future<ChangeInfo> changeInfo1 = executor.submit(createChange);
+        Future<ChangeInfo> changeInfo2 = executor.submit(createChange);
+        assertThat(changeInfo1.get().currentRevision)
+            .isNotEqualTo(changeInfo2.get().currentRevision);
+      }
+    } finally {
+      executor.shutdown();
+      executor.awaitTermination(5, TimeUnit.SECONDS);
+    }
+  }
+
   private ChangeInput newChangeInput(ChangeStatus status) {
     ChangeInput in = new ChangeInput();
     in.project = project.get();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 542c6a9..c57a035 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -28,6 +27,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
@@ -35,26 +35,14 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
 public class HashtagsIT extends AbstractDaemonTest {
   @Inject private ProjectOperations projectOperations;
 
-  @BeforeClass
-  public static void setTimeForTesting() {
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @AfterClass
-  public static void restoreTime() {
-    TestTimeUtil.useSystemTime();
-  }
-
   @Inject private RequestScopeOperations requestScopeOperations;
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 9882c77..58ec5eb 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -20,7 +20,6 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -29,6 +28,8 @@
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -52,7 +53,6 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -64,11 +64,11 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 @NoHttpd
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 public class GetRelatedIT extends AbstractDaemonTest {
   private static final int MAX_TERMS = 10;
 
@@ -84,20 +84,6 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
   @Inject private IndexConfig indexConfig;
   @Inject private ChangesCollection changes;
 
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
index cb5add3..6677583 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
@@ -15,6 +15,10 @@
 package com.google.gerrit.acceptance.server.git.receive;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -30,11 +34,10 @@
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 
 /**
  * Tests for comment validation when publishing drafts via the {@code --publish-comments} option.
@@ -45,14 +48,14 @@
 
   private static final String COMMENT_TEXT = "The comment text";
 
-  private Capture<ImmutableList<CommentForValidation>> capture = new Capture<>();
+  @Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> capture;
 
   @Override
   public Module createModule() {
     return new FactoryModule() {
       @Override
       public void configure() {
-        CommentValidator mockCommentValidator = EasyMock.createMock(CommentValidator.class);
+        CommentValidator mockCommentValidator = mock(CommentValidator.class);
         bind(CommentValidator.class)
             .annotatedWith(Exports.named(mockCommentValidator.getClass()))
             .toInstance(mockCommentValidator);
@@ -63,23 +66,17 @@
 
   @Before
   public void resetMock() {
-    EasyMock.reset(mockCommentValidator);
-  }
-
-  @After
-  public void verifyMock() {
-    EasyMock.verify(mockCommentValidator);
+    initMocks(this);
+    clearInvocations(mockCommentValidator);
   }
 
   @Test
   public void validateComments_commentOK() throws Exception {
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(
-                        CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of());
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of());
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     String revId = result.getCommit().getName();
@@ -96,13 +93,11 @@
   public void validateComments_commentRejected() throws Exception {
     CommentForValidation commentForValidation =
         CommentForValidation.create(CommentType.FILE_COMMENT, COMMENT_TEXT);
-    EasyMock.expect(
-            mockCommentValidator.validateComments(
-                ImmutableList.of(
-                    CommentForValidation.create(
-                        CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
-        .andReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(
+            ImmutableList.of(
+                CommentForValidation.create(
+                    CommentForValidation.CommentType.FILE_COMMENT, COMMENT_TEXT))))
+        .thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     String revId = result.getCommit().getName();
@@ -117,9 +112,7 @@
 
   @Test
   public void validateComments_inlineVsFileComments_allOK() throws Exception {
-    EasyMock.expect(mockCommentValidator.validateComments(EasyMock.capture(capture)))
-        .andReturn(ImmutableList.of());
-    EasyMock.replay(mockCommentValidator);
+    when(mockCommentValidator.validateComments(capture.capture())).thenReturn(ImmutableList.of());
     PushOneCommit.Result result = createChange();
     String changeId = result.getChangeId();
     String revId = result.getCommit().getName();
@@ -132,7 +125,7 @@
     assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).isEmpty();
     amendChange(changeId, "refs/for/master%publish-comments", admin, testRepo);
     assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(2);
-    assertThat(capture.getValues()).hasSize(1);
+    assertThat(capture.getAllValues()).hasSize(1);
     assertThat(capture.getValue())
         .containsExactly(
             CommentForValidation.create(
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 1386aec..0826c166 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.acceptance.server.mail;
 
 import static com.google.common.truth.Truth.assertThat;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -27,7 +28,6 @@
 import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender;
-import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.time.ZoneId;
@@ -37,28 +37,14 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 /** Tests the presence of required metadata in email headers, text and html. */
+@UseClockStep
+@UseTimezone(timezone = "US/Eastern")
 public class MailMetadataIT extends AbstractDaemonTest {
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  private String systemTimeZone;
-
-  @Before
-  public void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    TestTimeUtil.resetWithClockStep(1, SECONDS);
-  }
-
-  @After
-  public void resetTime() {
-    TestTimeUtil.useSystemTime();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
   @Test
   public void metadataOnNewChange() throws Exception {
     PushOneCommit.Result newChange = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
index 7d4b95e..8e0cd3d 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/DefaultQuotaBackendIT.java
@@ -16,9 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.resetToStrict;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.extensions.annotations.Exports;
@@ -35,13 +35,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import java.util.Collections;
-import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
 
 public class DefaultQuotaBackendIT extends AbstractDaemonTest {
 
-  private static final QuotaEnforcer quotaEnforcer = EasyMock.createStrictMock(QuotaEnforcer.class);
+  private static final QuotaEnforcer quotaEnforcer = mock(QuotaEnforcer.class);
 
   private IdentifiedUser identifiedAdmin;
   @Inject private QuotaBackend quotaBackend;
@@ -61,14 +60,13 @@
   @Before
   public void setUp() {
     identifiedAdmin = identifiedUserFactory.create(admin.id());
-    resetToStrict(quotaEnforcer);
+    clearInvocations(quotaEnforcer);
   }
 
   @Test
   public void requestTokenForUser() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -77,8 +75,7 @@
   public void requestTokenForUserAndAccount() {
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -87,8 +84,7 @@
   public void requestTokenForUserAndProject() {
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).project(project).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -102,8 +98,7 @@
             .change(changeId)
             .project(project)
             .build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenReturn(QuotaResponse.ok());
     assertThat(
             quotaBackend.user(identifiedAdmin).change(changeId, project).requestToken("testGroup"))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
@@ -112,8 +107,7 @@
   @Test
   public void requestTokens() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 123)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 123)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).requestTokens("testGroup", 123))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -121,8 +115,7 @@
   @Test
   public void dryRun() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.dryRun("testGroup", ctx, 123)).andReturn(QuotaResponse.ok());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.dryRun("testGroup", ctx, 123)).thenReturn(QuotaResponse.ok());
     assertThat(quotaBackend.user(identifiedAdmin).dryRun("testGroup", 123))
         .isEqualTo(singletonAggregation(QuotaResponse.ok()));
   }
@@ -132,8 +125,7 @@
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).availableTokens("testGroup"))
         .isEqualTo(singletonAggregation(r));
   }
@@ -143,8 +135,7 @@
     QuotaRequestContext ctx =
         QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(quotaBackend.user(identifiedAdmin).project(project).availableTokens("testGroup"))
         .isEqualTo(singletonAggregation(r));
   }
@@ -159,8 +150,7 @@
             .project(project)
             .build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(
             quotaBackend
                 .user(identifiedAdmin)
@@ -173,8 +163,7 @@
   public void availableTokens() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
     QuotaResponse r = QuotaResponse.ok(10L);
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(r);
     assertThat(quotaBackend.user(identifiedAdmin).availableTokens("testGroup"))
         .isEqualTo(singletonAggregation(r));
   }
@@ -182,9 +171,8 @@
   @Test
   public void requestTokenError() throws Exception {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1))
-        .andReturn(QuotaResponse.error("failed"));
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1))
+        .thenReturn(QuotaResponse.error("failed"));
 
     QuotaResponse.Aggregated result = quotaBackend.user(identifiedAdmin).requestToken("testGroup");
     assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
@@ -195,9 +183,7 @@
   @Test
   public void availableTokensError() throws Exception {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.availableTokens("testGroup", ctx))
-        .andReturn(QuotaResponse.error("failed"));
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenReturn(QuotaResponse.error("failed"));
     QuotaResponse.Aggregated result =
         quotaBackend.user(identifiedAdmin).availableTokens("testGroup");
     assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
@@ -208,8 +194,7 @@
   @Test
   public void requestTokenPluginThrowsAndRethrows() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.requestTokens("testGroup", ctx, 1)).andThrow(new NullPointerException());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.requestTokens("testGroup", ctx, 1)).thenThrow(new NullPointerException());
 
     assertThrows(
         NullPointerException.class,
@@ -219,8 +204,7 @@
   @Test
   public void availableTokensPluginThrowsAndRethrows() {
     QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
-    expect(quotaEnforcer.availableTokens("testGroup", ctx)).andThrow(new NullPointerException());
-    replay(quotaEnforcer);
+    when(quotaEnforcer.availableTokens("testGroup", ctx)).thenThrow(new NullPointerException());
 
     assertThrows(
         NullPointerException.class,
diff --git a/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java b/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
index 05b3b83..801288a 100644
--- a/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/quota/RepositorySizeQuotaIT.java
@@ -15,15 +15,16 @@
 package com.google.gerrit.acceptance.server.quota;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
 import static com.google.gerrit.server.quota.QuotaResponse.ok;
-import static org.easymock.EasyMock.anyLong;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.resetToStrict;
-import static org.easymock.EasyMock.verify;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.UseLocalDisk;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.quota.QuotaResponse;
 import com.google.inject.Module;
 import java.util.Collections;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.api.errors.TooLargeObjectInPackException;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.junit.Before;
@@ -42,9 +42,9 @@
 @UseLocalDisk
 public class RepositorySizeQuotaIT extends AbstractDaemonTest {
   private static final QuotaBackend.WithResource quotaBackendWithResource =
-      EasyMock.createStrictMock(QuotaBackend.WithResource.class);
+      mock(QuotaBackend.WithResource.class);
   private static final QuotaBackend.WithUser quotaBackendWithUser =
-      EasyMock.createStrictMock(QuotaBackend.WithUser.class);
+      mock(QuotaBackend.WithUser.class);
 
   @Override
   public Module createModule() {
@@ -70,64 +70,42 @@
 
   @Before
   public void setUp() {
-    resetToStrict(quotaBackendWithResource);
-    resetToStrict(quotaBackendWithUser);
+    clearInvocations(quotaBackendWithResource);
+    clearInvocations(quotaBackendWithUser);
   }
 
   @Test
   public void pushWithAvailableTokens() throws Exception {
-    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
-        .andReturn(singletonAggregation(ok(276L)))
-        .times(2);
-    expect(quotaBackendWithResource.requestTokens(eq(REPOSITORY_SIZE_GROUP), anyLong()))
-        .andReturn(singletonAggregation(ok()));
-    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
-    replay(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
+    when(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .thenReturn(singletonAggregation(ok(276L)));
+    when(quotaBackendWithResource.requestTokens(eq(REPOSITORY_SIZE_GROUP), anyLong()))
+        .thenReturn(singletonAggregation(ok()));
+    when(quotaBackendWithUser.project(project)).thenReturn(quotaBackendWithResource);
     pushCommit();
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    verify(quotaBackendWithResource, times(2)).availableTokens(REPOSITORY_SIZE_GROUP);
   }
 
   @Test
   public void pushWithNotSufficientTokens() throws Exception {
     long availableTokens = 1L;
-    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
-        .andReturn(singletonAggregation(ok(availableTokens)))
-        .anyTimes();
-    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
-    replay(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
-    try {
-      pushCommit();
-      assertWithMessage("expected TooLargeObjectInPackException").fail();
-    } catch (TooLargeObjectInPackException e) {
-      String msg = e.getMessage();
-      assertThat(msg).contains("Object too large");
-      assertThat(msg)
-          .contains(String.format("Max object size limit is %d bytes.", availableTokens));
-    }
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    when(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .thenReturn(singletonAggregation(ok(availableTokens)));
+    when(quotaBackendWithUser.project(project)).thenReturn(quotaBackendWithResource);
+    TooLargeObjectInPackException thrown =
+        assertThrows(TooLargeObjectInPackException.class, () -> pushCommit());
+    assertThat(thrown).hasMessageThat().contains("Object too large");
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains(String.format("Max object size limit is %d bytes.", availableTokens));
   }
 
   @Test
   public void errorGettingAvailableTokens() throws Exception {
     String msg = "quota error";
-    expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
-        .andReturn(singletonAggregation(QuotaResponse.error(msg)))
-        .anyTimes();
-    expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
-    replay(quotaBackendWithResource);
-    replay(quotaBackendWithUser);
-    try {
-      pushCommit();
-      assertWithMessage("expected TransportException").fail();
-    } catch (TransportException e) {
-      // TransportException has not much info about the cause
-    }
-    verify(quotaBackendWithUser);
-    verify(quotaBackendWithResource);
+    when(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
+        .thenReturn(singletonAggregation(QuotaResponse.error(msg)));
+    when(quotaBackendWithUser.project(project)).thenReturn(quotaBackendWithResource);
+    assertThrows(TransportException.class, () -> pushCommit());
   }
 
   private void pushCommit() throws Exception {
diff --git a/javatests/com/google/gerrit/httpd/BUILD b/javatests/com/google/gerrit/httpd/BUILD
index 6849d66..2254c4e 100644
--- a/javatests/com/google/gerrit/httpd/BUILD
+++ b/javatests/com/google/gerrit/httpd/BUILD
@@ -19,11 +19,11 @@
         "//lib:junit",
         "//lib:servlet-api-3_1-without-neverlink",
         "//lib:soy",
-        "//lib/easymock",
         "//lib/guice",
         "//lib/guice:guice-servlet",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/jgit/org.eclipse.jgit.junit:junit",
+        "//lib/mockito",
         "//lib/truth",
         "//lib/truth:truth-java8-extension",
     ],
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index 99835dd..77ab58b 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -15,10 +15,8 @@
 package com.google.gerrit.httpd.raw;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -35,22 +33,22 @@
 
   @Test
   public void renderTemplate() throws Exception {
-    Accounts accountsApi = createMock(Accounts.class);
-    expect(accountsApi.self()).andThrow(new AuthException("user needs to be authenticated"));
+    Accounts accountsApi = mock(Accounts.class);
+    when(accountsApi.self()).thenThrow(new AuthException("user needs to be authenticated"));
 
-    Server serverApi = createMock(Server.class);
-    expect(serverApi.getVersion()).andReturn("123");
-    expect(serverApi.topMenus()).andReturn(ImmutableList.of());
+    Server serverApi = mock(Server.class);
+    when(serverApi.getVersion()).thenReturn("123");
+    when(serverApi.topMenus()).thenReturn(ImmutableList.of());
     ServerInfo serverInfo = new ServerInfo();
     serverInfo.defaultTheme = "my-default-theme";
-    expect(serverApi.getInfo()).andReturn(serverInfo);
+    when(serverApi.getInfo()).thenReturn(serverInfo);
 
-    Config configApi = createMock(Config.class);
-    expect(configApi.server()).andReturn(serverApi);
+    Config configApi = mock(Config.class);
+    when(configApi.server()).thenReturn(serverApi);
 
-    GerritApi gerritApi = createMock(GerritApi.class);
-    expect(gerritApi.accounts()).andReturn(accountsApi);
-    expect(gerritApi.config()).andReturn(configApi);
+    GerritApi gerritApi = mock(GerritApi.class);
+    when(gerritApi.accounts()).thenReturn(accountsApi);
+    when(gerritApi.config()).thenReturn(configApi);
 
     String testCanonicalUrl = "foo-url";
     String testCdnPath = "bar-cdn";
@@ -60,18 +58,8 @@
 
     FakeHttpServletResponse response = new FakeHttpServletResponse();
 
-    replay(gerritApi);
-    replay(configApi);
-    replay(serverApi);
-    replay(accountsApi);
-
     servlet.doGet(new FakeHttpServletRequest(), response);
 
-    verify(gerritApi);
-    verify(configApi);
-    verify(serverApi);
-    verify(accountsApi);
-
     String output = response.getActualBodyString();
     assertThat(output).contains("<!DOCTYPE html>");
     assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl);
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index 8bac910..9f083b2 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -17,18 +17,15 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.getCurrentArguments;
-import static org.easymock.EasyMock.not;
-import static org.easymock.EasyMock.replay;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -41,6 +38,8 @@
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 public class UniversalGroupBackendTest {
   private static final AccountGroup.UUID OTHER_UUID = AccountGroup.uuid("other");
@@ -52,8 +51,7 @@
 
   @Before
   public void setup() {
-    user = createNiceMock(IdentifiedUser.class);
-    replay(user);
+    user = mock(IdentifiedUser.class);
     backends = new DynamicSet<>();
     backends.add("gerrit", new SystemGroupBackend(new Config()));
     backend =
@@ -103,23 +101,24 @@
   public void otherMemberships() {
     final AccountGroup.UUID handled = AccountGroup.uuid("handled");
     final AccountGroup.UUID notHandled = AccountGroup.uuid("not handled");
-    final IdentifiedUser member = createNiceMock(IdentifiedUser.class);
-    final IdentifiedUser notMember = createNiceMock(IdentifiedUser.class);
+    final IdentifiedUser member = mock(IdentifiedUser.class);
+    final IdentifiedUser notMember = mock(IdentifiedUser.class);
 
-    GroupBackend backend = createMock(GroupBackend.class);
-    expect(backend.handles(handled)).andStubReturn(true);
-    expect(backend.handles(not(eq(handled)))).andStubReturn(false);
-    expect(backend.membershipsOf(anyObject(IdentifiedUser.class)))
-        .andStubAnswer(
-            () -> {
-              Object[] args = getCurrentArguments();
-              GroupMembership membership = createMock(GroupMembership.class);
-              expect(membership.contains(eq(handled))).andStubReturn(args[0] == member);
-              expect(membership.contains(not(eq(notHandled)))).andStubReturn(false);
-              replay(membership);
-              return membership;
+    GroupBackend backend = mock(GroupBackend.class);
+    when(backend.handles(eq(handled))).thenReturn(true);
+    when(backend.handles(not(eq(handled)))).thenReturn(false);
+    when(backend.membershipsOf(any(IdentifiedUser.class)))
+        .thenAnswer(
+            new Answer<GroupMembership>() {
+              @Override
+              public GroupMembership answer(InvocationOnMock invocation) {
+                GroupMembership membership = mock(GroupMembership.class);
+                when(membership.contains(eq(handled)))
+                    .thenReturn(invocation.getArguments()[0] == member);
+                when(membership.contains(eq(notHandled))).thenReturn(false);
+                return membership;
+              }
             });
-    replay(member, notMember, backend);
 
     backends = new DynamicSet<>();
     backends.add("gerrit", backend);
diff --git a/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java b/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
index c8df548..2174927 100644
--- a/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
+++ b/javatests/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
@@ -16,8 +16,8 @@
 
 import static com.google.gerrit.server.edit.tree.TreeModificationSubject.assertThatList;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.replay;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -30,17 +30,16 @@
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
-import org.easymock.EasyMock;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
 public class FixReplacementInterpreterTest {
-  private final FileContentUtil fileContentUtil = createMock(FileContentUtil.class);
-  private final Repository repository = createMock(Repository.class);
-  private final ProjectState projectState = createMock(ProjectState.class);
-  private final ObjectId patchSetCommitId = createMock(ObjectId.class);
+  private final FileContentUtil fileContentUtil = mock(FileContentUtil.class);
+  private final Repository repository = mock(Repository.class);
+  private final ProjectState projectState = mock(ProjectState.class);
+  private final ObjectId patchSetCommitId = mock(ObjectId.class);
   private final String filePath1 = "an/arbitrary/file.txt";
   private final String filePath2 = "another/arbitrary/file.txt";
 
@@ -68,7 +67,6 @@
         new FixReplacement(filePath2, new Range(2, 0, 3, 0), "Another modified content");
     mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement, fixReplacement3, fixReplacement2);
     List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications);
@@ -99,7 +97,6 @@
     FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 0, 3, 0), "");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -114,7 +111,6 @@
         new FixReplacement(filePath1, new Range(2, 0, 2, 0), "A new line\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -128,7 +124,6 @@
     FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(1, 6, 3, 1), "and t");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -144,7 +139,6 @@
         new FixReplacement(filePath1, new Range(2, 7, 2, 11), "modification");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement1, fixReplacement2);
     assertThatList(treeModifications)
@@ -162,7 +156,6 @@
         new FixReplacement(filePath1, new Range(2, 7, 3, 5), "content");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement1, fixReplacement2);
     assertThatList(treeModifications)
@@ -178,7 +171,6 @@
         new FixReplacement(filePath1, new Range(4, 0, 4, 0), "New content");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -198,7 +190,6 @@
         new FixReplacement(filePath2, new Range(3, 0, 4, 0), "Second modification\n");
     mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement3, fixReplacement1, fixReplacement2);
     List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications);
@@ -219,7 +210,6 @@
     FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 11, 3, 0), "\r");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
     assertThatList(treeModifications)
         .onlyElement()
@@ -238,7 +228,6 @@
         new FixReplacement(filePath1, new Range(4, 0, 5, 0), "3rd modification\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\nFourth line\nFifth line\n");
 
-    replay(fileContentUtil);
     List<TreeModification> treeModifications =
         toTreeModifications(fixReplacement2, fixReplacement1, fixReplacement3);
     assertThatList(treeModifications)
@@ -255,7 +244,6 @@
         new FixReplacement(filePath1, new Range(5, 0, 5, 0), "A new line\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
@@ -265,8 +253,6 @@
         new FixReplacement(filePath1, new Range(0, 0, 0, 0), "A new line\n");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
     assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
@@ -276,7 +262,6 @@
         new FixReplacement(filePath1, new Range(1, 0, 1, 11), "modified");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
@@ -286,8 +271,6 @@
         new FixReplacement(filePath1, new Range(3, 0, 3, 11), "modified");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
-
     assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
@@ -297,14 +280,12 @@
         new FixReplacement(filePath1, new Range(1, -1, 1, 5), "modified");
     mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
 
-    replay(fileContentUtil);
     assertThrows(ResourceConflictException.class, () -> toTreeModifications(fixReplacement));
   }
 
   private void mockFileContent(String filePath, String fileContent) throws Exception {
-    EasyMock.expect(
-            fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath))
-        .andReturn(BinaryResult.create(fileContent));
+    when(fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath))
+        .thenReturn(BinaryResult.create(fileContent));
   }
 
   private List<TreeModification> toTreeModifications(FixReplacement... fixReplacements)
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index 491594b..cbef04a 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -16,10 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.reset;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.client.Project;
@@ -55,9 +53,8 @@
     site.resolve("git").toFile().mkdir();
     cfg = new Config();
     cfg.setString("gerrit", null, "basePath", "git");
-    configMock = createNiceMock(RepositoryConfig.class);
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of()).anyTimes();
-    replay(configMock);
+    configMock = mock(RepositoryConfig.class);
+    when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of());
     repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
   }
 
@@ -90,10 +87,8 @@
   public void alternateRepositoryLocation() throws IOException {
     Path alternateBasePath = temporaryFolder.newFolder().toPath();
     Project.NameKey someProjectKey = Project.nameKey("someProject");
-    reset(configMock);
-    expect(configMock.getBasePath(someProjectKey)).andReturn(alternateBasePath).anyTimes();
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of(alternateBasePath)).anyTimes();
-    replay(configMock);
+    when(configMock.getBasePath(someProjectKey)).thenReturn(alternateBasePath);
+    when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(alternateBasePath));
 
     Repository repo = repoManager.createRepository(someProjectKey);
     assertThat(repo.getDirectory()).isNotNull();
@@ -123,11 +118,9 @@
 
     Path alternateBasePath = temporaryFolder.newFolder().toPath();
 
-    reset(configMock);
-    expect(configMock.getBasePath(altPathProject)).andReturn(alternateBasePath).anyTimes();
-    expect(configMock.getBasePath(misplacedProject2)).andReturn(alternateBasePath).anyTimes();
-    expect(configMock.getAllBasePaths()).andReturn(ImmutableList.of(alternateBasePath)).anyTimes();
-    replay(configMock);
+    when(configMock.getBasePath(altPathProject)).thenReturn(alternateBasePath);
+    when(configMock.getBasePath(misplacedProject2)).thenReturn(alternateBasePath);
+    when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(alternateBasePath));
 
     repoManager.createRepository(basePathProject);
     repoManager.createRepository(altPathProject);
@@ -155,11 +148,8 @@
     assertThrows(
         IllegalStateException.class,
         () -> {
-          configMock = createNiceMock(RepositoryConfig.class);
-          expect(configMock.getAllBasePaths())
-              .andReturn(ImmutableList.of(Paths.get("repos")))
-              .anyTimes();
-          replay(configMock);
+          configMock = mock(RepositoryConfig.class);
+          when(configMock.getAllBasePaths()).thenReturn(ImmutableList.of(Paths.get("repos")));
           repoManager = new MultiBaseLocalDiskRepositoryManager(site, cfg, configMock);
         });
   }
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index a5b755e..81108ea 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -37,7 +37,7 @@
 
   @Before
   public void setUp() throws Exception {
-    auditLogReader = new AuditLogReader(SERVER_ID, allUsersName);
+    auditLogReader = new AuditLogReader(allUsersName);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index ff114fa7..6b0945c 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.mail.send;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.reviewdb.client.Account;
@@ -43,7 +43,7 @@
   public void setUp() throws Exception {
     config = new Config();
     ident = new PersonIdent("NAME", "e@email", 0, 0);
-    accountCache = createStrictMock(AccountCache.class);
+    accountCache = mock(AccountCache.class);
   }
 
   private FromAddressGenerator create() {
@@ -83,12 +83,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -98,12 +97,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(null, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isNull();
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -113,23 +111,21 @@
     final String name = "A U. Thor";
     final Account.Id user = user(name, null);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
   public void USER_NullUser() {
     setFrom("USER");
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
@@ -140,12 +136,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -156,12 +151,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -173,12 +167,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -190,12 +183,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -206,12 +198,11 @@
     final String email = "a.u.thor@test.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name);
     assertThat(r.getEmail()).isEqualTo(email);
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -234,23 +225,21 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = userNoLookup(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
   public void SERVER_NullUser() {
     setFrom("SERVER");
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
@@ -273,12 +262,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -288,12 +276,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(null, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo("Anonymous Coward (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -303,23 +290,21 @@
     final String name = "A U. Thor";
     final Account.Id user = user(name, null);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(name + " (Code Review)");
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
   public void MIXED_NullUser() {
     setFrom("MIXED");
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
-    verify(accountCache);
+    verifyZeroInteractions(accountCache);
   }
 
   @Test
@@ -330,12 +315,11 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(name, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo("A " + name + " B");
     assertThat(r.getEmail()).isEqualTo("my.server@email.address");
-    verify(accountCache);
+    verifyAccountCacheGet(user);
   }
 
   @Test
@@ -345,32 +329,32 @@
     final String email = "a.u.thor@test.example.com";
     final Account.Id user = user(null, email);
 
-    replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo("A Anonymous Coward B");
     assertThat(r.getEmail()).isEqualTo("my.server@email.address");
-    verify(accountCache);
   }
 
   @Test
   public void CUSTOM_NullUser() {
     setFrom("A ${user} B <my.server@email.address>");
 
-    replay(accountCache);
     final Address r = create().from(null);
     assertThat(r).isNotNull();
     assertThat(r.getName()).isEqualTo(ident.getName());
     assertThat(r.getEmail()).isEqualTo("my.server@email.address");
-    verify(accountCache);
   }
 
   private Account.Id user(String name, String email) {
     final AccountState s = makeUser(name, email);
-    expect(accountCache.get(eq(s.getAccount().id()))).andReturn(Optional.of(s));
+    when(accountCache.get(eq(s.getAccount().id()))).thenReturn(Optional.of(s));
     return s.getAccount().id();
   }
 
+  private void verifyAccountCacheGet(Account.Id id) {
+    verify(accountCache).get(eq(id));
+  }
+
   private Account.Id userNoLookup(String name, String email) {
     final AccountState s = makeUser(name, email);
     return s.getAccount().id();
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 3e54863..a4602e19 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -56,6 +56,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.UUID;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
@@ -66,6 +67,7 @@
       ObjectId.fromString("1234567812345678123456781234567812345678");
   private static final ByteString SHA_BYTES = ObjectIdConverter.create().toByteString(SHA);
   private static final String CHANGE_KEY = "Iabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
+  private static final String DEFAULT_SERVER_ID = UUID.randomUUID().toString();
 
   private ChangeColumns cols;
   private ChangeColumnsProto colsProto;
@@ -717,6 +719,7 @@
             ImmutableMap.<String, Type>builder()
                 .put("metaId", ObjectId.class)
                 .put("changeId", Change.Id.class)
+                .put("serverId", String.class)
                 .put("columns", ChangeColumns.class)
                 .put("pastAssignees", new TypeLiteral<ImmutableSet<Account.Id>>() {}.getType())
                 .put("hashtags", new TypeLiteral<ImmutableSet<String>>() {}.getType())
@@ -918,6 +921,19 @@
                 .build());
   }
 
+  @Test
+  public void serializeServerId() throws Exception {
+    assertRoundTrip(
+        newBuilder().serverId(DEFAULT_SERVER_ID).build(),
+        ChangeNotesStateProto.newBuilder()
+            .setMetaId(SHA_BYTES)
+            .setChangeId(ID.get())
+            .setServerId(DEFAULT_SERVER_ID)
+            .setHasServerId(true)
+            .setColumns(colsProto.toBuilder())
+            .build());
+  }
+
   private static ChangeNotesStateProto toProto(ChangeNotesState state) throws Exception {
     return ChangeNotesStateProto.parseFrom(Serializer.INSTANCE.serialize(state));
   }
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index 63f3eaf..8e57534 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -329,17 +329,8 @@
           name: 'ldap/tests tests'}});
         assert.equal(element._rules.length, 3);
         assert.equal(Object.keys(element._groupsWithRules).length, 3);
-        if (Polymer.Element) {
-          // Under Polymer 2 gr-rule-editor.js#_handleValueChange get's
-          // fully loaded before this change, thus `modified: true` get's managed
-          // to be added. Under Polymer 1 it was a mix hence why it was not
-          // added in time for when this test ran.
-          assert.deepEqual(element.permission.value.rules['ldap:CN=test test'],
-              {action: 'ALLOW', min: -2, max: 2, modified: true, added: true});
-        } else {
-          assert.deepEqual(element.permission.value.rules['ldap:CN=test test'],
-              {action: 'ALLOW', min: -2, max: 2, added: true});
-        }
+        assert.deepEqual(element.permission.value.rules['ldap:CN=test test'],
+            {action: 'ALLOW', min: -2, max: 2, added: true});
         // New rule should be removed if cancel from editing.
         element.editing = false;
         assert.equal(element._rules.length, 2);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index 53db6d5..f22c5a5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -102,10 +102,12 @@
     const SCHEMES = {http: {}, repo: {}, ssh: {}};
 
     function getFormFields() {
-      const selects = Polymer.dom(element.root).querySelectorAll('select');
-      const textareas =
-          Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea');
-      const inputs = Polymer.dom(element.root).querySelectorAll('input');
+      const selects = Array.from(
+          Polymer.dom(element.root).querySelectorAll('select'));
+      const textareas = Array.from(
+          Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea'));
+      const inputs = Array.from(
+          Polymer.dom(element.root).querySelectorAll('input'));
       return inputs.concat(textareas).concat(selects);
     }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index f206514..df5a9f3 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -118,11 +118,20 @@
       this._setupValues(this.rule);
     },
 
+    attached() {
+      if (!this.rule) { return; } // Check needed for test purposes.
+      if (!this._originalRuleValues) {
+        // Observer _handleValueChange is called after the ready()
+        // method finishes. Original values must be set later to
+        // avoid set .modified flag to true
+        this._setOriginalRuleValues(this.rule.value);
+      }
+    },
+
     _setupValues(rule) {
       if (!rule.value) {
         this._setDefaultRuleValues();
       }
-      this._setOriginalRuleValues(rule.value);
     },
 
     _computeForce(permission, action) {
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 17e8c6c..4ea3817 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -202,7 +202,7 @@
     });
 
     suite('already existing generic rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'submit';
         element.rule = {
@@ -218,6 +218,10 @@
         // by the parent element.
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -313,7 +317,7 @@
     });
 
     suite('new edit rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'editTopicName';
         element.rule = {
@@ -323,6 +327,10 @@
         element._setupValues(element.rule);
         flushAsynchronousOperations();
         element.rule.value.added = true;
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -362,7 +370,7 @@
     });
 
     suite('already existing rule with labels', () => {
-      setup(() => {
+      setup(done => {
         element.label = {values: [
           {value: -2, text: 'This shall not be merged'},
           {value: -1, text: 'I would prefer this is not merged as is'},
@@ -384,6 +392,10 @@
         element.section = 'refs/*';
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -416,7 +428,7 @@
     });
 
     suite('new rule with labels', () => {
-      setup(() => {
+      setup(done => {
         sandbox.spy(element, '_setDefaultRuleValues');
         element.label = {values: [
           {value: -2, text: 'This shall not be merged'},
@@ -434,6 +446,10 @@
         element._setupValues(element.rule);
         flushAsynchronousOperations();
         element.rule.value.added = true;
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -474,7 +490,7 @@
     });
 
     suite('already existing push rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'push';
         element.rule = {
@@ -487,6 +503,10 @@
         element.section = 'refs/*';
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -515,7 +535,7 @@
     });
 
     suite('new push rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'push';
         element.rule = {
@@ -525,6 +545,10 @@
         element._setupValues(element.rule);
         flushAsynchronousOperations();
         element.rule.value.added = true;
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -555,7 +579,7 @@
     });
 
     suite('already existing edit rule', () => {
-      setup(() => {
+      setup(done => {
         element.group = 'Group Name';
         element.permission = 'editTopicName';
         element.rule = {
@@ -568,6 +592,10 @@
         element.section = 'refs/*';
         element._setupValues(element.rule);
         flushAsynchronousOperations();
+        flush(() => {
+          element.attached();
+          done();
+        });
       });
 
       test('_ruleValues and _originalRuleValues are set correctly', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 282d8de..de02923 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -146,7 +146,8 @@
     });
 
     test('anchors use download attribute', () => {
-      const anchors = Polymer.dom(element.root).querySelectorAll('a');
+      const anchors = Array.from(
+          Polymer.dom(element.root).querySelectorAll('a'));
       assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 37beff4..9e6997c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -148,7 +148,7 @@
 
       _shownFiles: {
         type: Array,
-        computed: '_computeFilesShown(numFilesShown, _files.*)',
+        computed: '_computeFilesShown(numFilesShown, _files)',
       },
 
       /**
@@ -473,7 +473,7 @@
 
       this.set(['_files', index, 'isReviewed'], reviewed);
       if (index < this._shownFiles.length) {
-        this.set(['_shownFiles', index, 'isReviewed'], reviewed);
+        this.notifyPath(`_shownFiles.${index}.isReviewed`);
       }
 
       this._saveReviewedState(path, reviewed);
@@ -855,7 +855,7 @@
       const previousNumFilesShown = this._shownFiles ?
           this._shownFiles.length : 0;
 
-      const filesShown = files.base.slice(0, numFilesShown);
+      const filesShown = files.slice(0, numFilesShown);
       this.fire('files-shown-changed', {length: filesShown.length});
 
       // Start the timer for the rendering work hwere because this is where the
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 9529d38..cc3e937 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -1709,8 +1709,9 @@
 
       // Commit message should not have edit controls.
       const editControls =
-          Polymer.dom(element.root).querySelectorAll('.row:not(.header)')
-            .map(row => row.querySelector('gr-edit-file-controls'));
+          Array.from(
+              Polymer.dom(element.root).querySelectorAll('.row:not(.header)'))
+              .map(row => row.querySelector('gr-edit-file-controls'));
       assert.isTrue(editControls[0].classList.contains('invisible'));
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 4920e20..5f7ccbe 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -267,7 +267,7 @@
       assert.isTrue(element.$$('iron-selector').hidden);
     });
 
-    test('asymetrical labels', () => {
+    test('asymetrical labels', done => {
       element.permittedLabels = {
         'Code-Review': [
           '-2',
@@ -281,30 +281,35 @@
           '+1',
         ],
       };
-      flushAsynchronousOperations();
-      assert.strictEqual(element.$$('iron-selector')
-          .items.length, 2);
-      assert.strictEqual(Polymer.dom(element.root).
-          querySelectorAll('.placeholder').length, 3);
+      flush(() => {
+        assert.strictEqual(element.$$('iron-selector')
+            .items.length, 2);
+        assert.strictEqual(
+            Polymer.dom(element.root).querySelectorAll('.placeholder').length,
+            3);
 
-      element.permittedLabels = {
-        'Code-Review': [
-          ' 0',
-          '+1',
-        ],
-        'Verified': [
-          '-2',
-          '-1',
-          ' 0',
-          '+1',
-          '+2',
-        ],
-      };
-      flushAsynchronousOperations();
-      assert.strictEqual(element.$$('iron-selector')
-          .items.length, 5);
-      assert.strictEqual(Polymer.dom(element.root).
-          querySelectorAll('.placeholder').length, 0);
+        element.permittedLabels = {
+          'Code-Review': [
+            ' 0',
+            '+1',
+          ],
+          'Verified': [
+            '-2',
+            '-1',
+            ' 0',
+            '+1',
+            '+2',
+          ],
+        };
+        flush(() => {
+          assert.strictEqual(element.$$('iron-selector')
+              .items.length, 5);
+          assert.strictEqual(
+              Polymer.dom(element.root).querySelectorAll('.placeholder').length,
+              0);
+          done();
+        });
+      });
     });
 
     test('default_value', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 13c1754..80115c6 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -158,9 +158,13 @@
     _expandedChanged(exp) {
       for (let i = 0; i < this._processedMessages.length; i++) {
         this._processedMessages[i].expanded = exp;
-        if (i < this._visibleMessages.length) {
-          this.set(['_visibleMessages', i, 'expanded'], exp);
-        }
+      }
+      // _visibleMessages is a subarray of _processedMessages
+      // _processedMessages contains all items from _visibleMessages
+      // At this point all _visibleMessages.expanded values are set,
+      // and notifyPath must be used to notify Polymer about changes.
+      for (let i = 0; i < this._visibleMessages.length; i++) {
+        this.notifyPath(`_visibleMessages.${i}.expanded`);
       }
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 01f1f4d..6b738d8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -277,17 +277,19 @@
       });
     });
 
-    test('setlabelValue', () => {
+    test('setlabelValue', done => {
       element._account = {_account_id: 1};
-      flushAsynchronousOperations();
-      const label = 'Verified';
-      const value = '+1';
-      element.setLabelValue(label, value);
-      flushAsynchronousOperations();
-      const labels = element.$.labelScores.getLabelValues();
-      assert.deepEqual(labels, {
-        'Code-Review': 0,
-        'Verified': 1,
+      flush(() => {
+        const label = 'Verified';
+        const value = '+1';
+        element.setLabelValue(label, value);
+
+        const labels = element.$.labelScores.getLabelValues();
+        assert.deepEqual(labels, {
+          'Code-Review': 0,
+          'Verified': 1,
+        });
+        done();
       });
     });
 
@@ -310,6 +312,26 @@
       });
     }
 
+    function isFocusInsideElement(element) {
+      // In Polymer 2 focused element either <paper-input> or nested
+      // native input <input> element depending on the current focus
+      // in browser window.
+      // For example, the focus is changed if the developer console
+      // get a focus.
+      let activeElement = getActiveElement();
+      while (activeElement) {
+        if (activeElement === element) {
+          return true;
+        }
+        if (activeElement.parentElement) {
+          activeElement = activeElement.parentElement;
+        } else {
+          activeElement = activeElement.getRootNode().host;
+        }
+      }
+      return false;
+    }
+
     function testConfirmationDialog(done, cc) {
       const yesButton =
           element.$$('.reviewerConfirmationButtons gr-button:first-child');
@@ -363,7 +385,8 @@
         assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
 
         // We should be focused on account entry input.
-        assert.equal(getActiveElement().id, 'input');
+        assert.isTrue(
+            isFocusInsideElement(element.$.reviewers.$.entry.$.input.$.input));
 
         // No reviewer/CC should have been added.
         assert.equal(element.$$('#ccs').additions().length, 0);
@@ -408,7 +431,13 @@
             ]);
 
         // We should be focused on account entry input.
-        assert.equal(getActiveElement().id, 'input');
+        if (cc) {
+          assert.isTrue(
+              isFocusInsideElement(element.$.ccs.$.entry.$.input.$.input));
+        } else {
+          assert.isTrue(
+              isFocusInsideElement(element.$.reviewers.$.entry.$.input.$.input));
+        }
       }).then(done);
     }
 
@@ -1239,4 +1268,4 @@
       assert.equal(element.$.pluginMessage.textContent, 'foo');
     });
   });
-</script>
\ No newline at end of file
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector.js
deleted file mode 100644
index 28c46f4..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function() {
-  'use strict';
-
-  const JANK_SLEEP_TIME_MS = 1000;
-
-  const GrJankDetector = {
-    // Slowdowns counter.
-    jank: 0,
-    fps: 0,
-    _lastFrameTime: 0,
-
-    start() {
-      this._requestAnimationFrame(this._detect.bind(this));
-    },
-
-    _requestAnimationFrame(callback) {
-      window.requestAnimationFrame(callback);
-    },
-
-    _detect(now) {
-      if (this._lastFrameTime === 0) {
-        this._lastFrameTime = now;
-        this.fps = 0;
-        this._requestAnimationFrame(this._detect.bind(this));
-        return;
-      }
-      const fpsNow = 1000/(now - this._lastFrameTime);
-      this._lastFrameTime = now;
-      // Calculate moving average within last 3 measurements.
-      this.fps = this.fps === 0 ? fpsNow : ((this.fps * 2 + fpsNow) / 3);
-      if (this.fps > 10) {
-        this._requestAnimationFrame(this._detect.bind(this));
-      } else {
-        this.jank++;
-        console.warn('JANK', this.jank);
-        this._lastFrameTime = 0;
-        window.setTimeout(
-            () => this._requestAnimationFrame(this._detect.bind(this)),
-            JANK_SLEEP_TIME_MS);
-      }
-    },
-  };
-
-  window.GrJankDetector = GrJankDetector;
-})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
deleted file mode 100644
index 825a5fc..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-jank-detector_test.html
+++ /dev/null
@@ -1,80 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-jank-detector</title>
-<script src="/test/common-test-setup.js"></script>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-
-<script src="gr-jank-detector.js"></script>
-
-<script>
-  suite('gr-jank-detector tests', () => {
-    let sandbox;
-    let clock;
-    let instance;
-
-    const NOW_TIME = 100;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      clock = sinon.useFakeTimers(NOW_TIME);
-      instance = GrJankDetector;
-      instance._lastFrameTime = 0;
-      sandbox.stub(instance, '_requestAnimationFrame');
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    test('start() installs frame callback', () => {
-      sandbox.stub(instance, '_detect');
-      instance._requestAnimationFrame.callsArg(0);
-      instance.start();
-      assert.isTrue(instance._detect.calledOnce);
-    });
-
-    test('measures fps', () => {
-      instance._detect(10);
-      instance._detect(30);
-      assert.equal(instance.fps, 50);
-    });
-
-    test('detects jank', () => {
-      let now = 10;
-      instance._detect(now);
-      const fastFrame = () => instance._detect(now += 20);
-      const slowFrame = () => instance._detect(now += 300);
-      fastFrame();
-      assert.equal(instance.jank, 0);
-      _.times(4, slowFrame);
-      assert.equal(instance.jank, 0);
-      instance._requestAnimationFrame.reset();
-      slowFrame();
-      assert.equal(instance.jank, 1);
-      assert.isFalse(instance._requestAnimationFrame.called);
-      clock.tick(1000);
-      assert.isTrue(instance._requestAnimationFrame.called);
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index 6588df4..0ba8a22 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -18,6 +18,5 @@
 <link rel="import" href="/bower_components/polymer/polymer.html">
 
 <dom-module id="gr-reporting">
-  <script src="gr-jank-detector.js"></script>
   <script src="gr-reporting.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 2bd4a620..9ff425c 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -49,14 +49,6 @@
     STARTED_HIDDEN: 'hidden',
   };
 
-  // Frame rate related constants.
-  const JANK = {
-    TYPE: 'lifecycle',
-    CATEGORY: 'UI Latency',
-    // Reported events - alphabetize below.
-    COUNT: 'Jank count',
-  };
-
   // Navigation reporting constants.
   const NAVIGATION = {
     TYPE: 'nav-report',
@@ -150,8 +142,6 @@
   };
   catchErrors();
 
-  GrJankDetector.start();
-
   // The Polymer pass of JSCompiler requires this to be reassignable
   // eslint-disable-next-line prefer-const
   let GrReporting = Polymer({
@@ -291,11 +281,6 @@
     },
 
     beforeLocationChanged() {
-      if (GrJankDetector.jank > 0) {
-        this.reporter(
-            JANK.TYPE, JANK.CATEGORY, JANK.COUNT, GrJankDetector.jank);
-        GrJankDetector.jank = 0;
-      }
       for (const prop of Object.keys(this._baselines)) {
         delete this._baselines[prop];
       }
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 805cef0..ec24b19 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -95,11 +95,7 @@
     test('beforeLocationChanged', () => {
       element._baselines['garbage'] = 'monster';
       sandbox.stub(element, 'time');
-      GrJankDetector.jank = 42;
       element.beforeLocationChanged();
-      assert.equal(GrJankDetector.jank, 0);
-      assert.isTrue(element.reporter.calledWithExactly(
-          'lifecycle', 'UI Latency', 'Jank count', 42));
       assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
       assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
       assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index ff7593b..9cd0527 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -163,6 +163,7 @@
     _handleCopy(e) {
       // Let the browser handle the copy event for polymer 2
       // as selection across shadow DOM will be hard to process
+      // If you remove the following line, please remove it from tests also.
       if (window.POLYMER2) return;
 
       let commentSelected = false;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 0f5c6dd..a2696c2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -189,12 +189,18 @@
     });
 
     test('ignores copy for non-content Element', () => {
+      // See _handleCopy for explanation
+      if (window.POLYMER2) return;
+
       sandbox.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('.not-diff-row'));
       assert.isFalse(element._getSelectedText.called);
     });
 
     test('asks for text for left side Elements', () => {
+      // See _handleCopy for explanation
+      if (window.POLYMER2) return;
+
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
       sandbox.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('div.contentText'));
@@ -202,12 +208,18 @@
     });
 
     test('reacts to copy for content Elements', () => {
+      // See _handleCopy for explanation
+      if (window.POLYMER2) return;
+
       sandbox.stub(element, '_getSelectedText');
       emulateCopyOn(element.querySelector('div.contentText'));
       assert.isTrue(element._getSelectedText.called);
     });
 
     test('copy event is prevented for content Elements', () => {
+      // See _handleCopy for explanation
+      if (window.POLYMER2) return;
+
       sandbox.stub(element, '_getSelectedText');
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
       element._getSelectedText.returns('test');
@@ -216,6 +228,9 @@
     });
 
     test('inserts text into clipboard on copy', () => {
+      // See _handleCopy for explanation
+      if (window.POLYMER2) return;
+
       sandbox.stub(element, '_getSelectedText').returns('the text');
       const event = emulateCopyOn(element.querySelector('div.contentText'));
       assert.deepEqual(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index b30cc5c..2b12932 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -333,9 +333,9 @@
         user-select: text;
       }
 
-      /** Make comments selectable */
-      .selected-left ::slotted(gr-comment-thread[comment-side=left]),
-      .selected-right ::slotted(gr-comment-thread[comment-side=right]) {
+      /** Make comments selectable when selected */
+      .selected-left.selected-comment ::slotted(gr-comment-thread[comment-side=left]),
+      .selected-right.selected-comment ::slotted(gr-comment-thread[comment-side=right]) {
         -webkit-user-select: text;
         -moz-user-select: text;
         -ms-user-select: text;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index ece15bc..35b78ca 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -168,7 +168,11 @@
         // just make two separate queries.
         dialog.querySelectorAll('gr-autocomplete')
             .forEach(input => { input.text = ''; });
-        dialog.querySelectorAll('input')
+
+        // TODO: reveiw binding for input after drop Polymer 1 support
+        // All docs related to Polymer 2 set binding only for iron-input,
+        // and doesn't add binding to input.
+        dialog.querySelectorAll(window.POLYMER2 ? 'iron-input' : 'input')
             .forEach(input => { input.bindValue = ''; });
       }
 
diff --git a/polygerrit-ui/app/elements/gr-app-element.html b/polygerrit-ui/app/elements/gr-app-element.html
index 5c297e7..193f861 100644
--- a/polygerrit-ui/app/elements/gr-app-element.html
+++ b/polygerrit-ui/app/elements/gr-app-element.html
@@ -198,12 +198,13 @@
         <gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator>
       </div>
       <div>
-        <a class="feedback"
-            href$="[[_feedbackUrl]]"
-            rel="noopener"
-            target="_blank"
-            hidden$="[[!_showFeedbackUrl(_feedbackUrl)]]">Report bug</a>
-        | Press &ldquo;?&rdquo; for keyboard shortcuts
+        <template is="dom-if" if="[[_feedbackUrl]]">
+          <a class="feedback"
+              href$="[[_feedbackUrl]]"
+              rel="noopener"
+              target="_blank">Report bug</a> |
+        </template>
+        Press &ldquo;?&rdquo; for keyboard shortcuts
         <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
       </div>
     </footer>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 3c146f8..5692969 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -435,7 +435,9 @@
       if (window.VERSION_INFO) {
         console.log(`UI Version Info: ${window.VERSION_INFO}`);
       }
-      console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+      if (this._feedbackUrl) {
+        console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+      }
       console.groupEnd();
     },
 
@@ -453,14 +455,6 @@
       this.mobileSearch = !this.mobileSearch;
     },
 
-    _showFeedbackUrl(feedbackUrl) {
-      if (feedbackUrl) {
-        return feedbackUrl;
-      }
-
-      return false;
-    },
-
     getThemeEndpoint() {
       // For now, we only have dark mode and light mode
       return window.localStorage.getItem('dark-theme') ?
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index fa097ac..0883707 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -88,7 +88,7 @@
     test('decoration', () => {
       const element =
           container.querySelector('gr-endpoint-decorator[name="first"]');
-      const modules = Polymer.dom(element.root).children.filter(
+      const modules = Array.from(Polymer.dom(element.root).children).filter(
           element => element.nodeName === 'SOME-MODULE');
       assert.equal(modules.length, 1);
       const [module] = modules;
@@ -105,7 +105,7 @@
     test('replacement', () => {
       const element =
           container.querySelector('gr-endpoint-decorator[name="second"]');
-      const module = Polymer.dom(element.root).children.find(
+      const module = Array.from(Polymer.dom(element.root).children).find(
           element => element.nodeName === 'OTHER-MODULE');
       assert.isOk(module);
       assert.equal(module['someparam'], 'foofoo');
@@ -122,7 +122,7 @@
       flush(() => {
         const element =
             container.querySelector('gr-endpoint-decorator[name="banana"]');
-        const module = Polymer.dom(element.root).children.find(
+        const module = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'NOOB-NOOB');
         assert.isOk(module);
         done();
@@ -135,10 +135,10 @@
       flush(() => {
         const element =
             container.querySelector('gr-endpoint-decorator[name="banana"]');
-        const module1 = Polymer.dom(element.root).children.find(
+        const module1 = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'MOD-ONE');
         assert.isOk(module1);
-        const module2 = Polymer.dom(element.root).children.find(
+        const module2 = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'MOD-TWO');
         assert.isOk(module2);
         done();
@@ -152,14 +152,14 @@
       param['value'] = undefined;
       plugin.registerCustomComponent('banana', 'noob-noob');
       flush(() => {
-        let module = Polymer.dom(element.root).children.find(
+        let module = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'NOOB-NOOB');
         // Module waits for param to be defined.
         assert.isNotOk(module);
         const value = {abc: 'def'};
         param.value = value;
         flush(() => {
-          module = Polymer.dom(element.root).children.find(
+          module = Array.from(Polymer.dom(element.root).children).find(
               element => element.nodeName === 'NOOB-NOOB');
           assert.isOk(module);
           assert.strictEqual(module['someParam'], value);
@@ -177,7 +177,7 @@
       param.value = value1;
       plugin.registerCustomComponent('banana', 'noob-noob');
       flush(() => {
-        const module = Polymer.dom(element.root).children.find(
+        const module = Array.from(Polymer.dom(element.root).children).find(
             element => element.nodeName === 'NOOB-NOOB');
         assert.strictEqual(module['someParam'], value1);
         param.value = value2;
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index e8b7212..f865e77 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -57,6 +57,7 @@
       // Within <gr-external-style> itself the styles would have no effect.
       const topEl = document.getElementsByTagName('body')[0];
       topEl.insertBefore(cs, topEl.firstChild);
+      Polymer.updateStyles();
     },
 
     _importAndApply() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
index 879b392..1de8283 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
@@ -37,8 +37,11 @@
    * @return {string} Appropriate class name for the element is returned
    */
   GrStyleObject.prototype.getClassName = function(element) {
-    const rootNode = Polymer.Settings.useShadow
+    let rootNode = Polymer.Settings.useShadow
         ? element.getRootNode() : document.body;
+    if (rootNode === document) {
+      rootNode = document.head;
+    }
     if (!rootNode.__pg_js_api_style_tags) {
       rootNode.__pg_js_api_style_tags = {};
     }
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
index f508288..b30bd52 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -68,7 +68,7 @@
                     type="radio"
                     on-change="_handlePreferredChange"
                     name="preferred"
-                    value="[[item.email]]"
+                    bind-value="[[item.email]]"
                     checked$="[[item.preferred]]">
                   <input
                       is="iron-input"
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index e8ecd7a..8d3f2d2 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -51,7 +51,7 @@
 
       element = fixture('basic');
 
-      element.loadData().then(done);
+      element.loadData().then(flush(done));
     });
 
     test('renders', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index ac17521..3c3ece36 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -73,7 +73,8 @@
     teardown(() => { sandbox.restore(); });
 
     test('renders', () => {
-      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+      const rows = Array.from(
+          Polymer.dom(element.root).querySelectorAll('tbody tr'));
 
       assert.equal(rows.length, 3);
 
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
index a663fd2..1277424 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
@@ -71,7 +71,8 @@
     });
 
     test('renders', () => {
-      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+      const rows = Array.from(
+          Polymer.dom(element.root).querySelectorAll('tbody tr'));
 
       assert.equal(rows.length, 2);
 
@@ -84,7 +85,8 @@
     });
 
     test('renders email', () => {
-      const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+      const rows = Array.from(
+          Polymer.dom(element.root).querySelectorAll('tbody tr'));
 
       assert.equal(rows.length, 2);
 
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index 917026a..134e018 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -57,7 +57,7 @@
       MockInteractions.tap(button);
     }
 
-    setup(() => {
+    setup(done => {
       element = fixture('basic');
       menu = [
         {url: '/first/url', name: 'first name', target: '_blank'},
@@ -66,6 +66,7 @@
       ];
       element.set('menuItems', menu);
       Polymer.dom.flush();
+      flush(done);
     });
 
     test('renders', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 4193382..7a238ec 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -189,6 +189,7 @@
       element.$.newProject.value = {id: 'project b'};
       element.$.newProject.setText('project b');
       element.$.newFilter.bindValue = 'filter 1';
+      element.$.newFilter.value = 'filter 1';
 
       element._handleAddProject();
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 9edc9c8..1be55d2 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -253,8 +253,13 @@
       console.warn('received remove event for missing account', toRemove);
     },
 
+    _getNativeInput(paperInput) {
+      // In Polymer 2 inputElement isn't nativeInput anymore
+      return paperInput.$.nativeInput || paperInput.inputElement;
+    },
+
     _handleInputKeydown(e) {
-      const input = e.detail.input.inputElement;
+      const input = this._getNativeInput(e.detail.input);
       if (input.selectionStart !== input.selectionEnd ||
           input.selectionStart !== 0) {
         return;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index 22e3a3d..6f265e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -417,43 +417,51 @@
     });
 
     suite('keyboard interactions', () => {
-      test('backspace at text input start removes last account', () => {
+      test('backspace at text input start removes last account', done => {
         const input = element.$.entry.$.input;
         sandbox.stub(input, '_updateSuggestions');
         sandbox.stub(element, '_computeRemovable').returns(true);
-        // Next line is a workaround for Firefix not moving cursor
-        // on input field update
-        assert.equal(input.$.input.inputElement.selectionStart, 0);
-        input.text = 'test';
-        MockInteractions.focus(input.$.input);
-        flushAsynchronousOperations();
-        assert.equal(element.accounts.length, 2);
-        MockInteractions.pressAndReleaseKeyOn(
-            input.$.input.inputElement, 8); // Backspace
-        assert.equal(element.accounts.length, 2);
-        input.text = '';
-        MockInteractions.pressAndReleaseKeyOn(
-            input.$.input.inputElement, 8); // Backspace
-        assert.equal(element.accounts.length, 1);
+        flush(() => {
+          // Next line is a workaround for Firefix not moving cursor
+          // on input field update
+          assert.equal(
+              element._getNativeInput(input.$.input).selectionStart, 0);
+          input.text = 'test';
+          MockInteractions.focus(input.$.input);
+          flushAsynchronousOperations();
+          assert.equal(element.accounts.length, 2);
+          MockInteractions.pressAndReleaseKeyOn(
+              element._getNativeInput(input.$.input), 8); // Backspace
+          assert.equal(element.accounts.length, 2);
+          input.text = '';
+          MockInteractions.pressAndReleaseKeyOn(
+              element._getNativeInput(input.$.input), 8); // Backspace
+          flushAsynchronousOperations();
+          assert.equal(element.accounts.length, 1);
+          done();
+        });
       });
 
-      test('arrow key navigation', () => {
+      test('arrow key navigation', done => {
         const input = element.$.entry.$.input;
         input.text = '';
         element.accounts = [makeAccount(), makeAccount()];
-        MockInteractions.focus(input.$.input);
-        flushAsynchronousOperations();
-        const chips = element.accountChips;
-        const chipsOneSpy = sandbox.spy(chips[1], 'focus');
-        MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
-        assert.isTrue(chipsOneSpy.called);
-        const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
-        MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
-        assert.isTrue(chipsZeroSpy.called);
-        MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
-        assert.isTrue(chipsZeroSpy.calledOnce);
-        MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
-        assert.isTrue(chipsOneSpy.calledTwice);
+        flush(() => {
+          MockInteractions.focus(input.$.input);
+          flushAsynchronousOperations();
+          const chips = element.accountChips;
+          const chipsOneSpy = sandbox.spy(chips[1], 'focus');
+          MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
+          assert.isTrue(chipsOneSpy.called);
+          const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
+          MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
+          assert.isTrue(chipsZeroSpy.called);
+          MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
+          assert.isTrue(chipsZeroSpy.calledOnce);
+          MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
+          assert.isTrue(chipsOneSpy.calledTwice);
+          done();
+        });
       });
 
       test('delete', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index af31473..745cff1 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -177,6 +177,11 @@
       '_updateSuggestions(text, threshold, noDebounce)',
     ],
 
+    get _nativeInput() {
+      // In Polymer 2 inputElement isn't nativeInput anymore
+      return this.$.input.$.nativeInput || this.$.input.inputElement;
+    },
+
     attached() {
       this.listen(document.body, 'tap', '_handleBodyTap');
     },
@@ -195,7 +200,7 @@
     },
 
     selectAll() {
-      const nativeInputElement = this.$.input.inputElement;
+      const nativeInputElement = this._nativeInput;
       if (!this.$.input.value) { return; }
       nativeInputElement.setSelectionRange(0, this.$.input.value.length);
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index cff35b4..ea1fd50 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -82,16 +82,19 @@
       });
     });
 
-    test('selectAll', () => {
-      const nativeInput = element.$.input.inputElement;
-      const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
+    test('selectAll', done => {
+      flush(() => {
+        const nativeInput = element._nativeInput;
+        const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
 
-      element.selectAll();
-      assert.isFalse(selectionStub.called);
+        element.selectAll();
+        assert.isFalse(selectionStub.called);
 
-      element.$.input.value = 'test';
-      element.selectAll();
-      assert.isTrue(selectionStub.called);
+        element.$.input.value = 'test';
+        element.selectAll();
+        assert.isTrue(selectionStub.called);
+        done();
+      });
     });
 
     test('esc key behavior', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 42d7678..1c2afaa 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -129,14 +129,31 @@
             element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
       });
     });
+  });
+
+  suite('plugin has avatars', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-avatar', {
+        _getConfig: () => {
+          return Promise.resolve({plugin: {has_avatars: true}});
+        },
+      });
+
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
 
     test('dom for non available account', () => {
       assert.isFalse(element.hasAttribute('hidden'));
 
-      sandbox.stub(element, '_getConfig', () => {
-        return Promise.resolve({plugin: {has_avatars: true}});
-      });
-
       // Emulate plugins loaded.
       Gerrit._setPluginsPending([]);
 
@@ -149,45 +166,45 @@
         assert.strictEqual(element.style.backgroundImage, '');
       });
     });
+  });
 
-    test('avatar config not set and account not set', () => {
-      assert.isFalse(element.hasAttribute('hidden'));
+  suite('config not set', () => {
+    let element;
+    let sandbox;
 
-      sandbox.stub(element, '_getConfig', () => {
-        return Promise.resolve({});
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+
+      stub('gr-avatar', {
+        _getConfig: () => {
+          return Promise.resolve({});
+        },
       });
 
-      // Emulate plugins loaded.
-      Gerrit._setPluginsPending([]);
-
-      return Promise.all([
-        element.$.restAPI.getConfig(),
-        Gerrit.awaitPluginsLoaded(),
-      ]).then(() => {
-        assert.isTrue(element.hasAttribute('hidden'));
-      });
+      element = fixture('basic');
     });
 
-    test('avatar config not set and account set', () => {
-      assert.isFalse(element.hasAttribute('hidden'));
+    teardown(() => {
+      sandbox.restore();
+    });
 
-      sandbox.stub(element, '_getConfig', () => {
-        return Promise.resolve({});
-      });
+    test('avatar hidden when account set', () => {
+      flush(() => {
+        assert.isFalse(element.hasAttribute('hidden'));
 
-      element.imageSize = 64;
-      element.account = {
-        _account_id: 123,
-      };
+        element.imageSize = 64;
+        element.account = {
+          _account_id: 123,
+        };
+        // Emulate plugins loaded.
+        Gerrit._setPluginsPending([]);
 
-      // Emulate plugins loaded.
-      Gerrit._setPluginsPending([]);
-
-      return Promise.all([
-        element.$.restAPI.getConfig(),
-        Gerrit.awaitPluginsLoaded(),
-      ]).then(() => {
-        assert.isTrue(element.hasAttribute('hidden'));
+        return Promise.all([
+          element.$.restAPI.getConfig(),
+          Gerrit.awaitPluginsLoaded(),
+        ]).then(() => {
+          assert.isTrue(element.hasAttribute('hidden'));
+        });
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 76ff442..ad5c243 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -84,6 +84,24 @@
         padding: var(--padding, 4px 8px);
         @apply --gr-button;
       }
+      /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
+      /* BEGIN: Copy from paper-button */
+      paper-button[elevation="1"] {
+        @apply --paper-material-elevation-1;
+      }
+      paper-button[elevation="2"] {
+        @apply --paper-material-elevation-2;
+      }
+      paper-button[elevation="3"] {
+        @apply --paper-material-elevation-3;
+      }
+      paper-button[elevation="4"] {
+        @apply --paper-material-elevation-4;
+      }
+      paper-button[elevation="5"] {
+        @apply --paper-material-elevation-5;
+      }
+      /* END: Copy from paper-button */
       paper-button:hover {
         background: linear-gradient(
           rgba(0, 0, 0, .12),
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
index c092b7c..574dc1e 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -39,12 +39,13 @@
     let element;
     let sandbox;
 
-    setup(() => {
+    setup(done => {
       sandbox = sinon.sandbox.create();
       element = fixture('basic');
       element.text = `git fetch http://gerrit@localhost:8080/a/test-project
           refs/changes/05/5/1 && git checkout FETCH_HEAD`;
       flushAsynchronousOperations();
+      flush(done);
     });
 
     teardown(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
index 3b7e8f8..85a5b1f 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -67,12 +67,13 @@
     });
 
     suite('unauthenticated', () => {
-      setup(() => {
+      setup(done => {
         element = fixture('basic');
         element.schemes = SCHEMES;
         element.commands = COMMANDS;
         element.selectedScheme = SELECTED_SCHEME;
         flushAsynchronousOperations();
+        flush(done);
       });
 
       test('focusOnCopy', () => {
@@ -91,13 +92,13 @@
         assert.isTrue(isHidden(element.$$('.commands')));
       });
 
-      test('tab selection', () => {
+      test('tab selection', done => {
         assert.equal(element.$.downloadTabs.selected, '0');
         MockInteractions.tap(element.$$('[data-scheme="ssh"]'));
         flushAsynchronousOperations();
-
         assert.equal(element.selectedScheme, 'ssh');
         assert.equal(element.$.downloadTabs.selected, '2');
+        done();
       });
 
       test('loads scheme from preferences', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
index 5f07fc9..312cc34 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -94,77 +94,79 @@
       ];
       assert.equal(element.$$('paper-listbox').selected, element.value);
       assert.equal(element.text, 'Button Text 2');
-      flushAsynchronousOperations();
-      const items = Polymer.dom(element.root).querySelectorAll('paper-item');
-      const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
-      assert.equal(items.length, 3);
-      assert.equal(mobileItems.length, 3);
+      flush(() => {
+        const items = Polymer.dom(element.root).querySelectorAll('paper-item');
+        const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
+        assert.equal(items.length, 3);
+        assert.equal(mobileItems.length, 3);
 
-      // First Item
-      // The first item should be disabled, has no bottom text, and no date.
-      assert.isFalse(!!items[0].disabled);
-      assert.isFalse(mobileItems[0].disabled);
-      assert.isFalse(items[0].classList.contains('iron-selected'));
-      assert.isFalse(mobileItems[0].selected);
+        // First Item
+        // The first item should be disabled, has no bottom text, and no date.
+        assert.isFalse(!!items[0].disabled);
+        assert.isFalse(mobileItems[0].disabled);
+        assert.isFalse(items[0].classList.contains('iron-selected'));
+        assert.isFalse(mobileItems[0].selected);
 
-      assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
-      assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
-      assert.equal(items[0].value, element.items[0].value);
-      assert.equal(mobileItems[0].value, element.items[0].value);
-      assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
-          .innerText, element.items[0].text);
+        assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
+        assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
+        assert.equal(items[0].value, element.items[0].value);
+        assert.equal(mobileItems[0].value, element.items[0].value);
+        assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
+            .innerText, element.items[0].text);
 
-      // Since no mobile specific text, it should fall back to text.
-      assert.equal(mobileItems[0].text, element.items[0].text);
+        // Since no mobile specific text, it should fall back to text.
+        assert.equal(mobileItems[0].text, element.items[0].text);
 
 
-      // Second Item
-      // The second item should have top text, bottom text, and no date.
-      assert.isFalse(!!items[1].disabled);
-      assert.isFalse(mobileItems[1].disabled);
-      assert.isTrue(items[1].classList.contains('iron-selected'));
-      assert.isTrue(mobileItems[1].selected);
+        // Second Item
+        // The second item should have top text, bottom text, and no date.
+        assert.isFalse(!!items[1].disabled);
+        assert.isFalse(mobileItems[1].disabled);
+        assert.isTrue(items[1].classList.contains('iron-selected'));
+        assert.isTrue(mobileItems[1].selected);
 
-      assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
-      assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
-      assert.equal(items[1].value, element.items[1].value);
-      assert.equal(mobileItems[1].value, element.items[1].value);
-      assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
-          .innerText, element.items[1].text);
+        assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
+        assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
+        assert.equal(items[1].value, element.items[1].value);
+        assert.equal(mobileItems[1].value, element.items[1].value);
+        assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
+            .innerText, element.items[1].text);
 
-      // Since there is mobile specific text, it should that.
-      assert.equal(mobileItems[1].text, element.items[1].mobileText);
+        // Since there is mobile specific text, it should that.
+        assert.equal(mobileItems[1].text, element.items[1].mobileText);
 
-      // Since this item is selected, and it has triggerText defined, that
-      // should be used.
-      assert.equal(element.text, element.items[1].triggerText);
+        // Since this item is selected, and it has triggerText defined, that
+        // should be used.
+        assert.equal(element.text, element.items[1].triggerText);
 
-      // Third item
-      // The third item should be disabled, and have a date, and bottom content.
-      assert.isTrue(!!items[2].disabled);
-      assert.isTrue(mobileItems[2].disabled);
-      assert.isFalse(items[2].classList.contains('iron-selected'));
-      assert.isFalse(mobileItems[2].selected);
+        // Third item
+        // The third item should be disabled, and have a date, and bottom content.
+        assert.isTrue(!!items[2].disabled);
+        assert.isTrue(mobileItems[2].disabled);
+        assert.isFalse(items[2].classList.contains('iron-selected'));
+        assert.isFalse(mobileItems[2].selected);
 
-      assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
-      assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
-      assert.equal(items[2].value, element.items[2].value);
-      assert.equal(mobileItems[2].value, element.items[2].value);
-      assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
-          .innerText, element.items[2].text);
+        assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
+        assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
+        assert.equal(items[2].value, element.items[2].value);
+        assert.equal(mobileItems[2].value, element.items[2].value);
+        assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
+            .innerText, element.items[2].text);
 
-      // Since there is mobile specific text, it should that.
-      assert.equal(mobileItems[2].text, element.items[2].mobileText);
+        // Since there is mobile specific text, it should that.
+        assert.equal(mobileItems[2].text, element.items[2].mobileText);
 
-      // Select a new item.
-      MockInteractions.tap(items[0]);
-      flushAsynchronousOperations();
-      assert.equal(element.value, 1);
-      assert.isTrue(items[0].classList.contains('iron-selected'));
-      assert.isTrue(mobileItems[0].selected);
+        // Select a new item.
+        MockInteractions.tap(items[0]);
+        flushAsynchronousOperations();
+        assert.equal(element.value, 1);
+        assert.isTrue(items[0].classList.contains('iron-selected'));
+        assert.isTrue(mobileItems[0].selected);
 
-      // Since no triggerText, the fallback is used.
-      assert.equal(element.text, element.items[0].text);
+        // Since no triggerText, the fallback is used.
+        assert.equal(element.text, element.items[0].text);
+        done();
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index 5dad9a6..7ff0a14 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -61,13 +61,17 @@
     let label;
     let sandbox;
 
-    setup(() => {
+    setup(done => {
       element = fixture('basic');
       elementNoPlaceholder = fixture('no-placeholder');
 
-      input = element.$.input.$.input;
       label = element.$$('label');
       sandbox = sinon.sandbox.create();
+      flush(() => {
+        // In Polymer 2 inputElement isn't nativeInput anymore
+        input = element.$.input.$.nativeInput || element.$.input.inputElement;
+        done();
+      });
     });
 
     teardown(() => {
@@ -79,7 +83,7 @@
       assert.isFalse(element.$.dropdown.opened);
       assert.isTrue(label.classList.contains('editable'));
       assert.equal(label.textContent, 'value text');
-      const focusSpy = sandbox.spy(element.$.input.$.input, 'focus');
+      const focusSpy = sandbox.spy(input, 'focus');
       const showSpy = sandbox.spy(element, '_showDropdown');
 
       MockInteractions.tap(label);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
index 2a6487e..3223636 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -56,6 +56,7 @@
       el.setAttribute('data-side', 'right');
       lineNumberEl = document.createElement('td');
       lineNumberEl.classList.add('right');
+      document.body.appendChild(el);
       instance = new GrAnnotationActionsContext(
           el, lineNumberEl, line, 'dummy/path', '123', '1');
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index ecf542f..333bede 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -66,7 +66,7 @@
 
     ready() {
       // If not set via the property, set bind-value to the element value.
-      if (this.bindValue == undefined) {
+      if (this.bindValue == undefined && this.nativeSelect.options.length > 0) {
         this.bindValue = this.nativeSelect.value;
       }
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
index 66ebb79..b3abe5f 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -40,6 +40,15 @@
   </template>
 </test-fixture>
 
+<test-fixture id="noOptions">
+  <template>
+    <gr-select>
+      <select>
+      </select>
+    </gr-select>
+  </template>
+</test-fixture>
+
 <script>
   suite('gr-select tests', () => {
     let element;
@@ -48,6 +57,10 @@
       element = fixture('basic');
     });
 
+    test('bindValue must be set to the first option value', () => {
+      assert.equal(element.bindValue, '1');
+    });
+
     test('value of 0 should still trigger value updates', () => {
       element.bindValue = 0;
       assert.equal(element.nativeSelect.value, 0);
@@ -90,4 +103,16 @@
       assert.isTrue(changeStub.called);
     });
   });
+
+  suite('gr-select no options tests', () => {
+    let element;
+
+    setup(() => {
+      element = fixture('noOptions');
+    });
+
+    test('bindValue must not be changed', () => {
+      assert.isUndefined(element.bindValue);
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 8b6eff2..077f4b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -33,6 +33,18 @@
   </template>
 </test-fixture>
 
+<test-fixture id="monospace">
+  <template>
+    <gr-textarea monospace="true"></gr-textarea>
+  </template>
+</test-fixture>
+
+<test-fixture id="hideBorder">
+  <template>
+    <gr-textarea hide-border="true"></gr-textarea>
+  </template>
+</test-fixture>
+
 <script>
   suite('gr-textarea tests', () => {
     let element;
@@ -49,16 +61,10 @@
 
     test('monospace is set properly', () => {
       assert.isFalse(element.classList.contains('monospace'));
-      element.monospace = true;
-      element.ready();
-      assert.isTrue(element.classList.contains('monospace'));
     });
 
     test('hideBorder is set properly', () => {
       assert.isFalse(element.$.textarea.classList.contains('noBorder'));
-      element.hideBorder = true;
-      element.ready();
-      assert.isTrue(element.$.textarea.classList.contains('noBorder'));
     });
 
     test('emoji selector is not open with the textarea lacks focus', () => {
@@ -235,4 +241,52 @@
       });
     });
   });
+
+  suite('gr-textarea monospace', () => {
+    // gr-textarea set monospace class in the ready() method.
+    // In Polymer2, ready() is called from the fixture(...) method,
+    // If ready() is called again later, some nested elements doesn't
+    // handle it correctly. A separate test-fixture is used to set
+    // properties before ready() is called.
+
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('monospace');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('monospace is set properly', () => {
+      assert.isTrue(element.classList.contains('monospace'));
+    });
+  });
+
+  suite('gr-textarea hideBorder', () => {
+    // gr-textarea set noBorder class in the ready() method.
+    // In Polymer2, ready() is called from the fixture(...) method,
+    // If ready() is called again later, some nested elements doesn't
+    // handle it correctly. A separate test-fixture is used to set
+    // properties before ready() is called.
+
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('hideBorder');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('hideBorder is set properly', () => {
+      assert.isTrue(element.$.textarea.classList.contains('noBorder'));
+    });
+  });
 </script>
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index a6ada82..672c43f 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -95,5 +95,38 @@
     return style;
   };
 
+  /**
+   * Query selector on a dom element.
+   *
+   * This is shadow DOM compatible, but only works when selector is within
+   * one shadow host, won't work if your selector is crossing
+   * multiple shadow hosts.
+   *
+   */
+  util.querySelector = (el, selector) => {
+    let nodes = [el];
+    let element = null;
+    while (nodes.length) {
+      const node = nodes.pop();
+
+      // Skip if it's an invalid node.
+      if (!node || !node.querySelector) continue;
+
+      // Try find it with native querySelector directly
+      element = node.querySelector(selector);
+
+      if (element) {
+        break;
+      } else if (node.shadowRoot) {
+        // If shadowHost detected, add the host and its children
+        nodes = nodes.concat(Array.from(node.children));
+        nodes.push(node.shadowRoot);
+      } else {
+        nodes = nodes.concat(Array.from(node.children));
+      }
+    }
+    return element;
+  };
+
   window.util = util;
 })(window);
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index f5a64dc..75380dd 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -100,7 +100,6 @@
     'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html',
     'core/gr-main-header/gr-main-header_test.html',
     'core/gr-navigation/gr-navigation_test.html',
-    'core/gr-reporting/gr-jank-detector_test.html',
     'core/gr-reporting/gr-reporting_test.html',
     'core/gr-router/gr-router_test.html',
     'core/gr-search-bar/gr-search-bar_test.html',
diff --git a/proto/cache.proto b/proto/cache.proto
index 77b6908..7e6abcc 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -186,6 +186,9 @@
 
   // Number of updates to the change's meta ref.
   int32 update_count = 19;
+
+  string server_id = 20;
+  bool has_server_id = 21;
 }
 
 
diff --git a/tools/BUILD b/tools/BUILD
index 5531c3e..208a7fcb 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -15,6 +15,19 @@
     visibility = ["//visibility:public"],
 )
 
+# TODO(davido): Remove this workaround and switch to using toolchain_vanilla
+# from this Bazel package again: @bazel_tools//tools/jdk:toolchain_vanilla,
+# when this issue is fixed: https://github.com/bazelbuild/bazel/issues/9415.
+default_java_toolchain(
+    name = "toolchain_vanilla",
+    forcibly_disable_header_compilation = True,
+    javabuilder = ["@bazel_tools//tools/jdk:vanillajavabuilder"],
+    jvm_opts = [],
+    source_version = "",
+    target_version = "",
+    visibility = ["//visibility:public"],
+)
+
 default_java_toolchain(
     name = "error_prone_warnings_toolchain",
     bootclasspath = ["@bazel_tools//tools/jdk:platformclasspath.jar"],