Merge "Fix line number padding and size"
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index b6e6d46..2afdad99 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1277,7 +1277,7 @@
   }
 
   protected void assertNotifyTo(String expectedEmail, String expectedFullname) {
-    Address expectedAddress = new Address(expectedFullname, expectedEmail);
+    Address expectedAddress = Address.create(expectedFullname, expectedEmail);
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
     assertThat(m.rcpt()).containsExactly(expectedAddress);
@@ -1291,7 +1291,7 @@
   }
 
   protected void assertNotifyCc(String expectedEmail, String expectedFullname) {
-    Address expectedAddress = new Address(expectedFullname, expectedEmail);
+    Address expectedAddress = Address.create(expectedFullname, expectedEmail);
     assertNotifyCc(expectedAddress);
   }
 
@@ -1315,7 +1315,7 @@
   protected void assertNotifyBcc(String expectedEmail, String expectedFullName) {
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(new Address(expectedFullName, expectedEmail));
+    assertThat(m.rcpt()).containsExactly(Address.create(expectedFullName, expectedEmail));
     assertThat(m.headers().get("To").isEmpty()).isTrue();
     assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 4322f63..bb3901e 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -126,7 +126,7 @@
       recipients.put(
           BCC,
           message.rcpt().stream()
-              .map(Address::getEmail)
+              .map(Address::email)
               .filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e))
               .collect(toList()));
       this.users = users;
@@ -174,7 +174,7 @@
       }
       Truth.assertThat(header).isInstanceOf(AddressList.class);
       AddressList addrList = (AddressList) header;
-      return addrList.getAddressList().stream().map(Address::getEmail).collect(toList());
+      return addrList.getAddressList().stream().map(Address::email).collect(toList());
     }
 
     public FakeEmailSenderSubject to(String... emails) {
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index 87ce70a..a7a4a89 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -92,6 +92,6 @@
     //    emailAddress().
     //  * Address#equals only considers email, not name, whereas TestAccount#equals should include
     //    name.
-    return new Address(fullName(), email());
+    return Address.create(fullName(), email());
   }
 }
diff --git a/java/com/google/gerrit/mail/Address.java b/java/com/google/gerrit/mail/Address.java
index 455cc90..520a4c8 100644
--- a/java/com/google/gerrit/mail/Address.java
+++ b/java/com/google/gerrit/mail/Address.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.mail;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
 
 /** Represents an address (name + email) in an email message. */
-public class Address {
+@AutoValue
+public abstract class Address {
   public static Address parse(String in) {
     final int lt = in.indexOf('<');
     final int gt = in.indexOf('>');
@@ -33,11 +35,11 @@
       if (name.endsWith("\"")) {
         nameEnd--;
       }
-      return new Address(name.length() > 0 ? name.substring(nameStart, nameEnd) : null, email);
+      return Address.create(name.length() > 0 ? name.substring(nameStart, nameEnd) : null, email);
     }
 
     if (lt < 0 && gt < 0 && 0 < at && at < in.length() - 1) {
-      return new Address(in);
+      return Address.create(in);
     }
 
     throw new IllegalArgumentException("Invalid email address: " + in);
@@ -51,60 +53,52 @@
     }
   }
 
