Merge "Changes to font sizes"
diff --git a/WORKSPACE b/WORKSPACE
index 94138e4..757d86e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -181,8 +181,8 @@
 
 maven_jar(
     name = "gson",
-    artifact = "com.google.code.gson:gson:2.8.2",
-    sha1 = "3edcfe49d2c6053a70a2a47e4e1c2f94998a49cf",
+    artifact = "com.google.code.gson:gson:2.8.4",
+    sha1 = "d0de1ca9b69e69d1d497ee3c6009d015f64dad57",
 )
 
 maven_jar(
@@ -194,8 +194,8 @@
 
 maven_jar(
     name = "protobuf",
-    artifact = "com.google.protobuf:protobuf-java:3.4.0",
-    sha1 = "b32aba0cbe737a4ca953f71688725972e3ee927c",
+    artifact = "com.google.protobuf:protobuf-java:3.5.1",
+    sha1 = "8c3492f7662fa1cbf8ca76a0f5eb1146f7725acd",
 )
 
 load("//lib:guava.bzl", "GUAVA_VERSION", "GUAVA_BIN_SHA1")
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index f380051..13a09a1 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -32,7 +32,6 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import java.io.IOException;
 
 @Singleton
 public class OAuthTokenCache {
@@ -70,12 +69,7 @@
 
     @Override
     public OAuthToken deserialize(byte[] in) {
-      OAuthTokenProto proto;
-      try {
-        proto = OAuthTokenProto.parseFrom(in);
-      } catch (IOException e) {
-        throw new IllegalArgumentException("failed to deserialize OAuthToken");
-      }
+      OAuthTokenProto proto = ProtoCacheSerializers.parseUnchecked(OAuthTokenProto.parser(), in);
       return new OAuthToken(
           proto.getToken(),
           proto.getSecret(),
diff --git a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java b/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
index 9fe6b83..c6fc0b9 100644
--- a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
+++ b/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
@@ -14,11 +14,16 @@
 
 package com.google.gerrit.server.cache;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.CodedOutputStream;
 import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
 import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
 
 /** Static utilities for writing protobuf-based {@link CacheSerializer} implementations. */
 public class ProtoCacheSerializers {
@@ -68,5 +73,55 @@
     }
   }
 
+  /**
+   * Parses a byte array to a protobuf message.
+   *
+   * @param parser parser for the proto type.
+   * @param in byte array with the message contents.
+   * @return parsed proto.
+   */
+  public static <M extends MessageLite> M parseUnchecked(Parser<M> parser, byte[] in) {
+    try {
+      return parser.parseFrom(in);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("exception parsing byte array to proto", e);
+    }
+  }
+
+  /**
+   * Helper for serializing {@link ObjectId} instances to/from protobuf fields.
+   *
+   * <p>Reuse a single instance's {@link #toByteString(ObjectId)} and {@link
+   * #fromByteString(ByteString)} within a single {@link CacheSerializer#serialize} or {@link
+   * CacheSerializer#deserialize} method body to minimize allocation of temporary buffers.
+   *
+   * <p><strong>Note:</strong> This class is not threadsafe. Instances must not be stored in {@link
+   * CacheSerializer} fields if the serializer instances will be used from multiple threads.
+   */
+  public static class ObjectIdConverter {
+    public static ObjectIdConverter create() {
+      return new ObjectIdConverter();
+    }
+
+    private final byte[] buf = new byte[OBJECT_ID_LENGTH];
+
+    private ObjectIdConverter() {}
+
+    public ByteString toByteString(ObjectId id) {
+      id.copyRawTo(buf, 0);
+      return ByteString.copyFrom(buf);
+    }
+
+    public ObjectId fromByteString(ByteString in) {
+      checkArgument(
+          in.size() == OBJECT_ID_LENGTH,
+          "expected ByteString of length %s: %s",
+          OBJECT_ID_LENGTH,
+          in);
+      in.copyTo(buf, 0);
+      return ObjectId.fromRaw(buf);
+    }
+  }
+
   private ProtoCacheSerializers() {}
 }
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index a4eb90f..f3ab847 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.EnumCacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -41,8 +42,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -51,8 +50,8 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
@@ -106,7 +105,7 @@
         ObjectId prior,
         ObjectId next) {
       try {
-        Key key = new Key(prior, next, useRecursiveMerge);
+        Key key = Key.create(prior, next, useRecursiveMerge);
         return new Loader(key, repoManager, project, rw, repoConfig).call();
       } catch (IOException e) {
         log.warn(
@@ -127,78 +126,44 @@
     }
   }
 
-  public static class Key {
-    private transient ObjectId prior;
-    private transient ObjectId next;
-    private transient String strategyName;
-
-    private Key(ObjectId prior, ObjectId next, boolean useRecursiveMerge) {
-      checkNotNull(next, "next");
-      String strategyName = MergeUtil.mergeStrategyName(true, useRecursiveMerge);
-      this.prior = prior.copy();
-      this.next = next.copy();
-      this.strategyName = strategyName;
+  @AutoValue
+  public abstract static class Key {
+    public static Key create(AnyObjectId prior, AnyObjectId next, String strategyName) {
+      return new AutoValue_ChangeKindCacheImpl_Key(prior.copy(), next.copy(), strategyName);
     }
 
-    public Key(ObjectId prior, ObjectId next, String strategyName) {
-      this.prior = prior;
-      this.next = next;
-      this.strategyName = strategyName;
+    private static Key create(AnyObjectId prior, AnyObjectId next, boolean useRecursiveMerge) {
+      return create(prior, next, MergeUtil.mergeStrategyName(true, useRecursiveMerge));
     }
 
-    public ObjectId getPrior() {
-      return prior;
-    }
+    public abstract ObjectId prior();
 
-    public ObjectId getNext() {
-      return next;
-    }
+    public abstract ObjectId next();
 
-    public String getStrategyName() {
-      return strategyName;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof Key) {
-        Key k = (Key) o;
-        return Objects.equals(prior, k.prior)
-            && Objects.equals(next, k.next)
-            && Objects.equals(strategyName, k.strategyName);
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(prior, next, strategyName);
-    }
+    public abstract String strategyName();
 
     @VisibleForTesting
     static class Serializer implements CacheSerializer<Key> {
       @Override
       public byte[] serialize(Key object) {
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        ChangeKindKeyProto.Builder b = ChangeKindKeyProto.newBuilder();
-        object.getPrior().copyRawTo(buf, 0);
-        b.setPrior(ByteString.copyFrom(buf));
-        object.getNext().copyRawTo(buf, 0);
-        b.setNext(ByteString.copyFrom(buf));
-        b.setStrategyName(object.getStrategyName());
-        return ProtoCacheSerializers.toByteArray(b.build());
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
+        return ProtoCacheSerializers.toByteArray(
+            ChangeKindKeyProto.newBuilder()
+                .setPrior(idConverter.toByteString(object.prior()))
+                .setNext(idConverter.toByteString(object.next()))
+                .setStrategyName(object.strategyName())
+                .build());
       }
 
       @Override
       public Key deserialize(byte[] in) {
-        try {
-          ChangeKindKeyProto proto = ChangeKindKeyProto.parseFrom(in);
-          return new Key(
-              ObjectId.fromRaw(proto.getPrior().toByteArray()),
-              ObjectId.fromRaw(proto.getNext().toByteArray()),
-              proto.getStrategyName());
-        } catch (InvalidProtocolBufferException e) {
-          throw new IllegalArgumentException("Failed to deserialize object", e);
-        }
+        ChangeKindKeyProto proto =
+            ProtoCacheSerializers.parseUnchecked(ChangeKindKeyProto.parser(), in);
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
+        return create(
+            idConverter.fromByteString(proto.getPrior()),
+            idConverter.fromByteString(proto.getNext()),
+            proto.getStrategyName());
       }
     }
   }
@@ -231,7 +196,7 @@
     @SuppressWarnings("resource") // Resources are manually managed.
     @Override
     public ChangeKind call() throws IOException {
-      if (Objects.equals(key.prior, key.next)) {
+      if (Objects.equals(key.prior(), key.next())) {
         return ChangeKind.NO_CODE_CHANGE;
       }
 
@@ -244,9 +209,9 @@
         config = repo.getConfig();
       }
       try {
-        RevCommit prior = rw.parseCommit(key.prior);
+        RevCommit prior = rw.parseCommit(key.prior());
         rw.parseBody(prior);
-        RevCommit next = rw.parseCommit(key.next);
+        RevCommit next = rw.parseCommit(key.next());
         rw.parseBody(next);
 
         if (!next.getFullMessage().equals(prior.getFullMessage())) {
@@ -277,7 +242,7 @@
         // having the same tree as would exist when the prior commit is
         // cherry-picked onto the next commit's new first parent.
         try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
-          ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName);
+          ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName());
           merger.setBase(prior.getParent(0));
           if (merger.merge(next.getParent(0), prior)
               && merger.getResultTreeId().equals(next.getTree())) {
@@ -321,7 +286,7 @@
     }
 
     private static boolean isSameDeltaAndTree(RevCommit prior, RevCommit next) {
-      if (next.getTree() != prior.getTree()) {
+      if (!Objects.equals(next.getTree(), prior.getTree())) {
         return false;
       }
 
@@ -334,7 +299,7 @@
       // Make sure that the prior/next delta is the same - not just the tree.
       // This is done by making sure that the parent trees are equal.
       for (int i = 0; i < prior.getParentCount(); i++) {
-        if (next.getParent(i).getTree() != prior.getParent(i).getTree()) {
+        if (!Objects.equals(next.getParent(i).getTree(), prior.getParent(i).getTree())) {
           return false;
         }
       }
@@ -347,7 +312,7 @@
     public int weigh(Key key, ChangeKind changeKind) {
       return 16
           + 2 * 36
-          + 2 * key.strategyName.length() // Size of Key, 64 bit JVM
+          + 2 * key.strategyName().length() // Size of Key, 64 bit JVM
           + 2 * changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
     }
   }
@@ -377,7 +342,7 @@
       ObjectId prior,
       ObjectId next) {
     try {
-      Key key = new Key(prior, next, useRecursiveMerge);
+      Key key = Key.create(prior, next, useRecursiveMerge);
       return cache.get(key, new Loader(key, repoManager, project, rw, repoConfig));
     } catch (ExecutionException e) {
       log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index a192228..b57be15 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -37,13 +38,10 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -145,33 +143,24 @@
 
       @Override
       public byte[] serialize(EntryKey object) {
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        MergeabilityKeyProto.Builder b = MergeabilityKeyProto.newBuilder();
-        object.getCommit().copyRawTo(buf, 0);
-        b.setCommit(ByteString.copyFrom(buf));
-        object.getInto().copyRawTo(buf, 0);
-        b.setInto(ByteString.copyFrom(buf));
-        b.setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(object.getSubmitType()));
-        b.setMergeStrategy(object.getMergeStrategy());
-        return ProtoCacheSerializers.toByteArray(b.build());
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
+        return ProtoCacheSerializers.toByteArray(
+            MergeabilityKeyProto.newBuilder()
+                .setCommit(idConverter.toByteString(object.getCommit()))
+                .setInto(idConverter.toByteString(object.getInto()))
+                .setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(object.getSubmitType()))
+                .setMergeStrategy(object.getMergeStrategy())
+                .build());
       }
 
       @Override
       public EntryKey deserialize(byte[] in) {
-        MergeabilityKeyProto proto;
-        try {
-          proto = MergeabilityKeyProto.parseFrom(in);
-        } catch (IOException e) {
-          throw new IllegalArgumentException("Failed to deserialize mergeability cache key");
-        }
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        proto.getCommit().copyTo(buf, 0);
-        ObjectId commit = ObjectId.fromRaw(buf);
-        proto.getInto().copyTo(buf, 0);
-        ObjectId into = ObjectId.fromRaw(buf);
+        MergeabilityKeyProto proto =
+            ProtoCacheSerializers.parseUnchecked(MergeabilityKeyProto.parser(), in);
+        ObjectIdConverter idConverter = ObjectIdConverter.create();
         return new EntryKey(
-            commit,
-            into,
+            idConverter.fromByteString(proto.getCommit()),
+            idConverter.fromByteString(proto.getInto()),
             SUBMIT_TYPE_CONVERTER.convert(proto.getSubmitType()),
             proto.getMergeStrategy());
       }
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 3bf89c7..997907e 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -41,9 +41,6 @@
 public class GarbageCollection {
   private static final Logger log = LoggerFactory.getLogger(GarbageCollection.class);
 
-  public static final String LOG_NAME = "gc_log";
-  private static final Logger gcLog = LoggerFactory.getLogger(LOG_NAME);
-
   private final GitRepositoryManager repoManager;
   private final GarbageCollectionQueue gcQueue;
   private final GcConfig gcConfig;
@@ -142,7 +139,7 @@
       }
       b.append(s);
     }
-    gcLog.info(b.toString());
+    log.info(b.toString());
   }
 
   private static void logGcConfiguration(
@@ -182,7 +179,6 @@
     print(writer, "failed.\n\n");
     StringBuilder b = new StringBuilder();
     b.append("[").append(projectName.get()).append("]");
-    gcLog.error(b.toString(), e);
     log.error(b.toString(), e);
   }
 
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java b/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
index e03ef67..8796fdf 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionLogFile.java
@@ -26,6 +26,8 @@
 import org.eclipse.jgit.lib.Config;
 
 public class GarbageCollectionLogFile implements LifecycleListener {
+  private static final String LOG_NAME = "gc_log";
+
   @Inject
   public GarbageCollectionLogFile(SitePaths sitePaths, @GerritServerConfig Config config) {
     if (SystemLog.shouldConfigure()) {
@@ -38,15 +40,20 @@
 
   @Override
   public void stop() {
-    LogManager.getLogger(GarbageCollection.LOG_NAME).removeAllAppenders();
+    LogManager.getLogger(GarbageCollection.class).removeAllAppenders();
+    LogManager.getLogger(GarbageCollectionRunner.class).removeAllAppenders();
   }
 
   private static void initLogSystem(Path logdir, boolean rotate) {
-    Logger gcLogger = LogManager.getLogger(GarbageCollection.LOG_NAME);
+    initGcLogger(logdir, rotate, LogManager.getLogger(GarbageCollection.class));
+    initGcLogger(logdir, rotate, LogManager.getLogger(GarbageCollectionRunner.class));
+  }
+
+  private static void initGcLogger(Path logdir, boolean rotate, Logger gcLogger) {
     gcLogger.removeAllAppenders();
     gcLogger.addAppender(
         SystemLog.createAppender(
-            logdir, GarbageCollection.LOG_NAME, new PatternLayout("[%d] %-5p %x: %m%n"), rotate));
+            logdir, LOG_NAME, new PatternLayout("[%d] %-5p %x: %m%n"), rotate));
     gcLogger.setAdditivity(false);
   }
 }
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
index e4316c5..054e56a 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
@@ -24,7 +24,7 @@
 
 /** Runnable to enable scheduling gc to run periodically */
 public class GarbageCollectionRunner implements Runnable {
-  private static final Logger gcLog = LoggerFactory.getLogger(GarbageCollection.LOG_NAME);
+  private static final Logger log = LoggerFactory.getLogger(GarbageCollectionRunner.class);
 
   static class Lifecycle implements LifecycleListener {
     private final WorkQueue queue;
@@ -61,7 +61,7 @@
 
   @Override
   public void run() {
-    gcLog.info("Triggering gc on all repositories");
+    log.info("Triggering gc on all repositories");
     garbageCollectionFactory.create().run(Lists.newArrayList(projectCache.all()));
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index d1c28c4..06d940e 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -34,7 +35,6 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -42,7 +42,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 @Singleton
@@ -83,28 +82,22 @@
 
       @Override
       public byte[] serialize(Key object) {
-        byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-        object.id().copyRawTo(buf, 0);
         return ProtoCacheSerializers.toByteArray(
             ChangeNotesKeyProto.newBuilder()
                 .setProject(object.project().get())
                 .setChangeId(object.changeId().get())
-                .setId(ByteString.copyFrom(buf))
+                .setId(ObjectIdConverter.create().toByteString(object.id()))
                 .build());
       }
 
       @Override
       public Key deserialize(byte[] in) {
-        ChangeNotesKeyProto proto;
-        try {
-          proto = ChangeNotesKeyProto.parseFrom(in);
-        } catch (IOException e) {
-          throw new IllegalArgumentException("Failed to deserialize " + Key.class.getName());
-        }
+        ChangeNotesKeyProto proto =
+            ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), in);
         return Key.create(
             new Project.NameKey(proto.getProject()),
             new Change.Id(proto.getChangeId()),
-            ObjectId.fromRaw(proto.getId().toByteArray()));
+            ObjectIdConverter.create().fromByteString(proto.getId()));
       }
     }
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 1b09494..3eb06b2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -54,6 +54,7 @@
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
@@ -63,13 +64,11 @@
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gson.Gson;
-import com.google.protobuf.ByteString;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -447,9 +446,7 @@
       checkArgument(object.columns() != null, "ChangeColumns is required in: %s", object);
       ChangeNotesStateProto.Builder b = ChangeNotesStateProto.newBuilder();
 
-      byte[] idBuf = new byte[Constants.OBJECT_ID_LENGTH];
-      object.metaId().copyRawTo(idBuf, 0);
-      b.setMetaId(ByteString.copyFrom(idBuf))
+      b.setMetaId(ObjectIdConverter.create().toByteString(object.metaId()))
           .setChangeId(object.changeId().get())
           .setColumns(toChangeColumnsProto(object.columns()));
 
@@ -555,18 +552,13 @@
 
     @Override
     public ChangeNotesState deserialize(byte[] in) {
-      ChangeNotesStateProto proto;
-      try {
-        proto = ChangeNotesStateProto.parseFrom(in);
-      } catch (IOException e) {
-        throw new IllegalArgumentException(
-            "Failed to deserialize " + ChangeNotesState.class.getName());
-      }
+      ChangeNotesStateProto proto =
+          ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), in);
       Change.Id changeId = new Change.Id(proto.getChangeId());
 
       ChangeNotesState.Builder b =
           builder()
-              .metaId(ObjectId.fromRaw(proto.getMetaId().toByteArray()))
+              .metaId(ObjectIdConverter.create().fromByteString(proto.getMetaId()))
               .changeId(changeId)
               .columns(toChangeColumns(changeId, proto.getColumns()))
               .pastAssignees(
diff --git a/java/com/google/gerrit/server/query/change/ConflictKey.java b/java/com/google/gerrit/server/query/change/ConflictKey.java
index 0101ffe..9daf886 100644
--- a/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -14,62 +14,80 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.extensions.client.SubmitType;
-import java.io.Serializable;
-import java.util.Objects;
+import com.google.gerrit.server.cache.CacheSerializer;
+import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
-public class ConflictKey implements Serializable {
-  private static final long serialVersionUID = 2L;
-
-  private final ObjectId commit;
-  private final ObjectId otherCommit;
-  private final SubmitType submitType;
-  private final boolean contentMerge;
-
-  public ConflictKey(
-      ObjectId commit, ObjectId otherCommit, SubmitType submitType, boolean contentMerge) {
-    if (SubmitType.FAST_FORWARD_ONLY.equals(submitType) || commit.compareTo(otherCommit) < 0) {
-      this.commit = commit;
-      this.otherCommit = otherCommit;
-    } else {
-      this.commit = otherCommit;
-      this.otherCommit = commit;
+@AutoValue
+public abstract class ConflictKey {
+  public static ConflictKey create(
+      AnyObjectId commit, AnyObjectId otherCommit, SubmitType submitType, boolean contentMerge) {
+    ObjectId commitCopy = commit.copy();
+    ObjectId otherCommitCopy = otherCommit.copy();
+    if (submitType == SubmitType.FAST_FORWARD_ONLY) {
+      // The conflict check for FF-only is non-symmetrical, and we need to treat (X, Y) differently
+      // from (Y, X). Store the commits in the input order.
+      return new AutoValue_ConflictKey(commitCopy, otherCommitCopy, submitType, contentMerge);
     }
-    this.submitType = submitType;
-    this.contentMerge = contentMerge;
+    // Otherwise, the check is symmetrical; sort commit/otherCommit before storing, so the actual
+    // key is independent of the order in which they are passed to this method.
+    return new AutoValue_ConflictKey(
+        Ordering.natural().min(commitCopy, otherCommitCopy),
+        Ordering.natural().max(commitCopy, otherCommitCopy),
+        submitType,
+        contentMerge);
   }
 
-  public ObjectId getCommit() {
-    return commit;
+  @VisibleForTesting
+  static ConflictKey createWithoutNormalization(
+      AnyObjectId commit, AnyObjectId otherCommit, SubmitType submitType, boolean contentMerge) {
+    return new AutoValue_ConflictKey(commit.copy(), otherCommit.copy(), submitType, contentMerge);
   }
 
-  public ObjectId getOtherCommit() {
-    return otherCommit;
-  }
+  public abstract ObjectId commit();
 
-  public SubmitType getSubmitType() {
-    return submitType;
-  }
+  public abstract ObjectId otherCommit();
 
-  public boolean isContentMerge() {
-    return contentMerge;
-  }
+  public abstract SubmitType submitType();
 
-  @Override
-  public boolean equals(Object o) {
-    if (!(o instanceof ConflictKey)) {
-      return false;
+  public abstract boolean contentMerge();
+
+  public static enum Serializer implements CacheSerializer<ConflictKey> {
+    INSTANCE;
+
+    private static final Converter<String, SubmitType> SUBMIT_TYPE_CONVERTER =
+        Enums.stringConverter(SubmitType.class);
+
+    @Override
+    public byte[] serialize(ConflictKey object) {
+      ObjectIdConverter idConverter = ObjectIdConverter.create();
+      return ProtoCacheSerializers.toByteArray(
+          ConflictKeyProto.newBuilder()
+              .setCommit(idConverter.toByteString(object.commit()))
+              .setOtherCommit(idConverter.toByteString(object.otherCommit()))
+              .setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(object.submitType()))
+              .setContentMerge(object.contentMerge())
+              .build());
     }
-    ConflictKey other = (ConflictKey) o;
-    return commit.equals(other.commit)
-        && otherCommit.equals(other.otherCommit)
-        && submitType.equals(other.submitType)
-        && contentMerge == other.contentMerge;
-  }
 
-  @Override
-  public int hashCode() {
-    return Objects.hash(commit, otherCommit, submitType, contentMerge);
+    @Override
+    public ConflictKey deserialize(byte[] in) {
+      ConflictKeyProto proto = ProtoCacheSerializers.parseUnchecked(ConflictKeyProto.parser(), in);
+      ObjectIdConverter idConverter = ObjectIdConverter.create();
+      return create(
+          idConverter.fromByteString(proto.getCommit()),
+          idConverter.fromByteString(proto.getOtherCommit()),
+          SUBMIT_TYPE_CONVERTER.convert(proto.getSubmitType()),
+          proto.getContentMerge());
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/query/change/ConflictsCache.java b/java/com/google/gerrit/server/query/change/ConflictsCache.java
index e8b2fef..c7ee79b 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsCache.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsCache.java
@@ -18,7 +18,7 @@
 
 public interface ConflictsCache {
 
-  void put(ConflictKey key, Boolean value);
+  void put(ConflictKey key, boolean value);
 
   @Nullable
   Boolean getIfPresent(ConflictKey key);
diff --git a/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java b/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
index 1185677..0b8c5ee 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.cache.Cache;
+import com.google.gerrit.server.cache.BooleanCacheSerializer;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -29,7 +30,11 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        persist(NAME, ConflictKey.class, Boolean.class).maximumWeight(37400);
+        persist(NAME, ConflictKey.class, Boolean.class)
+            .version(1)
+            .keySerializer(ConflictKey.Serializer.INSTANCE)
+            .valueSerializer(BooleanCacheSerializer.INSTANCE)
+            .maximumWeight(37400);
         bind(ConflictsCache.class).to(ConflictsCacheImpl.class);
       }
     };
@@ -43,7 +48,7 @@
   }
 
   @Override
-  public void put(ConflictKey key, Boolean value) {
+  public void put(ConflictKey key, boolean value) {
     conflictsCache.put(key, value);
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index f870951..7dc7a0b 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -115,19 +115,19 @@
 
       ObjectId other = ObjectId.fromString(object.currentPatchSet().getRevision().get());
       ConflictKey conflictsKey =
-          new ConflictKey(
+          ConflictKey.create(
               changeDataCache.getTestAgainst(),
               other,
               str.type,
               projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
-      Boolean conflicts = args.conflictsCache.getIfPresent(conflictsKey);
-      if (conflicts != null) {
-        return conflicts;
+      Boolean maybeConflicts = args.conflictsCache.getIfPresent(conflictsKey);
+      if (maybeConflicts != null) {
+        return maybeConflicts;
       }
 
       try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
           CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
-        conflicts =
+        boolean conflicts =
             !args.submitDryRun.run(
                 str.type,
                 repo,
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 54ecd18..51a4090 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -95,14 +95,10 @@
       "Submit all ${topicSize} changes of the same topic "
           + "(${submitSize} changes including ancestors and other "
           + "changes related by topic)";
-  private static final String BLOCKED_SUBMIT_TOOLTIP =
-      "This change depends on other changes which are not ready";
   private static final String BLOCKED_HIDDEN_SUBMIT_TOOLTIP =
       "This change depends on other hidden changes which are not ready";
-  private static final String BLOCKED_WORK_IN_PROGRESS = "This change is marked work in progress";
   private static final String CLICK_FAILURE_TOOLTIP = "Clicking the button would fail";
   private static final String CHANGE_UNMERGEABLE = "Problems with integrating this change";
-  private static final String CHANGES_NOT_MERGEABLE = "Problems with change(s): ";
 
   public static class Output {
     transient Change change;
@@ -240,6 +236,11 @@
   }
 
   /**
+   * Returns a message describing what prevents the current change from being submitted - or null.
+   * This method only considers parent changes, and changes in the same topic. The caller is
+   * responsible for making sure the current change to be submitted can indeed be submitted
+   * (permissions, submit rules, is not a WIP...)
+   *
    * @param cd the change the user is currently looking at
    * @param cs set of changes to be submitted at once
    * @param user the user who is checking to submit
@@ -251,6 +252,11 @@
         return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
       }
       for (ChangeData c : cs.changes()) {
+        if (cd.getId().equals(c.getId())) {
+          // We ignore the change about to be submitted, as these checks are already done in the
+          // #apply and #getDescription methods.
+          continue;
+        }
         Set<ChangePermission> can =
             permissionBackend
                 .user(user)
@@ -261,12 +267,16 @@
           return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
         }
         if (!can.contains(ChangePermission.SUBMIT)) {
-          return BLOCKED_SUBMIT_TOOLTIP;
+          return "You don't have permission to submit change " + c.getId();
         }
         if (c.change().isWorkInProgress()) {
-          return BLOCKED_WORK_IN_PROGRESS;
+          return "Change " + c.getId() + " is marked work in progress";
         }
-        MergeOp.checkSubmitRule(c, false);
+        try {
+          MergeOp.checkSubmitRule(c, false);
+        } catch (ResourceConflictException e) {
+          return "Change " + c.getId() + " is not ready: " + e.getMessage();
+        }
       }
 
       Collection<ChangeData> unmergeable = unmergeableChanges(cs);
@@ -278,11 +288,10 @@
             return CHANGE_UNMERGEABLE;
           }
         }
-        return CHANGES_NOT_MERGEABLE
+
+        return "Problems with change(s): "
             + unmergeable.stream().map(c -> c.getId().toString()).collect(joining(", "));
       }
-    } catch (ResourceConflictException e) {
-      return BLOCKED_SUBMIT_TOOLTIP;
     } catch (PermissionBackendException | OrmException | IOException e) {
       log.error("Error checking if change is submittable", e);
       throw new OrmRuntimeException("Could not determine problems for the change", e);
@@ -294,6 +303,7 @@
   public UiAction.Description getDescription(RevisionResource resource) {
     Change change = resource.getChange();
     if (!change.getStatus().isOpen()
+        || change.isWorkInProgress()
         || !resource.isCurrent()
         || !resource.permissions().testOrFalse(ChangePermission.SUBMIT)) {
       return null; // submit not visible
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
index 4b6f8b2..41640a9 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -80,6 +80,9 @@
                   .flatMap(g -> g.members.stream())
                   .map(a -> a._accountId))
           .containsExactly(adminId.get());
+      // Query project index
+      assertThat(gApi.projects().query(project.get()).get().stream().map(p -> p.name))
+          .containsExactly(project.get());
     }
   }
 
@@ -220,7 +223,7 @@
   }
 
   private void setUpChange() throws Exception {
-    project = new Project.NameKey("project");
+    project = new Project.NameKey("reindex-project-test");
     try (ServerContext ctx = startServer()) {
       GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class);
       gApi.projects().create(project.get());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index f89f2a1..171babd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -105,7 +105,9 @@
   public void revisionActionsTwoChangesInTopic() throws Exception {
     String changeId = createChangeWithTopic().getChangeId();
     approve(changeId);
-    String changeId2 = createChangeWithTopic().getChangeId();
+    PushOneCommit.Result change2 = createChangeWithTopic();
+    int legacyId2 = change2.getChange().getId().get();
+    String changeId2 = change2.getChangeId();
     Map<String, ActionInfo> actions = getActions(changeId);
     commonActionsAssertions(actions);
     if (isSubmitWholeTopicEnabled()) {
@@ -113,7 +115,7 @@
       assertThat(info.enabled).isNull();
       assertThat(info.label).isEqualTo("Submit whole topic");
       assertThat(info.method).isEqualTo("POST");
-      assertThat(info.title).isEqualTo("This change depends on other changes which are not ready");
+      assertThat(info.title).matches("Change " + legacyId2 + " is not ready: needs Code-Review");
     } else {
       noSubmitWholeTopicAssertions(actions, 1);
 
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 586c065..5e93a09 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -1,6 +1,7 @@
 package com.google.gerrit.server.auth.oauth;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index 278330b..ab88169 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -5,12 +5,16 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/cache/testing",
         "//lib:guava",
         "//lib:gwtorm",
         "//lib:junit",
         "//lib:protobuf",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java b/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java
new file mode 100644
index 0000000..8bf9762
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java
@@ -0,0 +1,116 @@
+// 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.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ProtoCacheSerializersTest {
+  @Test
+  public void objectIdFromByteString() {
+    ObjectIdConverter idConverter = ObjectIdConverter.create();
+    assertThat(
+            idConverter.fromByteString(
+                bytes(
+                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+                    0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa)))
+        .isEqualTo(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
+    assertThat(
+            idConverter.fromByteString(
+                bytes(
+                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+                    0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb)))
+        .isEqualTo(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
+  }
+
+  @Test
+  public void objectIdFromByteStringWrongSize() {
+    try {
+      ObjectIdConverter.create().fromByteString(ByteString.copyFromUtf8("foo"));
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void objectIdToByteString() {
+    ObjectIdConverter idConverter = ObjectIdConverter.create();
+    assertThat(
+            idConverter.toByteString(
+                ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
+        .isEqualTo(
+            bytes(
+                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+                0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa));
+    assertThat(
+            idConverter.toByteString(
+                ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
+        .isEqualTo(
+            bytes(
+                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+                0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb));
+  }
+
+  @Test
+  public void parseUncheckedWrongProtoType() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
+    try {
+      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUncheckedInvalidData() {
+    byte[] bytes = new byte[] {0x00};
+    try {
+      ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), bytes);
+      assert_().fail("expected IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+  }
+
+  @Test
+  public void parseUnchecked() {
+    ChangeNotesKeyProto proto =
+        ChangeNotesKeyProto.newBuilder()
+            .setProject("project")
+            .setChangeId(1234)
+            .setId(ByteString.copyFromUtf8("foo"))
+            .build();
+    byte[] bytes = ProtoCacheSerializers.toByteArray(proto);
+    assertThat(ProtoCacheSerializers.parseUnchecked(ChangeNotesKeyProto.parser(), bytes))
+        .isEqualTo(proto);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
index 5b77094..03e0d4e 100644
--- a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.server.cache.CacheSerializer;
 import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
+import com.google.gerrit.server.change.ChangeKindCacheImpl.Key;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
@@ -28,7 +30,7 @@
   @Test
   public void keySerializer() throws Exception {
     ChangeKindCacheImpl.Key key =
-        new ChangeKindCacheImpl.Key(
+        Key.create(
             ObjectId.zeroId(),
             ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
             "aStrategy");
@@ -54,7 +56,7 @@
   @Test
   public void keyFields() throws Exception {
     assertThatSerializedClass(ChangeKindCacheImpl.Key.class)
-        .hasFields(
+        .hasAutoValueMethods(
             ImmutableMap.of(
                 "prior", ObjectId.class, "next", ObjectId.class, "strategyName", String.class));
   }
diff --git a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
index 69fc531..c8e6f2b 100644
--- a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
 import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index c0f2c43..3d65eae 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
 import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
+import static com.google.gerrit.server.cache.ProtoCacheSerializers.toByteString;
 import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableList;
@@ -40,7 +41,7 @@
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
@@ -50,7 +51,6 @@
 import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
 import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
 import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.StandardKeyEncoder;
 import com.google.inject.TypeLiteral;
 import com.google.protobuf.ByteString;
@@ -58,7 +58,6 @@
 import java.sql.Timestamp;
 import java.util.List;
 import java.util.Map;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
@@ -71,7 +70,7 @@
   private static final Change.Id ID = new Change.Id(123);
   private static final ObjectId SHA =
       ObjectId.fromString("1234567812345678123456781234567812345678");
-  private static final ByteString SHA_BYTES = toByteString(SHA);
+  private static final ByteString SHA_BYTES = ObjectIdConverter.create().toByteString(SHA);
   private static final String CHANGE_KEY = "Iabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
 
   private ChangeColumns cols;
@@ -944,14 +943,4 @@
     // additional assertions if necessary.
     return actual;
   }
-
-  private static ByteString toByteString(ObjectId id) {
-    byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
-    id.copyRawTo(buf, 0);
-    return ByteString.copyFrom(buf);
-  }
-
-  private <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
-    return ProtoCacheSerializers.toByteString(object, codec);
-  }
 }
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 78ec176..09e3243 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -28,13 +28,12 @@
     ],
 )
 
+LUCENE_QUERY_TEST = ["LuceneQueryChangesTest.java"]
+
 junit_tests(
     name = "lucene_query_test",
     size = "large",
-    srcs = glob(
-        ["*.java"],
-        exclude = ABSTRACT_QUERY_TEST,
-    ),
+    srcs = LUCENE_QUERY_TEST,
     visibility = ["//visibility:public"],
     deps = [
         ":abstract_query_tests",
@@ -50,3 +49,26 @@
         "//lib/truth",
     ],
 )
+
+junit_tests(
+    name = "small_tests",
+    size = "small",
+    srcs = glob(
+        ["*.java"],
+        exclude = ABSTRACT_QUERY_TEST + LUCENE_QUERY_TEST,
+    ),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/reviewdb:server",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/cache/testing",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib:gwtorm",
+        "//lib/jgit/org.eclipse.jgit:jgit",
+        "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
new file mode 100644
index 0000000..b87bbf7
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
@@ -0,0 +1,98 @@
+// 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.query.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
+import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ConflictKeyTest {
+  @Test
+  public void ffOnlyPreservesInputOrder() {
+    ObjectId id1 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
+    ObjectId id2 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    ConflictKey id1First = ConflictKey.create(id1, id2, FAST_FORWARD_ONLY, true);
+    ConflictKey id2First = ConflictKey.create(id2, id1, FAST_FORWARD_ONLY, true);
+
+    assertThat(id1First)
+        .isEqualTo(ConflictKey.createWithoutNormalization(id1, id2, FAST_FORWARD_ONLY, true));
+    assertThat(id2First)
+        .isEqualTo(ConflictKey.createWithoutNormalization(id2, id1, FAST_FORWARD_ONLY, true));
+    assertThat(id1First).isNotEqualTo(id2First);
+  }
+
+  @Test
+  public void nonFfOnlyNormalizesInputOrder() {
+    ObjectId id1 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
+    ObjectId id2 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    ConflictKey id1First = ConflictKey.create(id1, id2, MERGE_IF_NECESSARY, true);
+    ConflictKey id2First = ConflictKey.create(id2, id1, MERGE_IF_NECESSARY, true);
+    ConflictKey expected =
+        ConflictKey.createWithoutNormalization(id1, id2, MERGE_IF_NECESSARY, true);
+
+    assertThat(id1First).isEqualTo(expected);
+    assertThat(id2First).isEqualTo(expected);
+  }
+
+  @Test
+  public void serializer() throws Exception {
+    ConflictKey key =
+        ConflictKey.create(
+            ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"),
+            ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+            SubmitType.MERGE_IF_NECESSARY,
+            false);
+    byte[] serialized = ConflictKey.Serializer.INSTANCE.serialize(key);
+    assertThat(ConflictKeyProto.parseFrom(serialized))
+        .isEqualTo(
+            ConflictKeyProto.newBuilder()
+                .setCommit(
+                    bytes(
+                        0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee,
+                        0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee))
+                .setOtherCommit(
+                    bytes(
+                        0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
+                        0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
+                .setSubmitType("MERGE_IF_NECESSARY")
+                .setContentMerge(false)
+                .build());
+    assertThat(ConflictKey.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(key);
+  }
+
+  /**
+   * See {@link com.google.gerrit.server.cache.testing.SerializedClassSubject} for background and
+   * what to do if this test fails.
+   */
+  @Test
+  public void methods() throws Exception {
+    assertThatSerializedClass(ConflictKey.class)
+        .hasAutoValueMethods(
+            ImmutableMap.of(
+                "commit", ObjectId.class,
+                "otherCommit", ObjectId.class,
+                "submitType", SubmitType.class,
+                "contentMerge", boolean.class));
+  }
+}
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index ee50e45..53dccff 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit ee50e45b449e282ed78917175daf8b359da8d943
+Subproject commit 53dccff17c029459999ff70ac886b80626af634b
diff --git a/plugins/download-commands b/plugins/download-commands
index 37219fe..39b9d56 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 37219fe3fd59727af1ac7a3b0ee00a6924ff8e00
+Subproject commit 39b9d56312d505308f54b810e59ff481b9a380aa
diff --git a/plugins/hooks b/plugins/hooks
index da73b23..d497bed 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit da73b23cfb065fc28c9e7653860ccd34bd68f0f0
+Subproject commit d497bed6963134388e7f500364a4ae59b94bafe7
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 4672856..8ddb7e2 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 467285664ebf8eb6f1e03ff13ebc706eee6d8662
+Subproject commit 8ddb7e2ebda8cb7813ae0f3f0b8602e14915e300
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 83f1565..1d3a020 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -103,8 +103,6 @@
       _filesByPath: Object,
       _files: {
         type: Array,
-        computed: '_computeFiles(_filesByPath, changeComments, patchRange, ' +
-            '_reviewed)',
         observer: '_filesChanged',
         value() { return []; },
       },
@@ -183,6 +181,8 @@
 
     observers: [
       '_expandedPathsChanged(_expandedFilePaths.splices)',
+      '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
+          '_loading)',
     ],
 
     keyBindings: {
@@ -782,13 +782,14 @@
           'gr-icons:expand-less' : 'gr-icons:expand-more';
     },
 
-    _computeFiles(filesByPath, changeComments, patchRange, reviewed) {
+    _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
+      // Await all promises resolving from reload. @See Issue 9057
+      if (loading) { return; }
+
       const commentedPaths = changeComments.getPaths(patchRange);
       const files = Object.assign({}, filesByPath);
       Object.keys(commentedPaths).forEach(commentedPath => {
-        if (files.hasOwnProperty(commentedPath)) {
-          return;
-        }
+        if (files.hasOwnProperty(commentedPath)) { return; }
         files[commentedPath] = {status: 'U'};
       });
       const reviewedSet = new Set(reviewed || []);
@@ -797,7 +798,7 @@
         files[filePath].isReviewed = reviewedSet.has(filePath);
       }
 
-      return this._normalizeChangeFilesResponse(files);
+      this._files = this._normalizeChangeFilesResponse(files);
     },
 
     _computeFilesShown(numFilesShown, files) {
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 3c90a1f..0289449 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
@@ -60,9 +60,11 @@
       stub('gr-rest-api-interface', {
         getLoggedIn() { return Promise.resolve(true); },
         getPreferences() { return Promise.resolve({}); },
+        getDiffPreferences() { return Promise.resolve({}); },
         getDiffComments() { return Promise.resolve({}); },
         getDiffRobotComments() { return Promise.resolve({}); },
         getDiffDrafts() { return Promise.resolve({}); },
+        getAccountCapabilities() { return Promise.resolve({}); },
       });
       stub('gr-date-formatter', {
         _loadTimeFormat() { return Promise.resolve(''); },
@@ -85,6 +87,7 @@
             .returns({meta: {}, left: [], right: []});
         done();
       });
+      element._loading = false;
       element.diffPrefs = {};
       element.numFilesShown = 200;
       element.patchRange = {
@@ -1321,6 +1324,7 @@
             .returns({meta: {}, left: [], right: []});
         done();
       });
+      element._loading = false;
       element.numFilesShown = 75;
       element.selectedIndex = 0;
       element._filesByPath = {
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 28ff45d..de92d8a 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -112,7 +112,9 @@
     _getLibRoot() {
       // TODO(wyatta): Remove the remainder of this method logic once the
       // STATIC_RESOURCE_PATH variable is being provided generally.
-      if (window.STATIC_RESOURCE_PATH) { return window.STATIC_RESOURCE_PATH; }
+      if (window.STATIC_RESOURCE_PATH) {
+        return window.STATIC_RESOURCE_PATH + '/';
+      }
 
       if (this._cachedLibRoot) { return this._cachedLibRoot; }
 
diff --git a/proto/cache.proto b/proto/cache.proto
index 7e2e75a..a826f8c 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -184,3 +184,12 @@
   int64 read_only_until = 17;
   bool has_read_only_until = 18;
 }
+
+
+// Serialized form of com.google.gerrit.server.query.change.ConflictKey
+message ConflictKeyProto {
+  bytes commit = 1;
+  bytes other_commit = 2;
+  string submit_type = 3;
+  bool content_merge = 4;
+}