-  @Nullable private final String name;
-  private final String email;
-
-  public Address(String email) {
-    this(null, email);
+  public static Address create(String email) {
+    return create(null, email);
   }
 
-  public Address(String name, String email) {
-    this.name = name;
-    this.email = email;
+  public static Address create(String name, String email) {
+    return new AutoValue_Address(name, email);
   }
 
   @Nullable
-  public String getName() {
-    return name;
-  }
+  public abstract String name();
 
-  public String getEmail() {
-    return email;
+  public abstract String email();
+
+  @Override
+  public final int hashCode() {
+    return email().hashCode();
   }
 
   @Override
-  public int hashCode() {
-    return email.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object other) {
+  public final boolean equals(Object other) {
     if (other instanceof Address) {
-      return email.equals(((Address) other).email);
+      return email().equals(((Address) other).email());
     }
     return false;
   }
 
   @Override
-  public String toString() {
+  public final String toString() {
     return toHeaderString();
   }
 
   public String toHeaderString() {
-    if (name != null) {
-      return quotedPhrase(name) + " <" + email + ">";
+    if (name() != null) {
+      return quotedPhrase(name()) + " <" + email() + ">";
     } else if (isSimple()) {
-      return email;
+      return email();
     }
-    return "<" + email + ">";
+    return "<" + email() + ">";
   }
 
   private static final String MUST_QUOTE_EMAIL = "()<>,;:\\\"[]";
   private static final String MUST_QUOTE_NAME = MUST_QUOTE_EMAIL + "@.";
 
   private boolean isSimple() {
-    for (int i = 0; i < email.length(); i++) {
-      final char c = email.charAt(i);
+    for (int i = 0; i < email().length(); i++) {
+      final char c = email().charAt(i);
       if (c <= ' ' || 0x7F <= c || MUST_QUOTE_EMAIL.indexOf(c) != -1) {
         return false;
       }
diff --git a/java/com/google/gerrit/mail/EmailHeader.java b/java/com/google/gerrit/mail/EmailHeader.java
index 69d5fcd..9b11101 100644
--- a/java/com/google/gerrit/mail/EmailHeader.java
+++ b/java/com/google/gerrit/mail/EmailHeader.java
@@ -183,7 +183,7 @@
     }
 
     public void remove(java.lang.String email) {
-      list.removeIf(address -> address.getEmail().equals(email));
+      list.removeIf(address -> address.email().equals(email));
     }
 
     @Override
diff --git a/java/com/google/gerrit/mail/MailHeaderParser.java b/java/com/google/gerrit/mail/MailHeaderParser.java
index a4a6a03..43b1e31 100644
--- a/java/com/google/gerrit/mail/MailHeaderParser.java
+++ b/java/com/google/gerrit/mail/MailHeaderParser.java
@@ -29,7 +29,7 @@
   public static MailMetadata parse(MailMessage m) {
     MailMetadata metadata = new MailMetadata();
     // Find author
-    metadata.author = m.from().getEmail();
+    metadata.author = m.from().email();
 
     // Check email headers for X-Gerrit-<Name>
     for (String header : m.additionalHeaders()) {
diff --git a/java/com/google/gerrit/mail/RawMailParser.java b/java/com/google/gerrit/mail/RawMailParser.java
index 9c89d19..4e005a5 100644
--- a/java/com/google/gerrit/mail/RawMailParser.java
+++ b/java/com/google/gerrit/mail/RawMailParser.java
@@ -71,16 +71,16 @@
     // Add From, To and Cc
     if (mimeMessage.getFrom() != null && !mimeMessage.getFrom().isEmpty()) {
       Mailbox from = mimeMessage.getFrom().get(0);
-      messageBuilder.from(new Address(from.getName(), from.getAddress()));
+      messageBuilder.from(Address.create(from.getName(), from.getAddress()));
     }
     if (mimeMessage.getTo() != null) {
       for (Mailbox m : mimeMessage.getTo().flatten()) {
-        messageBuilder.addTo(new Address(m.getName(), m.getAddress()));
+        messageBuilder.addTo(Address.create(m.getName(), m.getAddress()));
       }
     }
     if (mimeMessage.getCc() != null) {
       for (Mailbox m : mimeMessage.getCc().flatten()) {
-        messageBuilder.addCc(new Address(m.getName(), m.getAddress()));
+        messageBuilder.addCc(Address.create(m.getName(), m.getAddress()));
       }
     }
 
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index b464104..8c4f275 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -786,7 +786,7 @@
 
   private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
     return addresses.stream()
-        .map(a -> new AccountInfo(a.getName(), a.getEmail()))
+        .map(a -> new AccountInfo(a.name(), a.email()))
         .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
         .collect(toList());
   }
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index b55b88d..d9462bf 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -379,7 +379,7 @@
     }
 
     Address adr = Address.tryParse(input.reviewer);
-    if (adr == null || !validator.isValid(adr.getEmail())) {
+    if (adr == null || !validator.isValid(adr.email())) {
       return fail(
           input,
           FailureType.NOT_FOUND,
@@ -481,7 +481,7 @@
         }
         accountLoaderFactory.create(true).fill(result.ccs);
         for (Address a : opResult.addedCCsByEmail()) {
-          result.ccs.add(new AccountInfo(a.getName(), a.getEmail()));
+          result.ccs.add(new AccountInfo(a.name(), a.email()));
         }
       } else {
         result.reviewers = Lists.newArrayListWithCapacity(opResult.addedReviewers().size());
@@ -496,7 +496,7 @@
         }
         accountLoaderFactory.create(true).fill(result.reviewers);
         for (Address a : opResult.addedReviewersByEmail()) {
-          result.reviewers.add(ReviewerInfo.byEmail(a.getName(), a.getEmail()));
+          result.reviewers.add(ReviewerInfo.byEmail(a.name(), a.email()));
         }
       }
     }
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index 6686ed8..39e5f74 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -75,7 +75,7 @@
       ReviewerInfo info;
       if (rsrc.isByEmail()) {
         Address address = rsrc.getReviewerByEmail();
-        info = ReviewerInfo.byEmail(address.getName(), address.getEmail());
+        info = ReviewerInfo.byEmail(address.name(), address.email());
       } else {
         Account.Id reviewerAccountId = rsrc.getReviewerUser().getAccountId();
         info = format(new ReviewerInfo(reviewerAccountId.get()), reviewerAccountId, cd);
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index bfc5135..7b976e8 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
@@ -164,8 +165,12 @@
    *
    * @see #waitFor(Future, long, TimeUnit)
    */
-  public void waitFor(Future<?> workerFuture) throws ExecutionException {
-    waitFor(workerFuture, 0, null);
+  public <T> T waitFor(Future<T> workerFuture) {
+    try {
+      return waitFor(workerFuture, 0, null);
+    } catch (TimeoutException e) {
+      throw new IllegalStateException("timout exception without setting a timeout", e);
+    }
   }
 
   /**
@@ -182,11 +187,10 @@
    * @throws ExecutionException if this thread or a worker thread was interrupted, the worker was
    *     cancelled, or timed out waiting for a worker to call {@link #end()}.
    */
-  public void waitFor(Future<?> workerFuture, long timeoutTime, TimeUnit timeoutUnit)
-      throws ExecutionException {
+  public <T> T waitFor(Future<T> workerFuture, long timeoutTime, TimeUnit timeoutUnit)
+      throws TimeoutException {
     long overallStart = System.nanoTime();
     long deadline;
-    String detailMessage = "";
     if (timeoutTime > 0) {
       deadline = overallStart + NANOSECONDS.convert(timeoutTime, timeoutUnit);
     } else {
@@ -200,7 +204,7 @@
         try {
           NANOSECONDS.timedWait(this, left);
         } catch (InterruptedException e) {
-          throw new ExecutionException(e);
+          throw new UncheckedExecutionException(e);
         }
 
         // Send an update on every wakeup (manual or spurious), but only move
@@ -210,13 +214,10 @@
         if (deadline > 0 && now > deadline) {
           workerFuture.cancel(true);
           if (workerFuture.isCancelled()) {
-            detailMessage =
-                String.format(
-                    "(timeout %sms, cancelled)",
-                    TimeUnit.MILLISECONDS.convert(now - deadline, NANOSECONDS));
             logger.atWarning().log(
-                "MultiProgressMonitor worker killed after %sms: %s",
-                TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS), detailMessage);
+                "MultiProgressMonitor worker killed after %sms: (timeout %sms, cancelled)",
+                TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS),
+                TimeUnit.MILLISECONDS.convert(now - deadline, NANOSECONDS));
           }
           break;
         }
@@ -240,14 +241,15 @@
     // The loop exits as soon as the worker calls end(), but we give it another
     // maxInterval to finish up and return.
     try {
-      workerFuture.get(maxIntervalNanos, NANOSECONDS);
-    } catch (InterruptedException e) {
-      throw new ExecutionException(e);
-    } catch (CancellationException e) {
-      throw new ExecutionException(detailMessage, e);
+      return workerFuture.get(maxIntervalNanos, NANOSECONDS);
+    } catch (InterruptedException | CancellationException e) {
+      logger.atWarning().withCause(e).log("unable to finish processing");
+      throw new UncheckedExecutionException(e);
     } catch (TimeoutException e) {
       workerFuture.cancel(true);
-      throw new ExecutionException(e);
+      throw e;
+    } catch (ExecutionException e) {
+      throw new UncheckedExecutionException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/ProjectRunnable.java b/java/com/google/gerrit/server/git/ProjectRunnable.java
index e74bf2d..76831a2 100644
--- a/java/com/google/gerrit/server/git/ProjectRunnable.java
+++ b/java/com/google/gerrit/server/git/ProjectRunnable.java
@@ -13,13 +13,70 @@
 // limitations under the License.
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
 
 /** Used to retrieve the project name from an operation * */
 public interface ProjectRunnable extends Runnable {
   Project.NameKey getProjectNameKey();
 
+  @Nullable
   String getRemoteName();
 
   boolean hasCustomizedPrint();
+
+  /**
+   * Wraps the callable as a {@link FutureTask} and makes it comply with the {@link ProjectRunnable}
+   * interface.
+   */
+  static <T> FutureTask<T> fromCallable(
+      Callable<T> callable,
+      Project.NameKey projectName,
+      String operationName,
+      @Nullable String remoteHostname,
+      boolean hasCustomPrint) {
+    return new FromCallable<>(callable, projectName, operationName, remoteHostname, hasCustomPrint);
+  }
+
+  class FromCallable<T> extends FutureTask<T> implements ProjectRunnable {
+    private final Project.NameKey project;
+    private final String operationName;
+    private final String remoteHostname;
+    private final boolean hasCustomPrint;
+
+    FromCallable(
+        Callable<T> callable,
+        Project.NameKey project,
+        String operationName,
+        @Nullable String remoteHostname,
+        boolean hasCustomPrint) {
+      super(callable);
+      this.project = project;
+      this.operationName = operationName;
+      this.remoteHostname = remoteHostname;
+      this.hasCustomPrint = hasCustomPrint;
+    }
+
+    @Override
+    public Project.NameKey getProjectNameKey() {
+      return project;
+    }
+
+    @Override
+    public String getRemoteName() {
+      return remoteHostname;
+    }
+
+    @Override
+    public boolean hasCustomizedPrint() {
+      return hasCustomPrint;
+    }
+
+    @Override
+    public String toString() {
+      return operationName;
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 8c20f30..85dc034 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.server.git.receive;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.Capable;
@@ -66,10 +68,13 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Collection;
-import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PreReceiveHook;
@@ -84,7 +89,7 @@
  * of time, it runs in the background so it can be monitored for timeouts and cancelled, and have
  * stalls reported to the user from the main thread.
  */
-public class AsyncReceiveCommits implements PreReceiveHook {
+public class AsyncReceiveCommits {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static final String TIMEOUT_NAME = "ReceiveCommitsOverallTimeout";
@@ -119,74 +124,30 @@
     }
   }
 
-  private class Worker implements ProjectRunnable {
-    final MultiProgressMonitor progress;
-    final String name;
+  private static MultiProgressMonitor newMultiProgressMonitor(MessageSender messageSender) {
+    return new MultiProgressMonitor(
+        new OutputStream() {
+          @Override
+          public void write(int b) {
+            messageSender.sendBytes(new byte[] {(byte) b});
+          }
 
-    private final Collection<ReceiveCommand> commands;
+          @Override
+          public void write(byte[] what, int off, int len) {
+            messageSender.sendBytes(what, off, len);
+          }
 
-    private Worker(Collection<ReceiveCommand> commands, String name) {
-      this.commands = commands;
-      this.name = name;
-      progress = new MultiProgressMonitor(new MessageSenderOutputStream(), "Processing changes");
-    }
+          @Override
+          public void write(byte[] what) {
+            messageSender.sendBytes(what);
+          }
 
-    @Override
-    public void run() {
-      String oldName = Thread.currentThread().getName();
-      Thread.currentThread().setName(oldName + "-for-" + name);
-      try {
-        receiveCommits.processCommands(commands, progress);
-      } finally {
-        Thread.currentThread().setName(oldName);
-      }
-    }
-
-    @Override
-    public Project.NameKey getProjectNameKey() {
-      return receiveCommits.getProject().getNameKey();
-    }
-
-    @Override
-    public String getRemoteName() {
-      return null;
-    }
-
-    @Override
-    public boolean hasCustomizedPrint() {
-      return true;
-    }
-
-    @Override
-    public String toString() {
-      return "receive-commits";
-    }
-
-    void sendMessages() {
-      receiveCommits.sendMessages();
-    }
-
-    private class MessageSenderOutputStream extends OutputStream {
-      @Override
-      public void write(int b) {
-        receiveCommits.getMessageSender().sendBytes(new byte[] {(byte) b});
-      }
-
-      @Override
-      public void write(byte[] what, int off, int len) {
-        receiveCommits.getMessageSender().sendBytes(what, off, len);
-      }
-
-      @Override
-      public void write(byte[] what) {
-        receiveCommits.getMessageSender().sendBytes(what);
-      }
-
-      @Override
-      public void flush() {
-        receiveCommits.getMessageSender().flush();
-      }
-    }
+          @Override
+          public void flush() {
+            messageSender.flush();
+          }
+        },
+        "Processing changes");
   }
 
   private enum PushType {
@@ -245,7 +206,6 @@
 
   private final Metrics metrics;
   private final ReceiveCommits receiveCommits;
-  private final ResultChangeIds resultChangeIds;
   private final PermissionBackend.ForProject perm;
   private final ReceivePack receivePack;
   private final ExecutorService executor;
@@ -303,7 +263,7 @@
     receivePack.setCheckReceivedObjects(projectState.getConfig().getCheckReceivedObjects());
     receivePack.setRefFilter(new ReceiveRefFilter());
     receivePack.setAllowPushOptions(true);
-    receivePack.setPreReceiveHook(this);
+    receivePack.setPreReceiveHook(asHook());
     receivePack.setPostReceiveHook(lazyPostReceive.create(user, projectName));
 
     try {
@@ -323,10 +283,8 @@
             queryProvider,
             projectName,
             user.getAccountId()));
-    resultChangeIds = new ResultChangeIds();
     receiveCommits =
-        factory.create(
-            projectState, user, receivePack, repo, allRefsWatcher, messageSender, resultChangeIds);
+        factory.create(projectState, user, receivePack, repo, allRefsWatcher, messageSender);
     receiveCommits.init();
     QuotaResponse.Aggregated availableTokens =
         quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
@@ -361,48 +319,90 @@
     return Capable.OK;
   }
 
-  @Override
-  public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+  /**
+   * Returns a {@link PreReceiveHook} implementation that can be used directly by JGit when
+   * processing a push.
+   */
+  public PreReceiveHook asHook() {
+    return (rp, commands) -> {
+      checkState(receivePack == rp, "can't perform PreReceive for a different receive pack");
+      long startNanos = System.nanoTime();
+      ReceiveCommitsResult result;
+      try {
+        result = preReceive(commands);
+      } catch (TimeoutException e) {
+        metrics.timeouts.increment();
+        logger.atWarning().withCause(e).log(
+            "Timeout in ReceiveCommits while processing changes for project %s",
+            projectState.getName());
+        receivePack.sendError("timeout while processing changes");
+        rejectCommandsNotAttempted(commands);
+        return;
+      } catch (Exception e) {
+        logger.atSevere().withCause(e.getCause()).log("error while processing push");
+        receivePack.sendError("internal error");
+        rejectCommandsNotAttempted(commands);
+        return;
+      } finally {
+        // Flush the messages queued up until now (if any).
+        receiveCommits.sendMessages();
+      }
+      reportMetrics(result, System.nanoTime() - startNanos);
+    };
+  }
+
+  /** Processes {@code commands}, applies them to Git storage and communicates back on the wire. */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public ReceiveCommitsResult preReceive(Collection<ReceiveCommand> commands)
+      throws TimeoutException, UncheckedExecutionException {
     if (commands.stream().anyMatch(c -> c.getResult() != Result.NOT_ATTEMPTED)) {
       // Stop processing when command was already processed by previously invoked
       // pre-receive hooks
-      return;
+      return ReceiveCommitsResult.empty();
     }
 
-    long startNanos = System.nanoTime();
-    Worker w = new Worker(commands, Thread.currentThread().getName());
+    MultiProgressMonitor monitor = newMultiProgressMonitor(receiveCommits.getMessageSender());
+    Callable<ReceiveCommitsResult> callable =
+        () -> {
+          String oldName = Thread.currentThread().getName();
+          Thread.currentThread().setName(oldName + "-for-" + Thread.currentThread().getName());
+          try {
+            return receiveCommits.processCommands(commands, monitor);
+          } finally {
+            Thread.currentThread().setName(oldName);
+          }
+        };
+
     try {
-      w.progress.waitFor(
-          executor.submit(scopePropagator.wrap(w)), timeoutMillis, TimeUnit.MILLISECONDS);
-    } catch (ExecutionException e) {
-      metrics.timeouts.increment();
-      logger.atWarning().withCause(e).log(
-          "Error in ReceiveCommits while processing changes for project %s",
-          projectState.getName());
-      rp.sendError("internal error while processing changes");
-      // ReceiveCommits has tried its best to catch errors, so anything at this
-      // point is very bad.
-      for (ReceiveCommand c : commands) {
-        if (c.getResult() == Result.NOT_ATTEMPTED) {
-          c.setResult(Result.REJECTED_OTHER_REASON, "internal error");
-        }
+      // WorkQueue does not support Callable<T>, so we have to covert it here.
+      FutureTask<ReceiveCommitsResult> runnable =
+          ProjectRunnable.fromCallable(
+              callable, receiveCommits.getProject().getNameKey(), "receive-commits", null, false);
+      monitor.waitFor(
+          executor.submit(scopePropagator.wrap(runnable)), timeoutMillis, TimeUnit.MILLISECONDS);
+      if (!runnable.isDone()) {
+        // At this point we are either done or have thrown a TimeoutException and bailed out.
+        throw new IllegalStateException("unable to get receive commits result");
       }
-    } finally {
-      w.sendMessages();
+      return runnable.get();
+    } catch (InterruptedException | ExecutionException e) {
+      throw new UncheckedExecutionException(e);
     }
+  }
 
-    long deltaNanos = System.nanoTime() - startNanos;
-    int totalChanges = 0;
-
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public void reportMetrics(ReceiveCommitsResult result, long deltaNanos) {
     PushType pushType;
-    if (resultChangeIds.isMagicPush()) {
+    int totalChanges = 0;
+    if (result.magicPush()) {
       pushType = PushType.CREATE_REPLACE;
-      List<Change.Id> created = resultChangeIds.get(ResultChangeIds.Key.CREATED);
-      List<Change.Id> replaced = resultChangeIds.get(ResultChangeIds.Key.REPLACED);
+      Set<Change.Id> created = result.changes().get(ReceiveCommitsResult.ChangeStatus.CREATED);
+      Set<Change.Id> replaced = result.changes().get(ReceiveCommitsResult.ChangeStatus.REPLACED);
       metrics.changes.record(pushType, created.size() + replaced.size());
       totalChanges = replaced.size() + created.size();
     } else {
-      List<Change.Id> autoclosed = resultChangeIds.get(ResultChangeIds.Key.AUTOCLOSED);
+      Set<Change.Id> autoclosed =
+          result.changes().get(ReceiveCommitsResult.ChangeStatus.AUTOCLOSED);
       if (!autoclosed.isEmpty()) {
         pushType = PushType.AUTOCLOSE;
         metrics.changes.record(pushType, autoclosed.size());
@@ -411,21 +411,25 @@
         pushType = PushType.NORMAL;
       }
     }
-
     if (totalChanges > 0) {
       metrics.latencyPerChange.record(pushType, deltaNanos / totalChanges, NANOSECONDS);
     }
-
     metrics.latencyPerPush.record(pushType, deltaNanos, NANOSECONDS);
   }
 
-  /** Returns the Change.Ids that were processed in onPreReceive */
-  @UsedAt(UsedAt.Project.GOOGLE)
-  public ResultChangeIds getResultChangeIds() {
-    return resultChangeIds;
-  }
-
   public ReceivePack getReceivePack() {
     return receivePack;
   }
+
+  /**
+   * Marks all commands that were not processed yet as {@link Result#REJECTED_OTHER_REASON}.
+   * Intended to be used to finish up remaining commands when errors occur during processing.
+   */
+  private static void rejectCommandsNotAttempted(Collection<ReceiveCommand> commands) {
+    for (ReceiveCommand c : commands) {
+      if (c.getResult() == Result.NOT_ATTEMPTED) {
+        c.setResult(Result.REJECTED_OTHER_REASON, "internal error");
+      }
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 2ffa7b6..9ad543c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -226,7 +226,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
@@ -255,8 +254,7 @@
         ReceivePack receivePack,
         Repository repository,
         AllRefsWatcher allRefsWatcher,
-        MessageSender messageSender,
-        ResultChangeIds resultChangeIds);
+        MessageSender messageSender);
   }
 
   private class ReceivePackMessageSender implements MessageSender {
@@ -379,9 +377,12 @@
   private Optional<String> tracePushOption;
 
   private MessageSender messageSender;
-  private ResultChangeIds resultChangeIds;
+  private ReceiveCommitsResult.Builder result;
   private ImmutableMap<String, String> loggingTags;
 
+  /** This object is for single use only. */
+  private boolean used;
+
   @Inject
   ReceiveCommits(
       AccountResolver accountResolver,
@@ -428,8 +429,7 @@
       @Assisted ReceivePack rp,
       @Assisted Repository repository,
       @Assisted AllRefsWatcher allRefsWatcher,
-      @Nullable @Assisted MessageSender messageSender,
-      @Assisted ResultChangeIds resultChangeIds)
+      @Nullable @Assisted MessageSender messageSender)
       throws IOException {
     // Injected fields.
     this.accountResolver = accountResolver;
@@ -493,6 +493,8 @@
     replaceByChange = new LinkedHashMap<>();
     updateGroups = new ArrayList<>();
 
+    used = false;
+
     this.allowProjectOwnersToChangeParent =
         config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
 
@@ -502,7 +504,7 @@
 
     // Handles for outputting back over the wire to the end user.
     this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender();
-    this.resultChangeIds = resultChangeIds;
+    this.result = ReceiveCommitsResult.builder();
     this.loggingTags = ImmutableMap.of();
 
     // TODO(hiesel): Make this decision implicit once vetted
@@ -565,7 +567,9 @@
     }
   }
 
-  void processCommands(Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
+  ReceiveCommitsResult processCommands(
+      Collection<ReceiveCommand> commands, MultiProgressMonitor progress) throws StorageException {
+    checkState(!used, "Tried to re-use a ReceiveCommits objects that is single-use only");
     parsePushOptions();
     int commandCount = commands.size();
     try (TraceContext traceContext =
@@ -604,6 +608,7 @@
       loggingTags = traceContext.getTags();
       logger.atFine().log("Processing commands done.");
     }
+    return result.build();
   }
 
   // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
@@ -666,9 +671,7 @@
         try {
           newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
         } catch (IOException e) {
-          logger.atSevere().withCause(e).log(
-              "Failed to select new changes in %s", project.getName());
-          return;
+          throw new StorageException("Failed to select new changes in " + project.getName(), e);
         }
       }
 
@@ -704,7 +707,7 @@
       throws PermissionBackendException, IOException, NoSuchProjectException {
     try (TraceTimer traceTimer =
         newTimer("handleRegularCommands", Metadata.builder().resourceCount(cmds.size()))) {
-      resultChangeIds.setMagicPush(false);
+      result.magicPush(false);
       for (ReceiveCommand cmd : cmds) {
         parseRegularCommand(cmd);
       }
@@ -728,8 +731,7 @@
         logger.atFine().log("Added %d additional ref updates", added);
         bu.execute();
       } catch (UpdateException | RestApiException e) {
-        rejectRemaining(cmds, INTERNAL_SERVER_ERROR);
-        logger.atFine().withCause(e).log("update failed:");
+        throw new StorageException(e);
       }
 
       Set<BranchNameKey> branches = new HashSet<>();
@@ -948,9 +950,12 @@
         }
 
         replaceByChange.values().stream()
-            .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.REPLACED, req.ontoChange));
+            .forEach(
+                req ->
+                    result.addChange(ReceiveCommitsResult.ChangeStatus.REPLACED, req.ontoChange));
         newChanges.stream()
-            .forEach(req -> resultChangeIds.add(ResultChangeIds.Key.CREATED, req.changeId));
+            .forEach(
+                req -> result.addChange(ReceiveCommitsResult.ChangeStatus.CREATED, req.changeId));
 
         if (magicBranchCmd != null) {
           magicBranchCmd.setResult(OK);
@@ -975,9 +980,7 @@
         logger.atFine().withCause(e).log("Rejecting due to client error");
         reject(magicBranchCmd, e.getMessage());
       } catch (RestApiException | IOException e) {
-        logger.atSevere().withCause(e).log(
-            "Can't insert change/patch set for %s", project.getName());
-        reject(magicBranchCmd, String.format("%s: %s", INTERNAL_SERVER_ERROR, e.getMessage()));
+        throw new StorageException("Can't insert change/patch set for " + project.getName(), e);
       }
 
       if (magicBranch != null && magicBranch.submit) {
@@ -1291,11 +1294,11 @@
       RevObject obj;
       try {
         obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
-      } catch (IOException err) {
-        logger.atSevere().withCause(err).log(
-            "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName());
-        reject(cmd, "invalid object");
-        return;
+      } catch (IOException e) {
+        throw new StorageException(
+            String.format(
+                "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
+            e);
       }
       logger.atFine().log("Creating %s", cmd);
 
@@ -1347,11 +1350,11 @@
     RevObject obj;
     try {
       obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
-    } catch (IOException err) {
-      logger.atSevere().withCause(err).log(
-          "Invalid object %s for %s", cmd.getNewId().name(), cmd.getRefName());
-      reject(cmd, "invalid object");
-      return false;
+    } catch (IOException e) {
+      throw new StorageException(
+          String.format(
+              "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
+          e);
     }
 
     if (obj instanceof RevCommit) {
@@ -1385,11 +1388,11 @@
     try (TraceTimer traceTimer = newTimer("parseRewind")) {
       try {
         receivePack.getRevWalk().parseCommit(cmd.getNewId());
-      } catch (IOException err) {
-        logger.atSevere().withCause(err).log(
-            "Invalid object %s for %s forced update", cmd.getNewId().name(), cmd.getRefName());
-        reject(cmd, "invalid object");
-        return;
+      } catch (IOException e) {
+        throw new StorageException(
+            String.format(
+                "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName()),
+            e);
       }
       logger.atFine().log("Rewinding %s", cmd);
 
@@ -1903,10 +1906,8 @@
               reject(cmd, "base not found");
               return;
             } catch (IOException e) {
-              logger.atWarning().withCause(e).log(
-                  "Project %s cannot read %s", project.getName(), id.name());
-              reject(cmd, INTERNAL_SERVER_ERROR);
-              return;
+              throw new StorageException(
+                  String.format("Project %s cannot read %s", project.getName(), id.name()), e);
             }
           }
         } else if (newChangeForAllNotInTarget) {
@@ -1929,16 +1930,14 @@
             }
           }
         }
-      } catch (IOException ex) {
-        logger.atWarning().withCause(ex).log(
-            "Error walking to %s in project %s", destBranch, project.getName());
-        reject(cmd, INTERNAL_SERVER_ERROR);
-        return;
+      } catch (IOException e) {
+        throw new StorageException(
+            String.format("Error walking to %s in project %s", destBranch, project.getName()), e);
       }
 
       if (validateConnected(magicBranch.cmd, magicBranch.dest, tip)) {
         this.magicBranch = magicBranch;
-        this.resultChangeIds.setMagicPush(true);
+        this.result.magicPush(true);
       }
     }
   }
@@ -1994,8 +1993,7 @@
       logger.atFine().log("HEAD = %s", head);
       return head;
     } catch (IOException e) {
-      logger.atSevere().withCause(e).log("Cannot read HEAD symref");
-      return null;
+      throw new StorageException("Cannot read HEAD symref", e);
     }
   }
 
@@ -2061,7 +2059,7 @@
       try {
         receivePack.getRevWalk().parseBody(create.commit);
       } catch (IOException e) {
-        continue;
+        throw new StorageException("Can't parse commit", e);
       }
       List<String> idList = create.commit.getFooterLines(FooterConstants.CHANGE_ID);
 
@@ -2306,14 +2304,7 @@
       } catch (IOException e) {
         // Should never happen, the core receive process would have
         // identified the missing object earlier before we got control.
-        //
-        magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
-        logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
-        return Collections.emptyList();
-      } catch (StorageException e) {
-        logger.atSevere().withCause(e).log("Cannot query database to locate prior changes");
-        reject(magicBranch.cmd, "database error");
-        return Collections.emptyList();
+        throw new StorageException("Invalid pack upload; one or more objects weren't sent", e);
       }
 
       if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
@@ -2325,25 +2316,20 @@
         return newChanges;
       }
 
-      try {
-        SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
-        List<Integer> newIds = seq.nextChangeIds(newChanges.size());
-        for (int i = 0; i < newChanges.size(); i++) {
-          CreateRequest create = newChanges.get(i);
-          create.setChangeId(newIds.get(i));
-          create.groups = ImmutableList.copyOf(groups.get(create.commit));
-        }
-        for (ReplaceRequest replace : replaceByChange.values()) {
-          replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
-        }
-        for (UpdateGroupsRequest update : updateGroups) {
-          update.groups = ImmutableList.copyOf((groups.get(update.commit)));
-        }
-        logger.atFine().log("Finished updating groups from GroupCollector");
-      } catch (StorageException e) {
-        logger.atSevere().withCause(e).log("Error collecting groups for changes");
-        reject(magicBranch.cmd, INTERNAL_SERVER_ERROR);
+      SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
+      List<Integer> newIds = seq.nextChangeIds(newChanges.size());
+      for (int i = 0; i < newChanges.size(); i++) {
+        CreateRequest create = newChanges.get(i);
+        create.setChangeId(newIds.get(i));
+        create.groups = ImmutableList.copyOf(groups.get(create.commit));
       }
+      for (ReplaceRequest replace : replaceByChange.values()) {
+        replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
+      }
+      for (UpdateGroupsRequest update : updateGroups) {
+        update.groups = ImmutableList.copyOf((groups.get(update.commit)));
+      }
+      logger.atFine().log("Finished updating groups from GroupCollector");
       return newChanges;
     }
   }
@@ -2656,14 +2642,9 @@
             req.validateNewPatchSet();
           }
         }
-      } catch (StorageException err) {
-        logger.atSevere().withCause(err).log(
-            "Cannot read database before replacement for project %s", project.getName());
-        rejectRemainingRequests(replaceByChange.values(), INTERNAL_SERVER_ERROR);
-      } catch (IOException | PermissionBackendException err) {
-        logger.atSevere().withCause(err).log(
-            "Cannot read repository before replacement for project %s", project.getName());
-        rejectRemainingRequests(replaceByChange.values(), INTERNAL_SERVER_ERROR);
+      } catch (IOException | PermissionBackendException e) {
+        throw new StorageException(
+            "Cannot read repository before replacement for project " + project.getName(), e);
       }
       logger.atFine().log("Read %d changes to replace", replaceByChange.size());
 
@@ -2671,11 +2652,11 @@
         // Cancel creations tied to refs/for/ command.
         for (ReplaceRequest req : replaceByChange.values()) {
           if (req.inputCommand == magicBranch.cmd && req.cmd != null) {
-            req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
+            req.cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, "aborted");
           }
         }
         for (CreateRequest req : newChanges) {
-          req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
+          req.cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, "aborted");
         }
       }
     }
@@ -3097,13 +3078,13 @@
           logger.atFine().log("Updating project description");
           repo.setGitwebDescription(ps.getProject().getDescription());
         } catch (IOException e) {
-          logger.atWarning().withCause(e).log("cannot update description of %s", project.getName());
+          throw new StorageException("cannot update description of " + project.getName(), e);
         }
         if (allProjectsName.equals(project.getNameKey())) {
           try {
             createGroupPermissionSyncer.syncIfNeeded();
           } catch (IOException | ConfigInvalidException e) {
-            logger.atSevere().withCause(e).log("Can't sync create group permissions");
+            throw new StorageException("cannot update description of " + project.getName(), e);
           }
         }
       }
@@ -3358,8 +3339,7 @@
                         existingPatchSets, newPatchSets);
                     bu.execute();
                   } catch (IOException | StorageException | PermissionBackendException e) {
-                    logger.atSevere().withCause(e).log("Failed to auto-close changes");
-                    return null;
+                    throw new StorageException("Failed to auto-close changes", e);
                   }
 
                   // If we are here, we didn't throw UpdateException. Record the result.
@@ -3367,7 +3347,8 @@
                   // doesn't
                   // fit into TreeSet.
                   ids.stream()
-                      .forEach(id -> resultChangeIds.add(ResultChangeIds.Key.AUTOCLOSED, id));
+                      .forEach(
+                          id -> result.addChange(ReceiveCommitsResult.ChangeStatus.AUTOCLOSED, id));
 
                   return null;
                 })
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java
new file mode 100644
index 0000000..ecbdcbc
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsResult.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.gerrit.entities.Change;
+import java.util.Arrays;
+import java.util.EnumMap;
+
+/** Keeps track of the change IDs thus far updated by {@link ReceiveCommits}. */
+@AutoValue
+public abstract class ReceiveCommitsResult {
+  /** Status of a change. Used to aggregate metrics. */
+  public enum ChangeStatus {
+    CREATED,
+    REPLACED,
+    AUTOCLOSED,
+  }
+
+  /**
+   * Returns change IDs of the given type for which the BatchUpdate succeeded, or empty list if
+   * there are none.
+   */
+  public abstract ImmutableMap<ChangeStatus, ImmutableSet<Change.Id>> changes();
+
+  /** Indicate that the ReceiveCommits call involved a magic branch, such as {@code refs/for/}. */
+  public abstract boolean magicPush();
+
+  public static Builder builder() {
+    return new AutoValue_ReceiveCommitsResult.Builder().magicPush(false);
+  }
+
+  public static ReceiveCommitsResult empty() {
+    return builder().build();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    private EnumMap<ChangeStatus, ImmutableSet.Builder<Change.Id>> changes;
+
+    Builder() {
+      changes = Maps.newEnumMap(ChangeStatus.class);
+      Arrays.stream(ChangeStatus.values()).forEach(k -> changes.put(k, ImmutableSet.builder()));
+    }
+
+    /** Record a change ID update as having completed. */
+    public Builder addChange(ChangeStatus key, Change.Id id) {
+      changes.get(key).add(id);
+      return this;
+    }
+
+    public abstract Builder magicPush(boolean isMagicPush);
+
+    public ReceiveCommitsResult build() {
+      ImmutableMap.Builder<ChangeStatus, ImmutableSet<Change.Id>> changesBuilder =
+          ImmutableMap.builder();
+      changes.entrySet().forEach(e -> changesBuilder.put(e.getKey(), e.getValue().build()));
+      changes(changesBuilder.build());
+      return autoBuild();
+    }
+
+    protected abstract Builder changes(ImmutableMap<ChangeStatus, ImmutableSet<Change.Id>> changes);
+
+    protected abstract ReceiveCommitsResult autoBuild();
+  }
+}
diff --git a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java b/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
deleted file mode 100644
index 805822c..0000000
--- a/java/com/google/gerrit/server/git/receive/ResultChangeIds.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git.receive;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.entities.Change;
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Keeps track of the change IDs thus far updated by ReceiveCommit.
- *
- * <p>This class is thread-safe.
- */
-public class ResultChangeIds {
-  public enum Key {
-    CREATED,
-    REPLACED,
-    AUTOCLOSED,
-  }
-
-  private boolean isMagicPush;
-  private final Map<Key, List<Change.Id>> ids;
-
-  ResultChangeIds() {
-    ids = new EnumMap<>(Key.class);
-    for (Key k : Key.values()) {
-      ids.put(k, new ArrayList<>());
-    }
-  }
-
-  /** Record a change ID update as having completed. Thread-safe. */
-  public synchronized void add(Key key, Change.Id id) {
-    ids.get(key).add(id);
-  }
-
-  /** Indicate that the ReceiveCommits call involved a magic branch. */
-  public synchronized void setMagicPush(boolean magic) {
-    isMagicPush = magic;
-  }
-
-  public synchronized boolean isMagicPush() {
-    return isMagicPush;
-  }
-
-  /**
-   * Returns change IDs of the given type for which the BatchUpdate succeeded, or empty list if
-   * there are none. Thread-safe.
-   */
-  public synchronized List<Change.Id> get(Key key) {
-    return ImmutableList.copyOf(ids.get(key));
-  }
-}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 1e97a44..72a3f07 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -45,11 +45,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.validators.ValidationMessage.Type;
-import com.google.gerrit.server.patch.DiffSummary;
-import com.google.gerrit.server.patch.DiffSummaryKey;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -70,6 +65,8 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -80,6 +77,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
 
 /**
  * Represents a list of {@link CommitValidationListener}s to run for a push to one branch of one
@@ -103,7 +101,6 @@
     private final AccountValidator accountValidator;
     private final ProjectCache projectCache;
     private final ProjectConfig.Factory projectConfigFactory;
-    private final PatchListCache patchListCache;
     private final Config config;
 
     @Inject
@@ -118,8 +115,7 @@
         ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
         AccountValidator accountValidator,
         ProjectCache projectCache,
-        ProjectConfig.Factory projectConfigFactory,
-        PatchListCache patchListCache) {
+        ProjectConfig.Factory projectConfigFactory) {
       this.gerritIdent = gerritIdent;
       this.urlFormatter = urlFormatter;
       this.config = config;
@@ -131,7 +127,6 @@
       this.accountValidator = accountValidator;
       this.projectCache = projectCache;
       this.projectConfigFactory = projectConfigFactory;
-      this.patchListCache = patchListCache;
     }
 
     public CommitValidators forReceiveCommits(
@@ -152,7 +147,7 @@
               new ProjectStateValidationListener(projectState),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
               new AuthorUploaderValidator(user, perm, urlFormatter.get()),
-              new FileCountValidator(patchListCache, config),
+              new FileCountValidator(repoManager, config),
               new CommitterUploaderValidator(user, perm, urlFormatter.get()),
               new SignedOffByValidator(user, perm, projectState),
               new ChangeIdValidator(
@@ -181,7 +176,7 @@
               new ProjectStateValidationListener(projectState),
               new AmendedGerritMergeCommitValidationListener(perm, gerritIdent),
               new AuthorUploaderValidator(user, perm, urlFormatter.get()),
-              new FileCountValidator(patchListCache, config),
+              new FileCountValidator(repoManager, config),
               new SignedOffByValidator(user, perm, projectState),
               new ChangeIdValidator(
                   projectState, user, urlFormatter.get(), config, sshInfo, change),
@@ -392,11 +387,11 @@
   /** Limits the number of files per change. */
   private static class FileCountValidator implements CommitValidationListener {
 
-    private final PatchListCache patchListCache;
+    private final GitRepositoryManager repoManager;
     private final int maxFileCount;
 
-    FileCountValidator(PatchListCache patchListCache, Config config) {
-      this.patchListCache = patchListCache;
+    FileCountValidator(GitRepositoryManager repoManager, Config config) {
+      this.repoManager = repoManager;
       maxFileCount = config.getInt("change", null, "maxFiles", 100_000);
     }
 
@@ -414,20 +409,17 @@
         return Collections.emptyList();
       }
 
-      PatchListKey patchListKey =
-          PatchListKey.againstBase(
-              receiveEvent.commit.getId(), receiveEvent.commit.getParentCount());
-      DiffSummaryKey diffSummaryKey = DiffSummaryKey.fromPatchListKey(patchListKey);
+      // Use DiffFormatter to compute the number of files in the change. This should be faster than
+      // the previous approach of using the PatchListCache.
       try {
-        DiffSummary diffSummary =
-            patchListCache.getDiffSummary(diffSummaryKey, receiveEvent.project.getNameKey());
-        if (diffSummary.getPaths().size() > maxFileCount) {
+        long changedFiles = countChangedFiles(receiveEvent);
+        if (changedFiles > maxFileCount) {
           throw new CommitValidationException(
               String.format(
                   "Exceeding maximum number of files per change (%d > %d)",
-                  diffSummary.getPaths().size(), maxFileCount));
+                  changedFiles, maxFileCount));
         }
-      } catch (PatchListNotAvailableException e) {
+      } catch (IOException e) {
         // This happens e.g. for cherrypicks.
         if (!receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
           logger.atWarning().withCause(e).log(
@@ -436,6 +428,21 @@
       }
       return Collections.emptyList();
     }
+
+    private long countChangedFiles(CommitReceivedEvent receiveEvent) throws IOException {
+      try (Repository repository = repoManager.openRepository(receiveEvent.project.getNameKey());
+          RevWalk revWalk = new RevWalk(repository);
+          DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+        diffFormatter.setReader(revWalk.getObjectReader(), repository.getConfig());
+        diffFormatter.setDetectRenames(true);
+        // For merge commits, i.e. >1 parents, we use parent #0 by convention.
+        List<DiffEntry> diffEntries =
+            diffFormatter.scan(
+                receiveEvent.commit.getParentCount() > 0 ? receiveEvent.commit.getParent(0) : null,
+                receiveEvent.commit);
+        return diffEntries.stream().map(DiffEntry::getNewPath).distinct().count();
+      }
+    }
   }
 
   /** If this is the special project configuration branch, validate the config. */
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 2b826ac..2a8a5ba 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -26,6 +26,7 @@
 import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
@@ -47,7 +48,6 @@
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -175,7 +175,7 @@
                 return null;
               },
               directExecutor()));
-    } catch (ExecutionException e) {
+    } catch (UncheckedExecutionException e) {
       logger.atSevere().withCause(e).log("Error in batch indexer");
       ok.set(false);
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 2be1324..bdb58b8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -384,9 +384,9 @@
         reviewersByEmail.asTable().cellSet()) {
       String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
       r.add(v);
-      if (c.getColumnKey().getName() != null) {
+      if (c.getColumnKey().name() != null) {
         // Add another entry without the name to provide search functionality on the email
-        Address emailOnly = new Address(c.getColumnKey().getEmail());
+        Address emailOnly = Address.create(c.getColumnKey().email());
         r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
       }
       r.add(v + ',' + c.getValue().getTime());
diff --git a/java/com/google/gerrit/server/mail/ListMailFilter.java b/java/com/google/gerrit/server/mail/ListMailFilter.java
index 1549f8d..23f7e12 100644
--- a/java/com/google/gerrit/server/mail/ListMailFilter.java
+++ b/java/com/google/gerrit/server/mail/ListMailFilter.java
@@ -52,7 +52,7 @@
       return true;
     }
 
-    boolean match = mailPattern.matcher(message.from().getEmail()).find();
+    boolean match = mailPattern.matcher(message.from().email()).find();
     if ((mode == ListFilterMode.WHITELIST && !match)
         || (mode == ListFilterMode.BLACKLIST && match)) {
       logger.atInfo().log("Mail message from %s rejected by list filter", message.from());
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 35b0027..3b7b2aa 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -58,7 +58,7 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", String.format("[Gerrit Code Review] New %s Keys Added", getKeyType()));
-    add(RecipientType.TO, new Address(getEmail()));
+    add(RecipientType.TO, Address.create(getEmail()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index c26e336..3df7f05 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -63,7 +63,7 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", String.format("[Gerrit Code Review] %s Keys Deleted", getKeyType()));
-    add(RecipientType.TO, new Address(getEmail()));
+    add(RecipientType.TO, Address.create(getEmail()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
index 55f2352..dfaabbe 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -52,8 +52,7 @@
 
     if (from == null || "MIXED".equalsIgnoreCase(from)) {
       ParameterizedString name = new ParameterizedString("${user} (Code Review)");
-      generator =
-          new PatternGen(srvAddr, accountCache, anonymousCowardName, name, srvAddr.getEmail());
+      generator = new PatternGen(srvAddr, accountCache, anonymousCowardName, name, srvAddr.email());
     } else if ("USER".equalsIgnoreCase(from)) {
       String[] domains = cfg.getStringList("sendemail", null, "allowedDomain");
       Pattern domainPattern = MailUtil.glob(domains);
@@ -64,18 +63,17 @@
       generator = new ServerGen(srvAddr);
     } else {
       final Address a = Address.parse(from);
-      final ParameterizedString name =
-          a.getName() != null ? new ParameterizedString(a.getName()) : null;
+      final ParameterizedString name = a.name() != null ? new ParameterizedString(a.name()) : null;
       if (name == null || name.getParameterNames().isEmpty()) {
         generator = new ServerGen(a);
       } else {
-        generator = new PatternGen(srvAddr, accountCache, anonymousCowardName, name, a.getEmail());
+        generator = new PatternGen(srvAddr, accountCache, anonymousCowardName, name, a.email());
       }
     }
   }
 
   private static Address toAddress(PersonIdent myIdent) {
-    return new Address(myIdent.getName(), myIdent.getEmailAddress());
+    return Address.create(myIdent.getName(), myIdent.getEmailAddress());
   }
 
   @Override
@@ -127,7 +125,7 @@
         String fullName = a.map(Account::fullName).orElse(null);
         String userEmail = a.map(Account::preferredEmail).orElse(null);
         if (canRelay(userEmail)) {
-          return new Address(fullName, userEmail);
+          return Address.create(fullName, userEmail);
         }
 
         if (fullName == null || "".equals(fullName.trim())) {
@@ -135,17 +133,17 @@
         }
         senderName = nameRewriteTmpl.replace("user", fullName).toString();
       } else {
-        senderName = serverAddress.getName();
+        senderName = serverAddress.name();
       }
 
       String senderEmail;
-      ParameterizedString senderEmailPattern = new ParameterizedString(serverAddress.getEmail());
+      ParameterizedString senderEmailPattern = new ParameterizedString(serverAddress.email());
       if (senderEmailPattern.getParameterNames().isEmpty()) {
         senderEmail = senderEmailPattern.getRawPattern();
       } else {
         senderEmail = senderEmailPattern.replace("userHash", hashOf(senderName)).toString();
       }
-      return new Address(senderName, senderEmail);
+      return Address.create(senderName, senderEmail);
     }
 
     /** check if Gerrit is allowed to send from {@code userEmail}. */
@@ -215,7 +213,7 @@
         senderName = namePattern.replace("user", fullName).toString();
 
       } else {
-        senderName = serverAddress.getName();
+        senderName = serverAddress.name();
       }
 
       String senderEmail;
@@ -224,7 +222,7 @@
       } else {
         senderEmail = senderEmailPattern.replace("userHash", hashOf(senderName)).toString();
       }
-      return new Address(senderName, senderEmail);
+      return Address.create(senderName, senderEmail);
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index d792b48..cec2bb5 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -42,7 +42,7 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
-    add(RecipientType.TO, new Address(getEmail()));
+    add(RecipientType.TO, Address.create(getEmail()));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 528755a..b35bbec 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -162,7 +162,7 @@
                 "Removing account %s from HTML email because user prefers plain text emails", id);
             removeUser(thisUserAccount);
             smtpRcptToPlaintextOnly.add(
-                new Address(thisUserAccount.fullName(), thisUserAccount.preferredEmail()));
+                Address.create(thisUserAccount.fullName(), thisUserAccount.preferredEmail()));
           }
         }
         if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
@@ -179,11 +179,11 @@
         if (fromId != null) {
           Address address = toAddress(fromId);
           if (address != null) {
-            j.add(address.getEmail());
+            j.add(address.email());
           }
         }
-        smtpRcptTo.stream().forEach(a -> j.add(a.getEmail()));
-        smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail()));
+        smtpRcptTo.stream().forEach(a -> j.add(a.email()));
+        smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.email()));
         setHeader(FieldName.REPLY_TO, j.toString());
       }
 
@@ -518,16 +518,16 @@
   }
 
   protected void add(RecipientType rt, Address addr, boolean override) {
-    if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) {
-      if (!args.validator.isValid(addr.getEmail())) {
-        logger.atWarning().log("Not emailing %s (invalid email address)", addr.getEmail());
-      } else if (args.emailSender.canEmail(addr.getEmail())) {
+    if (addr != null && addr.email() != null && addr.email().length() > 0) {
+      if (!args.validator.isValid(addr.email())) {
+        logger.atWarning().log("Not emailing %s (invalid email address)", addr.email());
+      } else if (args.emailSender.canEmail(addr.email())) {
         if (!smtpRcptTo.add(addr)) {
           if (!override) {
             return;
           }
-          ((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.getEmail());
-          ((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.getEmail());
+          ((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.email());
+          ((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.email());
         }
         switch (rt) {
           case TO:
@@ -554,7 +554,7 @@
     if (!account.isActive() || e == null) {
       return null;
     }
-    return new Address(account.fullName(), e);
+    return Address.create(account.fullName(), e);
   }
 
   protected void setupSoyContext() {
@@ -597,7 +597,7 @@
   protected void removeUser(Account user) {
     String fromEmail = user.preferredEmail();
     for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext(); ) {
-      if (j.next().getEmail().equals(fromEmail)) {
+      if (j.next().email().equals(fromEmail)) {
         j.remove();
       }
     }
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index d2fa69c..ef58744 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -185,7 +185,7 @@
       }
       if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
         // If the group has an email address, do not expand membership.
-        matching.emails.add(new Address(group.getEmailAddress()));
+        matching.emails.add(Address.create(group.getEmailAddress()));
         logger.atFine().log(
             "notify group email address %s; skip expanding to members", group.getEmailAddress());
         continue;
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
index e1daec6..bb2efe6 100644
--- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -54,7 +54,7 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", "[Gerrit Code Review] Email Verification");
-    add(RecipientType.TO, new Address(addr));
+    add(RecipientType.TO, Address.create(addr));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 7207c00..8e53558 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -206,9 +206,8 @@
     try {
       final SMTPClient client = open();
       try {
-        if (!client.setSender(from.getEmail())) {
-          throw new EmailException(
-              "Server " + smtpHost + " rejected from address " + from.getEmail());
+        if (!client.setSender(from.email())) {
+          throw new EmailException("Server " + smtpHost + " rejected from address " + from.email());
         }
 
         /* Do not prevent the email from being sent to "good" users simply
@@ -219,7 +218,7 @@
          * error(s) logged.
          */
         for (Address addr : rcpt) {
-          if (!client.addRecipient(addr.getEmail())) {
+          if (!client.addRecipient(addr.email())) {
             String error = client.getReplyString();
             rejected
                 .append("Server ")
diff --git a/java/com/google/gerrit/server/submit/FastForwardOnly.java b/java/com/google/gerrit/server/submit/FastForwardOnly.java
index 494884e..176b063 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOnly.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOnly.java
@@ -33,9 +33,10 @@
         args.mergeUtil.getFirstFastForward(args.mergeTip.getInitialTip(), args.rw, sorted);
     if (!newTipCommit.equals(args.mergeTip.getInitialTip())) {
       ops.add(new FastForwardOp(args, newTipCommit));
-    }
-    while (!sorted.isEmpty()) {
-      ops.add(new NotFastForwardOp(sorted.remove(0)));
+    } else {
+      for (CodeReviewCommit c : toMerge) {
+        ops.add(new NotFastForwardOp(c));
+      }
     }
     return ops;
   }
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
index 8cc595d..b533bebc 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.submit;
 
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
@@ -79,9 +82,7 @@
               args.mergeTip.getInitialTip(),
               args.mergeTip.getCurrentTip(),
               alreadyMerged);
-      for (Change.Id id : unmerged) {
-        commitStatus.problem(id, "internal error: change not reachable from new branch tip");
-      }
+      checkState(unmerged.isEmpty(), "changes not reachable from new branch tip: %s", unmerged);
     }
     commitStatus.maybeFailVerbose();
   }
@@ -103,11 +104,9 @@
     for (Change.Id id : commitStatus.getChangeIds()) {
       CodeReviewCommit commit = commitStatus.get(id);
       CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
-      if (s == null) {
-        logger.atSevere().log("change %d: change not processed by merge strategy", id.get());
-        commitStatus.problem(id, "internal error: change not processed by merge strategy");
-        continue;
-      }
+      requireNonNull(
+          s, String.format("change %d: change not processed by merge strategy", id.get()));
+
       if (commit.getStatusMessage().isPresent()) {
         logger.atFine().log(
             "change %d: Status for commit %s is %s. %s",
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 2312134..b8d20ab 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -1077,7 +1077,7 @@
 
     assertThat(sender.getMessages()).hasSize(1);
     Message m = sender.getMessages().get(0);
-    assertThat(m.rcpt()).containsExactly(new Address(email));
+    assertThat(m.rcpt()).containsExactly(Address.create(email));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index fd681d8..5c786a5 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1517,7 +1517,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.from().getName()).isEqualTo("Administrator (Code Review)");
+    assertThat(m.from().name()).isEqualTo("Administrator (Code Review)");
     assertThat(m.rcpt()).containsExactly(user.getNameEmail());
     assertThat(m.body()).contains("I'd like you to do a code review");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
@@ -1899,7 +1899,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(new Address(fullname, email));
+    assertThat(m.rcpt()).containsExactly(Address.create(fullname, email));
     assertThat(m.body()).contains("Hello " + fullname + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
@@ -1960,7 +1960,7 @@
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
     Message m = messages.get(0);
-    assertThat(m.rcpt()).containsExactly(new Address(myGroupUserFullname, myGroupUserEmail));
+    assertThat(m.rcpt()).containsExactly(Address.create(myGroupUserFullname, myGroupUserEmail));
     assertThat(m.body()).contains("Hello " + myGroupUserFullname + ",\n");
     assertThat(m.body()).contains("I'd like you to do a code review.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
@@ -4461,7 +4461,7 @@
     amendChange(r.getChangeId());
     List<Message> messages = sender.getMessages();
     assertThat(messages).hasSize(1);
-    Address address = new Address(fullname, email);
+    Address address = Address.create(fullname, email);
     assertThat(messages.get(0).rcpt()).containsExactly(address);
 
     // Review notification is not sent to users ignoring the change
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 36c11da..7213a9f 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -1851,7 +1851,8 @@
 
   @Test
   public void pushWithEmailInFooterNotFound() throws Exception {
-    pushWithReviewerInFooter(new Address("No Body", "notarealuser@example.com").toString(), null);
+    pushWithReviewerInFooter(
+        Address.create("No Body", "notarealuser@example.com").toString(), null);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 28c9a9e..d3c0855 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -514,7 +514,7 @@
       TestAccount expected, @Nullable RecipientType expectedRecipientType) {
     String expectedEmail = expected.email();
     String expectedFullName = expected.fullName();
-    Address expectedAddress = new Address(expectedFullName, expectedEmail);
+    Address expectedAddress = Address.create(expectedFullName, expectedEmail);
     assertThat(sender.getMessages()).hasSize(2);
     Message message = sender.getMessages().get(0);
     assertThat(message.body().contains("review")).isTrue();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 9624ec2..72db9b3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -202,12 +202,14 @@
                   "Failed to submit 3 changes due to the following problems:\n"
                       + "Change "
                       + change2.getChange().getId()
-                      + ": internal error: "
-                      + "change not processed by merge strategy\n"
+                      + ": Project policy "
+                      + "requires all submissions to be a fast-forward. Please "
+                      + "rebase the change locally and upload again for review.\n"
                       + "Change "
                       + change3.getChange().getId()
-                      + ": internal error: "
-                      + "change not processed by merge strategy\n"
+                      + ": Project policy "
+                      + "requires all submissions to be a fast-forward. Please "
+                      + "rebase the change locally and upload again for review.\n"
                       + "Change "
                       + change4.getChange().getId()
                       + ": Project policy "
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 090146e..843ecc6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -368,6 +368,6 @@
   }
 
   private static String toRfcAddressString(AccountInfo info) {
-    return (new Address(info.name, info.email)).toString();
+    return (Address.create(info.name, info.email)).toString();
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 35576b8..94357b9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -830,7 +830,7 @@
   }
 
   private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
-    AccountInfo userInfo = new AccountInfo(user.fullName(), user.getNameEmail().getEmail());
+    AccountInfo userInfo = new AccountInfo(user.fullName(), user.getNameEmail().email());
     userInfo._accountId = user.id().get();
     userInfo.username = user.username();
     assertThat(gApi.changes().id(changeId).get().reviewers)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index d831b62..a0ebf02 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -303,7 +303,7 @@
     assertThat(author).email().isEqualTo(input.author.email);
     assertThat(author).name().isEqualTo(input.author.name);
     GitPerson committer = rApi.commit(false).committer;
-    assertThat(committer).email().isEqualTo(admin.getNameEmail().getEmail());
+    assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
   }
 
   @Test
@@ -463,7 +463,7 @@
     GitPerson author = rApi.commit(false).author;
     assertThat(author).email().isEqualTo(in.author.email);
     GitPerson committer = rApi.commit(false).committer;
-    assertThat(committer).email().isEqualTo(admin.getNameEmail().getEmail());
+    assertThat(committer).email().isEqualTo(admin.getNameEmail().email());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsLimitsIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsLimitsIT.java
index c32827a..89b5f6e 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsLimitsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsLimitsIT.java
@@ -14,54 +14,97 @@
 
 package com.google.gerrit.acceptance.server.git.receive;
 
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.server.patch.DiffSummary;
-import com.google.gerrit.server.patch.DiffSummaryKey;
-import com.google.inject.Inject;
-import com.google.inject.name.Named;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 /** Tests for applying limits to e.g. number of files per change. */
 public class ReceiveCommitsLimitsIT extends AbstractDaemonTest {
-
-  @Inject
-  private @Named("diff_summary") Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
-
   @Test
-  @GerritConfig(name = "change.maxFiles", value = "1")
+  @GerritConfig(name = "change.maxFiles", value = "2")
   public void limitFileCount() throws Exception {
-    PushOneCommit.Result result =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                testRepo,
-                "foo",
-                ImmutableMap.of("foo", "foo-1.0", "bar", "bar-1.0"))
-            .to("refs/for/master");
-    result.assertErrorStatus("Exceeding maximum number of files per change (2 > 1)");
+    // Create the parent.
+    RevCommit parent =
+        commitBuilder()
+            .add("foo.txt", "same old, same old")
+            .add("bar.txt", "bar")
+            .message("blah")
+            .create();
+    testRepo.reset(parent);
+
+    // A commit with 2 files is OK.
+    pushFactory
+        .create(
+            admin.newIdent(),
+            testRepo,
+            "blah",
+            ImmutableMap.of(
+                "foo.txt", "same old, same old", "bar.txt", "changed file", "baz.txt", "new file"))
+        .setParent(parent)
+        .to("refs/for/master")
+        .assertOkStatus();
+
+    // A commit with 3 files is rejected.
+    pushFactory
+        .create(
+            admin.newIdent(),
+            testRepo,
+            "blah",
+            ImmutableMap.of(
+                "foo.txt",
+                "same old, same old",
+                "bar.txt",
+                "changed file",
+                "baz.txt",
+                "new file",
+                "boom.txt",
+                "boom!"))
+        .setParent(parent)
+        .to("refs/for/master")
+        .assertErrorStatus("Exceeding maximum number of files per change (3 > 2)");
   }
 
   @Test
-  public void cacheKeyMatches() throws Exception {
-    int cacheSizeBefore = diffSummaryCache.asMap().size();
-    PushOneCommit.Result result =
-        pushFactory
-            .create(
-                admin.newIdent(),
-                testRepo,
-                "foo",
-                ImmutableMap.of("foo", "foo-1.0", "bar", "bar-1.0"))
-            .to("refs/for/master");
-    result.assertOkStatus();
+  @GerritConfig(name = "change.maxFiles", value = "1")
+  public void limitFileCount_merge() throws Exception {
+    // Create the parents.
+    RevCommit commitFoo =
+        commitBuilder().add("foo.txt", "same old, same old").message("blah").create();
+    RevCommit commitBar =
+        testRepo
+            .branch("branch")
+            .commit()
+            .insertChangeId()
+            .add("bar.txt", "bar")
+            .message("blah")
+            .create();
+    testRepo.reset(commitFoo);
 
-    // Assert that we only performed the diff computation once. This would e.g. catch
-    // bugs/deviations in the computation of the cache key.
-    assertThat(diffSummaryCache.asMap()).hasSize(cacheSizeBefore + 1);
+    // By convention we diff against the first parent.
+
+    // commitFoo is first -> 1 file changed -> OK
+    pushFactory
+        .create(
+            admin.newIdent(),
+            testRepo,
+            "blah",
+            ImmutableMap.of("foo.txt", "same old, same old", "bar.txt", "changed file"))
+        .setParents(ImmutableList.of(commitFoo, commitBar))
+        .to("refs/for/master")
+        .assertOkStatus();
+
+    // commitBar is first -> 2 files changed -> rejected
+    pushFactory
+        .create(
+            admin.newIdent(),
+            testRepo,
+            "blah",
+            ImmutableMap.of("foo.txt", "same old, same old", "bar.txt", "changed file"))
+        .setParents(ImmutableList.of(commitBar, commitFoo))
+        .to("refs/for/master")
+        .assertErrorStatus("Exceeding maximum number of files per change (2 > 1)");
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 7ad8e14..7a80cbd 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -49,7 +49,7 @@
 
   @Test
   public void newPatchSetsNotifyConfig() throws Exception {
-    Address addr = new Address("Watcher", "watcher@example.com");
+    Address addr = Address.create("Watcher", "watcher@example.com");
     NotifyConfig nc = new NotifyConfig();
     nc.addEmail(addr);
     nc.setName("new-patch-set");
@@ -90,7 +90,7 @@
 
   @Test
   public void noNotificationForPrivateChangesForWatchersInNotifyConfig() throws Exception {
-    Address addr = new Address("Watcher", "watcher@example.com");
+    Address addr = Address.create("Watcher", "watcher@example.com");
     NotifyConfig nc = new NotifyConfig();
     nc.addEmail(addr);
     nc.setName("team");
@@ -122,7 +122,7 @@
   @Test
   public void noNotificationForChangeThatIsTurnedPrivateForWatchersInNotifyConfig()
       throws Exception {
-    Address addr = new Address("Watcher", "watcher@example.com");
+    Address addr = Address.create("Watcher", "watcher@example.com");
     NotifyConfig nc = new NotifyConfig();
     nc.addEmail(addr);
     nc.setName("team");
@@ -151,7 +151,7 @@
 
   @Test
   public void noNotificationForWipChangesForWatchersInNotifyConfig() throws Exception {
-    Address addr = new Address("Watcher", "watcher@example.com");
+    Address addr = Address.create("Watcher", "watcher@example.com");
     NotifyConfig nc = new NotifyConfig();
     nc.addEmail(addr);
     nc.setName("team");
@@ -182,7 +182,7 @@
 
   @Test
   public void noNotificationForChangeThatIsTurnedWipForWatchersInNotifyConfig() throws Exception {
-    Address addr = new Address("Watcher", "watcher@example.com");
+    Address addr = Address.create("Watcher", "watcher@example.com");
     NotifyConfig nc = new NotifyConfig();
     nc.addEmail(addr);
     nc.setName("team");
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index 3c84420..f3c8671 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -84,7 +84,7 @@
   protected static MailMessage.Builder newMailMessageBuilder() {
     MailMessage.Builder b = MailMessage.builder();
     b.id("id");
-    b.from(new Address("Foo Bar", "foo@bar.com"));
+    b.from(Address.create("Foo Bar", "foo@bar.com"));
     b.dateReceived(Instant.now());
     b.subject("");
     return b;
diff --git a/javatests/com/google/gerrit/mail/AddressTest.java b/javatests/com/google/gerrit/mail/AddressTest.java
index da26123..8addcf8 100644
--- a/javatests/com/google/gerrit/mail/AddressTest.java
+++ b/javatests/com/google/gerrit/mail/AddressTest.java
@@ -23,57 +23,57 @@
   @Test
   public void parse_NameEmail1() {
     final Address a = Address.parse("A U Thor <author@example.com>");
-    assertThat(a.getName()).isEqualTo("A U Thor");
-    assertThat(a.getEmail()).isEqualTo("author@example.com");
+    assertThat(a.name()).isEqualTo("A U Thor");
+    assertThat(a.email()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_NameEmail2() {
     final Address a = Address.parse("A <a@b>");
-    assertThat(a.getName()).isEqualTo("A");
-    assertThat(a.getEmail()).isEqualTo("a@b");
+    assertThat(a.name()).isEqualTo("A");
+    assertThat(a.email()).isEqualTo("a@b");
   }
 
   @Test
   public void parse_NameEmail3() {
     final Address a = Address.parse("<a@b>");
-    assertThat(a.getName()).isNull();
-    assertThat(a.getEmail()).isEqualTo("a@b");
+    assertThat(a.name()).isNull();
+    assertThat(a.email()).isEqualTo("a@b");
   }
 
   @Test
   public void parse_NameEmail4() {
     final Address a = Address.parse("A U Thor<author@example.com>");
-    assertThat(a.getName()).isEqualTo("A U Thor");
-    assertThat(a.getEmail()).isEqualTo("author@example.com");
+    assertThat(a.name()).isEqualTo("A U Thor");
+    assertThat(a.email()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_NameEmail5() {
     final Address a = Address.parse("A U Thor  <author@example.com>");
-    assertThat(a.getName()).isEqualTo("A U Thor");
-    assertThat(a.getEmail()).isEqualTo("author@example.com");
+    assertThat(a.name()).isEqualTo("A U Thor");
+    assertThat(a.email()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_Email1() {
     final Address a = Address.parse("author@example.com");
-    assertThat(a.getName()).isNull();
-    assertThat(a.getEmail()).isEqualTo("author@example.com");
+    assertThat(a.name()).isNull();
+    assertThat(a.email()).isEqualTo("author@example.com");
   }
 
   @Test
   public void parse_Email2() {
     final Address a = Address.parse("a@b");
-    assertThat(a.getName()).isNull();
-    assertThat(a.getEmail()).isEqualTo("a@b");
+    assertThat(a.name()).isNull();
+    assertThat(a.email()).isEqualTo("a@b");
   }
 
   @Test
   public void parse_NewTLD() {
     Address a = Address.parse("A U Thor <author@example.systems>");
-    assertThat(a.getName()).isEqualTo("A U Thor");
-    assertThat(a.getEmail()).isEqualTo("author@example.systems");
+    assertThat(a.name()).isEqualTo("A U Thor");
+    assertThat(a.email()).isEqualTo("author@example.systems");
   }
 
   @Test
@@ -148,6 +148,6 @@
   }
 
   private static String format(String name, String email) {
-    return new Address(name, email).toHeaderString();
+    return Address.create(name, email).toHeaderString();
   }
 }
diff --git a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
index 2d2c2ea..296d1a1 100644
--- a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
+++ b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
@@ -38,11 +38,11 @@
     b.addAdditionalHeader(
         MailHeader.COMMENT_DATE.fieldWithDelimiter() + "Tue, 25 Oct 2016 02:11:35 -0700");
 
-    Address author = new Address("Diffy", "test@gerritcodereview.com");
+    Address author = Address.create("Diffy", "test@gerritcodereview.com");
     b.from(author);
 
     MailMetadata meta = MailHeaderParser.parse(b.build());
-    assertThat(meta.author).isEqualTo(author.getEmail());
+    assertThat(meta.author).isEqualTo(author.email());
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
@@ -71,11 +71,11 @@
         .append("Tue, 25 Oct 2016 02:11:35 -0700\r\n");
     b.textContent(stringBuilder.toString());
 
-    Address author = new Address("Diffy", "test@gerritcodereview.com");
+    Address author = Address.create("Diffy", "test@gerritcodereview.com");
     b.from(author);
 
     MailMetadata meta = MailHeaderParser.parse(b.build());
-    assertThat(meta.author).isEqualTo(author.getEmail());
+    assertThat(meta.author).isEqualTo(author.email());
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
@@ -112,11 +112,11 @@
         .append("</div>");
     b.htmlContent(stringBuilder.toString());
 
-    Address author = new Address("Diffy", "test@gerritcodereview.com");
+    Address author = Address.create("Diffy", "test@gerritcodereview.com");
     b.from(author);
 
     MailMetadata meta = MailHeaderParser.parse(b.build());
-    assertThat(meta.author).isEqualTo(author.getEmail());
+    assertThat(meta.author).isEqualTo(author.email());
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
     assertThat(meta.messageType).isEqualTo("comment");
diff --git a/javatests/com/google/gerrit/mail/data/AttachmentMessage.java b/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
index 1d94d68..aea59ba 100644
--- a/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
+++ b/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
@@ -77,8 +77,8 @@
     MailMessage.Builder expect = MailMessage.builder();
     expect
         .id("<CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w@mail.gmail.com>")
-        .from(new Address("Patrick Hiesel", "hiesel@google.com"))
-        .addTo(new Address("Patrick Hiesel", "hiesel@google.com"))
+        .from(Address.create("Patrick Hiesel", "hiesel@google.com"))
+        .addTo(Address.create("Patrick Hiesel", "hiesel@google.com"))
         .textContent("Contains unwanted attachment")
         .htmlContent("<div dir=\"ltr\">Contains unwanted attachment</div>")
         .subject("Test Subject")
diff --git a/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java b/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
index aa19537..957ee6e 100644
--- a/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
+++ b/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
@@ -53,10 +53,10 @@
     expect
         .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
         .from(
-            new Address(
+            Address.create(
                 "Jonathan Nieder (Gerrit)",
                 "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
-        .addTo(new Address("ekempin", "ekempin@google.com"))
+        .addTo(Address.create("ekempin", "ekempin@google.com"))
         .textContent(textContent)
         .subject("\uD83D\uDE1B test")
         .dateReceived(
diff --git a/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java b/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
index 1d68cc8..e5e2ed8 100644
--- a/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
+++ b/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
@@ -91,10 +91,10 @@
     expect
         .id("<001a114cd8be55b4ab053face5cd@google.com>")
         .from(
-            new Address(
+            Address.create(
                 "ekempin (Gerrit)", "noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com"))
-        .addCc(new Address("ekempin", "ekempin@google.com"))
-        .addTo(new Address("Patrick Hiesel", "hiesel@google.com"))
+        .addCc(Address.create("ekempin", "ekempin@google.com"))
+        .addTo(Address.create("Patrick Hiesel", "hiesel@google.com"))
         .textContent(textContent)
         .htmlContent(unencodedHtmlContent)
         .subject("Change in gerrit[master]: Implement receiver class structure and bindings")
diff --git a/javatests/com/google/gerrit/mail/data/NonUTF8Message.java b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
index 32915e7..e183a37 100644
--- a/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
+++ b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
@@ -57,10 +57,10 @@
     expect
         .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
         .from(
-            new Address(
+            Address.create(
                 "Jonathan Nieder (Gerrit)",
                 "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
-        .addTo(new Address("ekempin", "ekempin@google.com"))
+        .addTo(Address.create("ekempin", "ekempin@google.com"))
         .textContent(textContent)
         .subject("\uD83D\uDE1B test")
         .dateReceived(
diff --git a/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java b/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
index 47e813a..ac739c8 100644
--- a/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
+++ b/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
@@ -54,10 +54,10 @@
     expect
         .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
         .from(
-            new Address(
+            Address.create(
                 "Jonathan Nieder (Gerrit)",
                 "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
-        .addTo(new Address("ekempin", "ekempin@google.com"))
+        .addTo(Address.create("ekempin", "ekempin@google.com"))
         .textContent(textContent)
         .subject("âme vulgaire")
         .dateReceived(
diff --git a/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
index c4737e6..3f8e62f 100644
--- a/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
+++ b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
@@ -116,13 +116,13 @@
     expect
         .id("<001a114da7ae26e2eb053fe0c29c@google.com>")
         .from(
-            new Address(
+            Address.create(
                 "Jonathan Nieder (Gerrit)",
                 "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com"))
-        .addTo(new Address("ekempin", "ekempin@google.com"))
-        .addCc(new Address("Dave Borowitz", "dborowitz@google.com"))
-        .addCc(new Address("Jonathan Nieder", "jrn@google.com"))
-        .addCc(new Address("Patrick Hiesel", "hiesel@google.com"))
+        .addTo(Address.create("ekempin", "ekempin@google.com"))
+        .addCc(Address.create("Dave Borowitz", "dborowitz@google.com"))
+        .addCc(Address.create("Jonathan Nieder", "jrn@google.com"))
+        .addCc(Address.create("Patrick Hiesel", "hiesel@google.com"))
         .textContent(textContent)
         .subject("Change in gerrit[master]: (Re)enable voting buttons for merged changes")
         .dateReceived(
diff --git a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
index 9dcb08c..805c542 100644
--- a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
+++ b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
@@ -61,8 +61,8 @@
     // Build Message
     MailMessage.Builder b = MailMessage.builder();
     b.id("some id");
-    b.from(new Address("admim@example.com"));
-    b.addTo(new Address("gerrit@my-company.com")); // Not evaluated
+    b.from(Address.create("admim@example.com"));
+    b.addTo(Address.create("gerrit@my-company.com")); // Not evaluated
     b.subject("");
     b.dateReceived(Instant.now());
     b.textContent("I am currently out of office, please leave a code review after the beep.");
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 74f44a1..a383d56 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -85,8 +85,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name);
-    assertThat(r.getEmail()).isEqualTo(email);
+    assertThat(r.name()).isEqualTo(name);
+    assertThat(r.email()).isEqualTo(email);
     verifyAccountCacheGet(user);
   }
 
@@ -99,8 +99,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isNull();
-    assertThat(r.getEmail()).isEqualTo(email);
+    assertThat(r.name()).isNull();
+    assertThat(r.email()).isEqualTo(email);
     verifyAccountCacheGet(user);
   }
 
@@ -113,8 +113,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name + " (Code Review)");
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(name + " (Code Review)");
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyAccountCacheGet(user);
   }
 
@@ -123,8 +123,8 @@
     setFrom("USER");
     final Address r = create().from(null);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(ident.getName());
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(ident.getName());
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyZeroInteractions(accountCache);
   }
 
@@ -138,8 +138,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name);
-    assertThat(r.getEmail()).isEqualTo(email);
+    assertThat(r.name()).isEqualTo(name);
+    assertThat(r.email()).isEqualTo(email);
     verifyAccountCacheGet(user);
   }
 
@@ -153,8 +153,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name + " (Code Review)");
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(name + " (Code Review)");
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyAccountCacheGet(user);
   }
 
@@ -169,8 +169,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name);
-    assertThat(r.getEmail()).isEqualTo(email);
+    assertThat(r.name()).isEqualTo(name);
+    assertThat(r.email()).isEqualTo(email);
     verifyAccountCacheGet(user);
   }
 
@@ -185,8 +185,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name + " (Code Review)");
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(name + " (Code Review)");
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyAccountCacheGet(user);
   }
 
@@ -200,8 +200,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name);
-    assertThat(r.getEmail()).isEqualTo(email);
+    assertThat(r.name()).isEqualTo(name);
+    assertThat(r.email()).isEqualTo(email);
     verifyAccountCacheGet(user);
   }
 
@@ -227,8 +227,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(ident.getName());
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(ident.getName());
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyZeroInteractions(accountCache);
   }
 
@@ -237,8 +237,8 @@
     setFrom("SERVER");
     final Address r = create().from(null);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(ident.getName());
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(ident.getName());
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyZeroInteractions(accountCache);
   }
 
@@ -264,8 +264,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name + " (Code Review)");
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(name + " (Code Review)");
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyAccountCacheGet(user);
   }
 
@@ -278,8 +278,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo("Anonymous Coward (Code Review)");
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo("Anonymous Coward (Code Review)");
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyAccountCacheGet(user);
   }
 
@@ -292,8 +292,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(name + " (Code Review)");
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(name + " (Code Review)");
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyAccountCacheGet(user);
   }
 
@@ -302,8 +302,8 @@
     setFrom("MIXED");
     final Address r = create().from(null);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(ident.getName());
-    assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress());
+    assertThat(r.name()).isEqualTo(ident.getName());
+    assertThat(r.email()).isEqualTo(ident.getEmailAddress());
     verifyZeroInteractions(accountCache);
   }
 
@@ -317,8 +317,8 @@
 
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo("A " + name + " B");
-    assertThat(r.getEmail()).isEqualTo("my.server@email.address");
+    assertThat(r.name()).isEqualTo("A " + name + " B");
+    assertThat(r.email()).isEqualTo("my.server@email.address");
     verifyAccountCacheGet(user);
   }
 
@@ -331,8 +331,8 @@
 
     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");
+    assertThat(r.name()).isEqualTo("A Anonymous Coward B");
+    assertThat(r.email()).isEqualTo("my.server@email.address");
   }
 
   @Test
@@ -341,8 +341,8 @@
 
     final Address r = create().from(null);
     assertThat(r).isNotNull();
-    assertThat(r.getName()).isEqualTo(ident.getName());
-    assertThat(r.getEmail()).isEqualTo("my.server@email.address");
+    assertThat(r.name()).isEqualTo(ident.getName());
+    assertThat(r.email()).isEqualTo("my.server@email.address");
   }
 
   private Account.Id user(String name, String email) {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 10b4eb7..efbaed6 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -431,11 +431,11 @@
                     ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
                         .put(
                             ReviewerStateInternal.CC,
-                            new Address("Name1", "email1@example.com"),
+                            Address.create("Name1", "email1@example.com"),
                             new Timestamp(1212L))
                         .put(
                             ReviewerStateInternal.REVIEWER,
-                            new Address("Name2", "email2@example.com"),
+                            Address.create("Name2", "email2@example.com"),
                             new Timestamp(3434L))
                         .build()))
             .build(),
@@ -465,7 +465,7 @@
                     ReviewerByEmailSet.fromTable(
                         ImmutableTable.of(
                             ReviewerStateInternal.CC,
-                            new Address("emailonly@example.com"),
+                            Address.create("emailonly@example.com"),
                             new Timestamp(1212L))))
                 .build(),
             ChangeNotesStateProto.newBuilder()
@@ -484,8 +484,8 @@
     ImmutableSet<Address> ccs = actual.reviewersByEmail().byState(ReviewerStateInternal.CC);
     assertThat(ccs).hasSize(1);
     Address address = Iterables.getOnlyElement(ccs);
-    assertThat(address.getName()).isNull();
-    assertThat(address.getEmail()).isEqualTo("emailonly@example.com");
+    assertThat(address.name()).isNull();
+    assertThat(address.email()).isEqualTo("emailonly@example.com");
   }
 
   @Test
@@ -525,11 +525,11 @@
                     ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
                         .put(
                             ReviewerStateInternal.CC,
-                            new Address("Name1", "email1@example.com"),
+                            Address.create("Name1", "email1@example.com"),
                             new Timestamp(1212L))
                         .put(
                             ReviewerStateInternal.REVIEWER,
-                            new Address("Name2", "email2@example.com"),
+                            Address.create("Name2", "email2@example.com"),
                             new Timestamp(3434L))
                         .build()))
             .build(),
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 4ec52e3..964187c 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -2920,7 +2920,7 @@
 
   @Test
   public void putReviewerByEmail() throws Exception {
-    Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+    Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
 
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -2933,7 +2933,7 @@
 
   @Test
   public void putAndRemoveReviewerByEmail() throws Exception {
-    Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+    Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
 
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -2950,7 +2950,7 @@
 
   @Test
   public void putRemoveAndAddBackReviewerByEmail() throws Exception {
-    Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+    Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
 
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -2971,8 +2971,8 @@
 
   @Test
   public void putReviewerByEmailAndCcByEmail() throws Exception {
-    Address adrReviewer = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
-    Address adrCc = new Address("Foo Bor", "foo.bar.2@gerritcodereview.com");
+    Address adrReviewer = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
+    Address adrCc = Address.create("Foo Bor", "foo.bar.2@gerritcodereview.com");
 
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -2993,7 +2993,7 @@
 
   @Test
   public void putReviewerByEmailAndChangeToCc() throws Exception {
-    Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+    Address adr = Address.create("Foo Bar", "foo.bar@gerritcodereview.com");
 
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -3049,8 +3049,8 @@
 
   @Test
   public void pendingReviewers() throws Exception {
-    Address adr1 = new Address("Foo Bar1", "foo.bar1@gerritcodereview.com");
-    Address adr2 = new Address("Foo Bar2", "foo.bar2@gerritcodereview.com");
+    Address adr1 = Address.create("Foo Bar1", "foo.bar1@gerritcodereview.com");
+    Address adr2 = Address.create("Foo Bar2", "foo.bar2@gerritcodereview.com");
     Account.Id ownerId = changeOwner.getAccount().id();
     Account.Id otherUserId = otherUser.getAccount().id();
 
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index b32ac19..6a090c1 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -388,7 +388,7 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.putReviewerByEmail(
-        new Address("John Doe", "j.doe@gerritcodereview.com"), ReviewerStateInternal.REVIEWER);
+        Address.create("John Doe", "j.doe@gerritcodereview.com"), ReviewerStateInternal.REVIEWER);
     update.commit();
 
     assertBodyEquals(
@@ -401,7 +401,8 @@
   public void ccByEmail() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewerByEmail(new Address("j.doe@gerritcodereview.com"), ReviewerStateInternal.CC);
+    update.putReviewerByEmail(
+        Address.create("j.doe@gerritcodereview.com"), ReviewerStateInternal.CC);
     update.commit();
 
     assertBodyEquals(
diff --git a/plugins/replication b/plugins/replication
index 864c077..841ed8f 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 864c077e5e13ebbae5e7d0a3abc95fc8ae3fdc8b
+Subproject commit 841ed8fe0417a8aef985d1dd4bfd368d9a00b355
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index b249d78..fc24278 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -21,7 +21,7 @@
 module.exports = {
   "extends": ["eslint:recommended", "google"],
   "parserOptions": {
-    "ecmaVersion": 8,
+    "ecmaVersion": 9,
     "sourceType": "module"
   },
   "env": {
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index e9b2043..cb21a9f 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -103,7 +103,6 @@
 import '../../scripts/bundled-polymer.js';
 
 import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
-import '../../types/polymer-behaviors.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
 const DOC_ONLY = 'DOC_ONLY';
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 08b25ca..e0c70a4 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
@@ -44,7 +44,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
-import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
 import './gr-file-list.js';
 import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -1506,7 +1506,6 @@
     ];
 
     const setupDiff = function(diff) {
-      const mock = document.createElement('mock-diff-response');
       diff.comments = {
         left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [],
         right: [],
@@ -1534,7 +1533,7 @@
         theme: 'DEFAULT',
         ignore_whitespace: 'IGNORE_NONE',
       };
-      diff.diff = mock.diffResponse;
+      diff.diff = getMockDiffResponse();
       diff.$.diff.flushDebouncer('renderDiffTable');
     };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
index 4abdb615..77c72d4 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
@@ -14,41 +14,42 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
 
-  class CommentApiMock extends Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element)) {
-    static get is() { return 'comment-api-mock'; }
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 
-    static get properties() {
-      return {
-        _changeComments: Object,
-      };
-    }
+class CommentApiMock extends GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement)) {
+  static get is() { return 'comment-api-mock'; }
 
-    loadComments() {
-      return this._reloadComments();
-    }
-
-    /**
-     * For the purposes of the mock, _reloadDrafts is not included because its
-     * response is the same type as reloadComments, just makes less API
-     * requests. Since this is for test purposes/mocked data anyway, keep this
-     * file simpler by just using _reloadComments here instead.
-     */
-    _reloadDraftsWithCallback(e) {
-      return this._reloadComments().then(() => e.detail.resolve());
-    }
-
-    _reloadComments() {
-      return this.$.commentAPI.loadAll(this._changeNum)
-          .then(comments => {
-            this._changeComments = this.$.commentAPI._changeComments;
-          });
-    }
+  static get properties() {
+    return {
+      _changeComments: Object,
+    };
   }
 
-  customElements.define(CommentApiMock.is, CommentApiMock);
-})();
+  loadComments() {
+    return this._reloadComments();
+  }
+
+  /**
+   * For the purposes of the mock, _reloadDrafts is not included because its
+   * response is the same type as reloadComments, just makes less API
+   * requests. Since this is for test purposes/mocked data anyway, keep this
+   * file simpler by just using _reloadComments here instead.
+   */
+  _reloadDraftsWithCallback(e) {
+    return this._reloadComments().then(() => e.detail.resolve());
+  }
+
+  _reloadComments() {
+    return this.$.commentAPI.loadAll(this._changeNum)
+        .then(comments => {
+          this._changeComments = this.$.commentAPI._changeComments;
+        });
+  }
+}
+
+customElements.define(CommentApiMock.is, CommentApiMock);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index dabf884d..0edf9b0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -51,7 +51,7 @@
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
 import './gr-diff-builder-element.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
@@ -898,7 +898,7 @@
 
     setup(done => {
       element = fixture('mock-diff');
-      diff = document.createElement('mock-diff-response').diffResponse;
+      diff = getMockDiffResponse();
       element.diff = diff;
 
       prefs = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index d4a9ce1..4577696 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -26,7 +26,6 @@
 
 <test-fixture id="basic">
   <template>
-    <mock-diff-response></mock-diff-response>
     <gr-diff></gr-diff>
     <gr-diff-cursor></gr-diff-cursor>
     <gr-rest-api-interface></gr-rest-api-interface>
@@ -44,22 +43,20 @@
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 suite('gr-diff-cursor tests', () => {
   let sandbox;
   let cursorElement;
   let diffElement;
-  let mockDiffResponse;
 
   setup(done => {
     sandbox = sinon.sandbox.create();
 
     const fixtureElems = fixture('basic');
-    mockDiffResponse = fixtureElems[0];
-    diffElement = fixtureElems[1];
-    cursorElement = fixtureElems[2];
-    const restAPI = fixtureElems[3];
+    diffElement = fixtureElems[0];
+    cursorElement = fixtureElems[1];
+    const restAPI = fixtureElems[2];
 
     // Register the diff with the cursor.
     cursorElement.push('diffs', diffElement);
@@ -81,7 +78,7 @@
 
     restAPI.getDiffPreferences().then(prefs => {
       diffElement.prefs = prefs;
-      diffElement.diff = mockDiffResponse.diffResponse;
+      diffElement.diff = getMockDiffResponse();
     });
   });
 
@@ -262,7 +259,7 @@
       done();
     }
     diffElement.addEventListener('render', renderHandler);
-    diffElement._diffChanged(mockDiffResponse.diffResponse);
+    diffElement._diffChanged(getMockDiffResponse());
   });
 
   test('initialLineNumber provided', done => {
@@ -285,7 +282,7 @@
     cursorElement.initialLineNumber = 10;
     cursorElement.side = 'right';
 
-    diffElement._diffChanged(mockDiffResponse.diffResponse);
+    diffElement._diffChanged(getMockDiffResponse());
   });
 
   test('getTargetDiffElement', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index e6e8ee8..f7769f5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -34,7 +34,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
 import './gr-diff.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
@@ -235,9 +235,8 @@
       element.patchRange = {basePatchNum: 1, patchNum: 2};
       element.path = 'file.txt';
 
-      const mock = document.createElement('mock-diff-response');
       element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
-          mock.diffResponse, Object.assign({}, MINIMAL_PREFS));
+          getMockDiffResponse(), Object.assign({}, MINIMAL_PREFS));
 
       // No thread groups.
       assert.isNotOk(element._getThreadGroupForLine(contentEl));
@@ -597,8 +596,7 @@
 
     suite('getCursorStops', () => {
       const setupDiff = function() {
-        const mock = document.createElement('mock-diff-response');
-        element.diff = mock.diffResponse;
+        element.diff = getMockDiffResponse();
         element.prefs = {
           context: 10,
           tab_size: 8,
@@ -777,9 +775,8 @@
                 new CustomEvent('render', {bubbles: true, composed: true}));
             return Promise.resolve({});
           });
-      const mock = document.createElement('mock-diff-response');
       sandbox.stub(element, 'getDiffLength').returns(10000);
-      element.diff = mock.diffResponse;
+      element.diff = getMockDiffResponse();
       element.noRenderOnPrefsChange = true;
     });
 
@@ -1074,7 +1071,7 @@
   });
 
   test('getDiffLength', () => {
-    const diff = document.createElement('mock-diff-response').diffResponse;
+    const diff = getMockDiffResponse();
     assert.equal(element.getDiffLength(diff), 52);
   });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index d0334e7..af2458a 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -42,7 +42,6 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../gr-comment-api/gr-comment-api.js';
-import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
 import '../../shared/revision-info/revision-info.js';
 import './gr-patch-range-select.js';
 import '../gr-comment-api/gr-comment-api-mock_test.js';
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 36af6ae..07f95f4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -32,7 +32,7 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
 import './gr-syntax-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
@@ -65,8 +65,7 @@
   setup(() => {
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
-    const mock = document.createElement('mock-diff-response');
-    diff = mock.diffResponse;
+    diff = getMockDiffResponse();
     element.diff = diff;
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 521dcd1..46e8829 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -28,7 +28,6 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 /**
- * @appliesMixin Polymer.IronFitMixin
  * @extends Polymer.Element
  */
 class GrAutocompleteDropdown extends mixinBehaviors( [
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
deleted file mode 100644
index 015d71e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
+++ /dev/null
@@ -1,168 +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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<dom-module id="mock-diff-response">
-  <template></template>
-  <script>
-    (function() {
-      'use strict';
-
-      const RESPONSE = {
-        meta_a: {
-          name: 'lorem-ipsum.txt',
-          content_type: 'text/plain',
-          lines: 45,
-        },
-        meta_b: {
-          name: 'lorem-ipsum.txt',
-          content_type: 'text/plain',
-          lines: 48,
-        },
-        intraline_status: 'OK',
-        change_type: 'MODIFIED',
-        diff_header: [
-          'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
-          'index b2adcf4..554ae49 100644',
-          '--- a/lorem-ipsum.txt',
-          '+++ b/lorem-ipsum.txt',
-        ],
-        content: [
-          {
-            ab: [
-              'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
-                'nulla phasellus.',
-              'Mattis lectus.',
-              'Sodales duis.',
-              'Orci a faucibus.',
-            ],
-          },
-          {
-            b: [
-              'Nullam neque, ligula ac, id blandit.',
-              'Sagittis tincidunt torquent, tempor nunc amet.',
-              'At rhoncus id.',
-            ],
-          },
-          {
-            ab: [
-              'Sem nascetur, erat ut, non in.',
-              'A donec, venenatis pellentesque dis.',
-              'Mauris mauris.',
-              'Quisque nisl duis, facilisis viverra.',
-              'Justo purus, semper eget et.',
-            ],
-          },
-          {
-            a: [
-              'Est amet, vestibulum pellentesque.',
-              'Erat ligula.',
-              'Justo eros.',
-              'Fringilla quisque.',
-            ],
-          },
-          {
-            ab: [
-              'Arcu eget, rhoncus amet cursus, ipsum elementum.',
-              'Eros suspendisse.',
-            ],
-          },
-          {
-            a: [
-              'Rhoncus tempor, ultricies aliquam ipsum.',
-            ],
-            b: [
-              'Rhoncus tempor, ultricies praesent ipsum.',
-            ],
-            edit_a: [
-              [
-                26,
-                7,
-              ],
-            ],
-            edit_b: [
-              [
-                26,
-                8,
-              ],
-            ],
-          },
-          {
-            ab: [
-              'Sollicitudin duis.',
-              'Blandit blandit, ante nisl fusce.',
-              'Felis ac at, tellus consectetuer.',
-              'Sociis ligula sapien, egestas leo.',
-              'Cum pulvinar, sed mauris, cursus neque velit.',
-              'Augue porta lobortis.',
-              'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
-              'Id quam ipsum, id urna et, massa suspendisse.',
-              'Ac nec, nibh praesent.',
-              'Rutrum vestibulum.',
-              'Est tellus, bibendum habitasse.',
-              'Justo facilisis, vel nulla.',
-              'Donec eu, vulputate neque aliquam, nulla dui.',
-              'Risus adipiscing in.',
-              'Lacus arcu arcu.',
-              'Urna velit.',
-              'Urna a dolor.',
-              'Lectus magna augue, convallis mattis tortor, sed tellus ' +
-                'consequat.',
-              'Etiam dui, blandit wisi.',
-              'Mi nec.',
-              'Vitae eget vestibulum.',
-              'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
-              'Ac eget.',
-              'Vel fringilla, interdum pellentesque placerat, proin ante.',
-            ],
-          },
-          {
-            b: [
-              'Eu congue risus.',
-              'Enim ac, quis elementum.',
-              'Non et elit.',
-              'Etiam aliquam, diam vel nunc.',
-            ],
-          },
-          {
-            ab: [
-              'Nec at.',
-              'Arcu mauris, venenatis lacus fermentum, praesent duis.',
-              'Pellentesque amet et, tellus duis.',
-              'Ipsum arcu vitae, justo elit, sed libero tellus.',
-              'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
-            ],
-          },
-        ],
-      };
-
-      Polymer({
-        is: 'mock-diff-response',
-
-        properties: {
-          diffResponse: {
-            type: Object,
-            value() {
-              return RESPONSE;
-            },
-          },
-        },
-      });
-    })();
-  </script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
deleted file mode 100644
index e7bc662..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
+++ /dev/null
@@ -1,166 +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.
- */
-import '../../../scripts/bundled-polymer.js';
-
-import '../../../test/common-test-setup.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-const RESPONSE = {
-  meta_a: {
-    name: 'lorem-ipsum.txt',
-    content_type: 'text/plain',
-    lines: 45,
-  },
-  meta_b: {
-    name: 'lorem-ipsum.txt',
-    content_type: 'text/plain',
-    lines: 48,
-  },
-  intraline_status: 'OK',
-  change_type: 'MODIFIED',
-  diff_header: [
-    'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
-    'index b2adcf4..554ae49 100644',
-    '--- a/lorem-ipsum.txt',
-    '+++ b/lorem-ipsum.txt',
-  ],
-  content: [
-    {
-      ab: [
-        'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
-          'nulla phasellus.',
-        'Mattis lectus.',
-        'Sodales duis.',
-        'Orci a faucibus.',
-      ],
-    },
-    {
-      b: [
-        'Nullam neque, ligula ac, id blandit.',
-        'Sagittis tincidunt torquent, tempor nunc amet.',
-        'At rhoncus id.',
-      ],
-    },
-    {
-      ab: [
-        'Sem nascetur, erat ut, non in.',
-        'A donec, venenatis pellentesque dis.',
-        'Mauris mauris.',
-        'Quisque nisl duis, facilisis viverra.',
-        'Justo purus, semper eget et.',
-      ],
-    },
-    {
-      a: [
-        'Est amet, vestibulum pellentesque.',
-        'Erat ligula.',
-        'Justo eros.',
-        'Fringilla quisque.',
-      ],
-    },
-    {
-      ab: [
-        'Arcu eget, rhoncus amet cursus, ipsum elementum.',
-        'Eros suspendisse.',
-      ],
-    },
-    {
-      a: [
-        'Rhoncus tempor, ultricies aliquam ipsum.',
-      ],
-      b: [
-        'Rhoncus tempor, ultricies praesent ipsum.',
-      ],
-      edit_a: [
-        [
-          26,
-          7,
-        ],
-      ],
-      edit_b: [
-        [
-          26,
-          8,
-        ],
-      ],
-    },
-    {
-      ab: [
-        'Sollicitudin duis.',
-        'Blandit blandit, ante nisl fusce.',
-        'Felis ac at, tellus consectetuer.',
-        'Sociis ligula sapien, egestas leo.',
-        'Cum pulvinar, sed mauris, cursus neque velit.',
-        'Augue porta lobortis.',
-        'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
-        'Id quam ipsum, id urna et, massa suspendisse.',
-        'Ac nec, nibh praesent.',
-        'Rutrum vestibulum.',
-        'Est tellus, bibendum habitasse.',
-        'Justo facilisis, vel nulla.',
-        'Donec eu, vulputate neque aliquam, nulla dui.',
-        'Risus adipiscing in.',
-        'Lacus arcu arcu.',
-        'Urna velit.',
-        'Urna a dolor.',
-        'Lectus magna augue, convallis mattis tortor, sed tellus ' +
-          'consequat.',
-        'Etiam dui, blandit wisi.',
-        'Mi nec.',
-        'Vitae eget vestibulum.',
-        'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
-        'Ac eget.',
-        'Vel fringilla, interdum pellentesque placerat, proin ante.',
-      ],
-    },
-    {
-      b: [
-        'Eu congue risus.',
-        'Enim ac, quis elementum.',
-        'Non et elit.',
-        'Etiam aliquam, diam vel nunc.',
-      ],
-    },
-    {
-      ab: [
-        'Nec at.',
-        'Arcu mauris, venenatis lacus fermentum, praesent duis.',
-        'Pellentesque amet et, tellus duis.',
-        'Ipsum arcu vitae, justo elit, sed libero tellus.',
-        'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
-      ],
-    },
-  ],
-};
-
-Polymer({
-  _template: html`
-
-`,
-
-  is: 'mock-diff-response',
-
-  properties: {
-    diffResponse: {
-      type: Object,
-      value() {
-        return RESPONSE;
-      },
-    },
-  },
-});
diff --git a/polygerrit-ui/app/test/mock-diff-response.js b/polygerrit-ui/app/test/mock-diff-response.js
new file mode 100644
index 0000000..8ca44c2
--- /dev/null
+++ b/polygerrit-ui/app/test/mock-diff-response.js
@@ -0,0 +1,149 @@
+/**
+ * @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.
+ */
+
+export function getMockDiffResponse() {
+  // Return new response, so tests can't affect each other - if a test somehow
+  // modifies it, the future calls return original value
+  // Do not put it to a const outside of a method
+  return {
+    meta_a: {
+      name: 'lorem-ipsum.txt',
+      content_type: 'text/plain',
+      lines: 45,
+    },
+    meta_b: {
+      name: 'lorem-ipsum.txt',
+      content_type: 'text/plain',
+      lines: 48,
+    },
+    intraline_status: 'OK',
+    change_type: 'MODIFIED',
+    diff_header: [
+      'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+      'index b2adcf4..554ae49 100644',
+      '--- a/lorem-ipsum.txt',
+      '+++ b/lorem-ipsum.txt',
+    ],
+    content: [
+      {
+        ab: [
+          'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
+          'nulla phasellus.',
+          'Mattis lectus.',
+          'Sodales duis.',
+          'Orci a faucibus.',
+        ],
+      },
+      {
+        b: [
+          'Nullam neque, ligula ac, id blandit.',
+          'Sagittis tincidunt torquent, tempor nunc amet.',
+          'At rhoncus id.',
+        ],
+      },
+      {
+        ab: [
+          'Sem nascetur, erat ut, non in.',
+          'A donec, venenatis pellentesque dis.',
+          'Mauris mauris.',
+          'Quisque nisl duis, facilisis viverra.',
+          'Justo purus, semper eget et.',
+        ],
+      },
+      {
+        a: [
+          'Est amet, vestibulum pellentesque.',
+          'Erat ligula.',
+          'Justo eros.',
+          'Fringilla quisque.',
+        ],
+      },
+      {
+        ab: [
+          'Arcu eget, rhoncus amet cursus, ipsum elementum.',
+          'Eros suspendisse.',
+        ],
+      },
+      {
+        a: [
+          'Rhoncus tempor, ultricies aliquam ipsum.',
+        ],
+        b: [
+          'Rhoncus tempor, ultricies praesent ipsum.',
+        ],
+        edit_a: [
+          [
+            26,
+            7,
+          ],
+        ],
+        edit_b: [
+          [
+            26,
+            8,
+          ],
+        ],
+      },
+      {
+        ab: [
+          'Sollicitudin duis.',
+          'Blandit blandit, ante nisl fusce.',
+          'Felis ac at, tellus consectetuer.',
+          'Sociis ligula sapien, egestas leo.',
+          'Cum pulvinar, sed mauris, cursus neque velit.',
+          'Augue porta lobortis.',
+          'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
+          'Id quam ipsum, id urna et, massa suspendisse.',
+          'Ac nec, nibh praesent.',
+          'Rutrum vestibulum.',
+          'Est tellus, bibendum habitasse.',
+          'Justo facilisis, vel nulla.',
+          'Donec eu, vulputate neque aliquam, nulla dui.',
+          'Risus adipiscing in.',
+          'Lacus arcu arcu.',
+          'Urna velit.',
+          'Urna a dolor.',
+          'Lectus magna augue, convallis mattis tortor, sed tellus ' +
+          'consequat.',
+          'Etiam dui, blandit wisi.',
+          'Mi nec.',
+          'Vitae eget vestibulum.',
+          'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
+          'Ac eget.',
+          'Vel fringilla, interdum pellentesque placerat, proin ante.',
+        ],
+      },
+      {
+        b: [
+          'Eu congue risus.',
+          'Enim ac, quis elementum.',
+          'Non et elit.',
+          'Etiam aliquam, diam vel nunc.',
+        ],
+      },
+      {
+        ab: [
+          'Nec at.',
+          'Arcu mauris, venenatis lacus fermentum, praesent duis.',
+          'Pellentesque amet et, tellus duis.',
+          'Ipsum arcu vitae, justo elit, sed libero tellus.',
+          'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
+        ],
+      },
+    ],
+  };
+}
diff --git a/polygerrit-ui/app/types/polymer-behaviors.js b/polygerrit-ui/app/types/polymer-behaviors.js
deleted file mode 100644
index 30b5f5d..0000000
--- a/polygerrit-ui/app/types/polymer-behaviors.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * @license
- * 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.
- */
-
-/**
- * For the purposes of template type checking, externs should be added for
- * anything set on the window object. Note that sub-properties of these
- * declared properties are considered something separate.
- *
- * This file is only for template type checking, not used in Gerrit code.
- */
-
-/* eslint-disable no-var */
-/* eslint-disable no-unused-vars */
-
-function PolymerMixins() {
-  // This function must not be called.
-  // Due to an issue in polymer linter the linter can't
-  // process correctly some behaviors from Polymer library.
-  // To workaround this issue, here we define a minimal mixin to allow
-  // linter process our code correctly. You can add more properties to mixins
-  // if needed.
-
-  // Important! Use mixins from these file only inside JSDoc comments.
-  // Do not use it in the real code
-
-  /**
-   * @polymer
-   * @mixinFunction
-   * */
-  Polymer.IronFitMixin = base =>
-    class extends base {
-      static get properties() {
-        return {
-          positionTarget: Object,
-        };
-      }
-    };
-
-  /**
-   * @polymerBehavior
-   * */
-  Polymer.IronA11yKeysBehavior = [];
-}
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index b428a2d..5440b88 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -506,6 +506,16 @@
             cmd = "sed 's/<script src=\"" + name + "_combined.js\"/<script src=\"" + plugin_name + ".js\"/g' $(SRCS) > $(OUTS)",
             output_to_bindir = True,
         )
+    else:
+        # For polymer 3 migration, we will only have js plugins, in case server side
+        # is still asking for *.html, we still want to create a html placeholder just to load the js
+        # TODO(taoalpha): this should be cleaned up once polymer 3 plugins are the only ones gerrit supports
+        native.genrule(
+            name = name + "_rename_html",
+            outs = [plugin_name + ".html"],
+            cmd = "echo \"<script src='" + plugin_name + ".js'></script>\" > $(OUTS)",
+            output_to_bindir = True,
+        )
 
     native.genrule(
         name = name + "_rename_js",
@@ -515,10 +525,7 @@
         output_to_bindir = True,
     )
 
-    if html_plugin:
-        static_files = [plugin_name + ".js", plugin_name + ".html"]
-    else:
-        static_files = [plugin_name + ".js"]
+    static_files = [plugin_name + ".js", plugin_name + ".html"]
 
     if assets:
         nested, direct = [], []