Merge changes from topic "auto-annotations"

* changes:
  ConfigAnnotationParser: Use AutoAnnotation to construct GerritConfig
  Replace ApproveOption with AutoAnnotation
  Move newOption method to a new public class
  CmdLineParser: Replace HelpOption with AutoAnnotation
  CmdLineParser: Replace PrefixedOption with AutoAnnotation
  Implement @Export with AutoAnnotation
diff --git a/.gitignore b/.gitignore
index 8cd43dd..c0e14ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,18 @@
 /local.properties
 /node_modules/
 /package-lock.json
-/plugins/cookbook-plugin/
+/plugins/*
+!/plugins/BUILD
+!/plugins/codemirror-editor
+!/plugins/commit-message-length-validator
+!/plugins/delete-project
+!/plugins/download-commands
+!/plugins/external_plugin_deps.bzl
+!/plugins/gitiles
+!/plugins/hooks
+!/plugins/replication
+!/plugins/reviewnotes
+!/plugins/singleusergroup
+!/plugins/webhooks
 /test_site
 /tools/format
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8479a8e..c0f195d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -979,6 +979,11 @@
 Caches parsed `rules.pl` contents for each project. This cache uses the same
 size as the `projects` cache, and cannot be configured independently.
 
+cache `"pure_revert"`::
++
+Result of checking if one change or commit is a pure/clean revert of
+another.
+
 cache `"sshkeys"`::
 +
 Caches unpacked versions of user SSH keys, so the internal SSH daemon
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 3a80910..81c9927 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -234,6 +234,17 @@
 link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/config.md[
 Configuration]
 
+[[checks]]
+=== checks
+
+The checks plugin provides a REST API and UI extensions for integrating
+CI systems with Gerrit.
+
+link:https://gerrit-review.googlesource.com/admin/repos/plugins/checks[
+Project] |
+link:https://gerrit.googlesource.com/plugins/checks/+doc/master/src/main/resources/Documentation/about.md[
+Plugin Documentation]]
+
 [[egit]]
 === egit
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 1d10025..1e54cbf 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -718,7 +718,6 @@
 
 [source,java]
 ----
-@Singleton
 public class SampleOperator
     implements ChangeQueryBuilder.ChangeOperatorFactory {
   public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
@@ -751,7 +750,6 @@
 new `has:sample_pluginName` operand is shown below:
 
 ====
-  @Singleton
   public class SampleHasOperand implements ChangeHasOperandFactory {
     public static class Module extends AbstractModule {
       @Override
diff --git a/WORKSPACE b/WORKSPACE
index a984c41..b4b1a64 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -156,18 +156,18 @@
     sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
 )
 
-FLOGGER_VERS = "0.3.1"
+FLOGGER_VERS = "0.4"
 
 maven_jar(
     name = "flogger",
     artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
-    sha1 = "585030fe1ec709760cbef997a459729fb965df0e",
+    sha1 = "9c8863dcc913b56291c0c88e6d4ca9715b43df98",
 )
 
 maven_jar(
     name = "flogger-log4j-backend",
     artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
-    sha1 = "d5085e3996bddc4b105d53b886190cc9a8811a9e",
+    sha1 = "17aa5e31daa1354187e14b6978597d630391c028",
 )
 
 maven_jar(
@@ -1054,12 +1054,12 @@
     sha1 = "76716d529710fc03d1d429b43e3cedd4419f78d4",
 )
 
-# When upgrading elasticsearch-rest-client, also upgrade http-niocore
+# When upgrading elasticsearch-rest-client, also upgrade httpcore-nio
 # and httpasyncclient as necessary.
 maven_jar(
     name = "elasticsearch-rest-client",
-    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.6.1",
-    sha1 = "dc1c9284ffca28cd169fae2776c3956e90b76c00",
+    artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.6.2",
+    sha1 = "2c429141e488091c358aa43b1e6873d457464c5d",
 )
 
 JACKSON_VERSION = "2.9.8"
@@ -1070,18 +1070,18 @@
     sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
 )
 
-TESTCONTAINERS_VERSION = "1.10.3"
+TESTCONTAINERS_VERSION = "1.10.7"
 
 maven_jar(
     name = "testcontainers",
     artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
-    sha1 = "e561ce99fc616b383d85f35ce881e58e8de59ae7",
+    sha1 = "e7575fedfd010ca1ad80c8c9bf971a8057b1ff8a",
 )
 
 maven_jar(
     name = "testcontainers-elasticsearch",
     artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
-    sha1 = "0cb114ecba0ed54a116e2be2f031bc45ca4cbfc8",
+    sha1 = "1ee43ebd81aea1f29bf60a56643bad80c134f998",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index 9d62e20..ccd30ab 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.Objects.requireNonNull;
@@ -31,6 +32,8 @@
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.gerrit.sshd.commands.Query;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
 import java.util.List;
@@ -164,6 +167,27 @@
     return pluginInfo.stream().map(MyInfo.class::cast).collect(toImmutableList());
   }
 
+  /**
+   * Decode {@code MyInfo}s from a raw list of maps returned from Gson.
+   *
+   * <p>This method is used instead of decoding {@code ChangeInfo} or {@code ChangAttribute}, since
+   * Gson would decode the {@code plugins} field as a {@code List<PluginDefinedInfo>}, which would
+   * return the base type and silently ignore any fields that are defined only in the subclass.
+   * Instead, decode the enclosing {@code ChangeInfo} or {@code ChangeAttribute} as a raw {@code
+   * Map<String, Object>}, and pass the {@code "plugins"} value to this method.
+   *
+   * @param gson Gson converter.
+   * @param plugins list of {@code MyInfo} objects, each as a raw map returned from Gson.
+   * @return decoded list of {@code MyInfo}s.
+   */
+  protected static List<MyInfo> decodeRawPluginsList(Gson gson, @Nullable Object plugins) {
+    if (plugins == null) {
+      return null;
+    }
+    checkArgument(plugins instanceof List, "not a list: %s", plugins);
+    return gson.fromJson(gson.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
+  }
+
   @FunctionalInterface
   protected interface PluginInfoGetter {
     List<MyInfo> call(Change.Id id) throws Exception;
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index b700835..956ec75 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
@@ -160,6 +161,7 @@
     install(ChangeKindCacheImpl.module());
     install(MergeabilityCacheImpl.module());
     install(TagCache.module());
+    install(PureRevertCache.module());
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ProjectState.Factory.class);
diff --git a/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java b/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java
new file mode 100644
index 0000000..180646b
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/ProtobufSerializer.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize;
+
+import com.google.gerrit.proto.Protos;
+import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+
+/** A CacheSerializer for Protobuf messages. */
+public class ProtobufSerializer<T extends MessageLite> implements CacheSerializer<T> {
+  private final Parser<T> parser;
+
+  public ProtobufSerializer(Parser<T> parser) {
+    this.parser = parser;
+  }
+
+  @Override
+  public byte[] serialize(T object) {
+    return Protos.toByteArray(object);
+  }
+
+  @Override
+  public T deserialize(byte[] in) {
+    return Protos.parseUnchecked(parser, in);
+  }
+}
diff --git a/java/com/google/gerrit/server/change/PureRevert.java b/java/com/google/gerrit/server/change/PureRevert.java
index 0135683..e7fb67a 100644
--- a/java/com/google/gerrit/server/change/PureRevert.java
+++ b/java/com/google/gerrit/server/change/PureRevert.java
@@ -14,109 +14,49 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.common.PureRevertInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
+import java.util.Optional;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
+/** Can check if a change is a pure revert (= a revert with no further modifications). */
 @Singleton
 public class PureRevert {
-  private final MergeUtil.Factory mergeUtilFactory;
-  private final GitRepositoryManager repoManager;
-  private final ProjectCache projectCache;
-  private final ChangeNotes.Factory notesFactory;
-  private final PatchSetUtil psUtil;
+  private final PureRevertCache pureRevertCache;
 
   @Inject
-  PureRevert(
-      MergeUtil.Factory mergeUtilFactory,
-      GitRepositoryManager repoManager,
-      ProjectCache projectCache,
-      ChangeNotes.Factory notesFactory,
-      PatchSetUtil psUtil) {
-    this.mergeUtilFactory = mergeUtilFactory;
-    this.repoManager = repoManager;
-    this.projectCache = projectCache;
-    this.notesFactory = notesFactory;
-    this.psUtil = psUtil;
+  PureRevert(PureRevertCache pureRevertCache) {
+    this.pureRevertCache = pureRevertCache;
   }
 
-  public PureRevertInfo get(ChangeNotes notes, @Nullable String claimedOriginal)
+  public boolean get(ChangeNotes notes, Optional<String> claimedOriginal)
       throws OrmException, IOException, BadRequestException, ResourceConflictException {
-    PatchSet currentPatchSet = psUtil.current(notes);
+    PatchSet currentPatchSet = notes.getCurrentPatchSet();
     if (currentPatchSet == null) {
       throw new ResourceConflictException("current revision is missing");
     }
-
-    if (claimedOriginal == null) {
-      if (notes.getChange().getRevertOf() == null) {
-        throw new BadRequestException("no ID was provided and change isn't a revert");
-      }
-      PatchSet ps =
-          psUtil.current(
-              notesFactory.createChecked(notes.getProjectName(), notes.getChange().getRevertOf()));
-      claimedOriginal = ps.getRevision().get();
+    if (!claimedOriginal.isPresent()) {
+      return pureRevertCache.isPureRevert(notes);
     }
 
-    try (Repository repo = repoManager.openRepository(notes.getProjectName());
-        ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit claimedOriginalCommit;
-      try {
-        claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
-      } catch (InvalidObjectIdException | MissingObjectException e) {
-        throw new BadRequestException("invalid object ID");
-      }
-      if (claimedOriginalCommit.getParentCount() == 0) {
-        throw new BadRequestException("can't check against initial commit");
-      }
-      RevCommit claimedRevertCommit =
-          rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
-      if (claimedRevertCommit.getParentCount() == 0) {
-        throw new BadRequestException("claimed revert has no parents");
-      }
-      // Rebase claimed revert onto claimed original
-      ThreeWayMerger merger =
-          mergeUtilFactory
-              .create(projectCache.checkedGet(notes.getProjectName()))
-              .newThreeWayMerger(oi, repo.getConfig());
-      merger.setBase(claimedRevertCommit.getParent(0));
-      boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
-      if (!success || merger.getResultTreeId() == null) {
-        // Merge conflict during rebase
-        return new PureRevertInfo(false);
-      }
-
-      // Any differences between claimed original's parent and the rebase result indicate that the
-      // claimedRevert is not a pure revert but made content changes
-      try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
-        df.setReader(oi.newReader(), repo.getConfig());
-        List<DiffEntry> entries =
-            df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
-        return new PureRevertInfo(entries.isEmpty());
-      }
+    ObjectId claimedOriginalObjectId;
+    try {
+      claimedOriginalObjectId = ObjectId.fromString(claimedOriginal.get());
+    } catch (InvalidObjectIdException e) {
+      throw new BadRequestException("invalid object ID");
     }
+
+    return pureRevertCache.isPureRevert(
+        notes.getProjectName(),
+        ObjectId.fromString(notes.getCurrentPatchSet().getRevision().get()),
+        claimedOriginalObjectId);
   }
 }
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index e7baa55..f168ef9 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -117,6 +117,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.NotesBranchUtil;
+import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.git.ReceivePackInitializer;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
@@ -237,6 +238,7 @@
     install(SubmitStrategy.module());
     install(TagCache.module());
     install(OAuthTokenCache.module());
+    install(PureRevertCache.module());
 
     install(new AccessControlModule());
     install(new CmdLineParserModule());
diff --git a/java/com/google/gerrit/server/git/PureRevertCache.java b/java/com/google/gerrit/server/git/PureRevertCache.java
new file mode 100644
index 0000000..53f004f
--- /dev/null
+++ b/java/com/google/gerrit/server/git/PureRevertCache.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.proto.Cache;
+import com.google.gerrit.server.cache.proto.Cache.PureRevertKeyProto;
+import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
+import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
+import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.protobuf.ByteString;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Computes and caches if a change is a pure revert of another change. */
+@Singleton
+public class PureRevertCache {
+  private static final String ID_CACHE = "pure_revert";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(ID_CACHE, Cache.PureRevertKeyProto.class, Boolean.class)
+            .maximumWeight(100)
+            .loader(Loader.class)
+            .version(1)
+            .keySerializer(new ProtobufSerializer<>(Cache.PureRevertKeyProto.parser()))
+            .valueSerializer(BooleanCacheSerializer.INSTANCE);
+      }
+    };
+  }
+
+  private final LoadingCache<PureRevertKeyProto, Boolean> cache;
+  private final ChangeNotes.Factory notesFactory;
+
+  @Inject
+  PureRevertCache(
+      @Named(ID_CACHE) LoadingCache<PureRevertKeyProto, Boolean> cache,
+      ChangeNotes.Factory notesFactory) {
+    this.cache = cache;
+    this.notesFactory = notesFactory;
+  }
+
+  /**
+   * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of the change that is
+   * referenced in {@link Change#getRevertOf()}.
+   *
+   * @return {@code true} if {@code claimedRevert} is a pure (clean) revert.
+   * @throws IOException if there was a problem with the storage layer
+   * @throws OrmException if there was a problem with the storage layer
+   * @throws BadRequestException if there is a problem with the provided {@link ChangeNotes}
+   */
+  public boolean isPureRevert(ChangeNotes claimedRevert)
+      throws OrmException, IOException, BadRequestException {
+    if (claimedRevert.getChange().getRevertOf() == null) {
+      throw new BadRequestException("revertOf not set");
+    }
+    ChangeNotes claimedOriginal =
+        notesFactory.createChecked(
+            claimedRevert.getProjectName(), claimedRevert.getChange().getRevertOf());
+    return isPureRevert(
+        claimedRevert.getProjectName(),
+        ObjectId.fromString(claimedRevert.getCurrentPatchSet().getRevision().get()),
+        ObjectId.fromString(claimedOriginal.getCurrentPatchSet().getRevision().get()));
+  }
+
+  /**
+   * Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
+   * claimedOriginal}.
+   *
+   * @return {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
+   *     claimedOriginal}.
+   * @throws IOException if there was a problem with the storage layer
+   * @throws BadRequestException if there is a problem with the provided {@link ObjectId}s
+   */
+  public boolean isPureRevert(
+      Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal)
+      throws IOException, BadRequestException {
+    try {
+      return cache.get(key(project, claimedRevert, claimedOriginal));
+    } catch (ExecutionException e) {
+      Throwables.throwIfInstanceOf(e.getCause(), BadRequestException.class);
+      throw new IOException(e);
+    }
+  }
+
+  @VisibleForTesting
+  static PureRevertKeyProto key(
+      Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal) {
+    ByteString original = ObjectIdConverter.create().toByteString(claimedOriginal);
+    ByteString revert = ObjectIdConverter.create().toByteString(claimedRevert);
+    return PureRevertKeyProto.newBuilder()
+        .setProject(project.get())
+        .setClaimedOriginal(original)
+        .setClaimedRevert(revert)
+        .build();
+  }
+
+  static class Loader extends CacheLoader<PureRevertKeyProto, Boolean> {
+    private final GitRepositoryManager repoManager;
+    private final MergeUtil.Factory mergeUtilFactory;
+    private final ProjectCache projectCache;
+
+    @Inject
+    Loader(
+        GitRepositoryManager repoManager,
+        MergeUtil.Factory mergeUtilFactory,
+        ProjectCache projectCache) {
+      this.repoManager = repoManager;
+      this.mergeUtilFactory = mergeUtilFactory;
+      this.projectCache = projectCache;
+    }
+
+    @Override
+    public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
+      try (TraceContext.TraceTimer ignored =
+          TraceContext.newTimer("Loading pure revert for %s", key)) {
+        ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
+        ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
+        Project.NameKey project = new Project.NameKey(key.getProject());
+
+        try (Repository repo = repoManager.openRepository(project);
+            ObjectInserter oi = repo.newObjectInserter();
+            RevWalk rw = new RevWalk(repo)) {
+          RevCommit claimedOriginalCommit;
+          try {
+            claimedOriginalCommit = rw.parseCommit(original);
+          } catch (InvalidObjectIdException | MissingObjectException e) {
+            throw new BadRequestException("invalid object ID");
+          }
+          if (claimedOriginalCommit.getParentCount() == 0) {
+            throw new BadRequestException("can't check against initial commit");
+          }
+          RevCommit claimedRevertCommit = rw.parseCommit(revert);
+          if (claimedRevertCommit.getParentCount() == 0) {
+            return false;
+          }
+          // Rebase claimed revert onto claimed original
+          ThreeWayMerger merger =
+              mergeUtilFactory
+                  .create(projectCache.checkedGet(project))
+                  .newThreeWayMerger(oi, repo.getConfig());
+          merger.setBase(claimedRevertCommit.getParent(0));
+          boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
+          if (!success || merger.getResultTreeId() == null) {
+            // Merge conflict during rebase
+            return false;
+          }
+
+          // Any differences between claimed original's parent and the rebase result indicate that
+          // the
+          // claimedRevert is not a pure revert but made content changes
+          try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+            df.setReader(oi.newReader(), repo.getConfig());
+            List<DiffEntry> entries =
+                df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
+            return entries.isEmpty();
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index b058166..5a300b4 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -1111,7 +1111,7 @@
       return null;
     }
     try {
-      return pureRevert.get(notes(), null).isPureRevert;
+      return pureRevert.get(notes(), Optional.empty());
     } catch (IOException | BadRequestException | ResourceConflictException e) {
       throw new OrmException("could not compute pure revert", e);
     }
diff --git a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
index 75019af..dcafe56 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPureRevert.java
@@ -25,6 +25,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Optional;
 import org.kohsuke.args4j.Option;
 
 public class GetPureRevert implements RestReadView<ChangeResource> {
@@ -49,6 +50,7 @@
   public PureRevertInfo apply(ChangeResource rsrc)
       throws ResourceConflictException, IOException, BadRequestException, OrmException,
           AuthException {
-    return pureRevert.get(rsrc.getNotes(), claimedOriginal);
+    boolean isPureRevert = pureRevert.get(rsrc.getNotes(), Optional.ofNullable(claimedOriginal));
+    return new PureRevertInfo(isPureRevert);
   }
 }
diff --git a/java/com/google/gerrit/testing/TestTimeUtil.java b/java/com/google/gerrit/testing/TestTimeUtil.java
index 9228123..2020e5d 100644
--- a/java/com/google/gerrit/testing/TestTimeUtil.java
+++ b/java/com/google/gerrit/testing/TestTimeUtil.java
@@ -118,6 +118,15 @@
     clockMs.addAndGet(clockStepUnit.toMillis(clockStep));
   }
 
+  /**
+   * Returns the current timestamp.
+   *
+   * @return current timestamp
+   */
+  public static synchronized Timestamp getCurrentTimestamp() {
+    return new Timestamp(clockMs.get());
+  }
+
   /** Reset the clock to use the actual system clock. */
   public static synchronized void useSystemTime() {
     clockMs = null;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 9cc277f..5aed312 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3695,7 +3695,7 @@
   @Test
   public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
     exception.expect(BadRequestException.class);
-    exception.expectMessage("no ID was provided and change isn't a revert");
+    exception.expectMessage("revertOf not set");
     gApi.changes().id(createChange().getChangeId()).pureRevert();
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
index 9ab730c..d5089ff 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PluginFieldsIT.java
@@ -26,31 +26,31 @@
   // No tests for /detail via the extension API, since the extension API doesn't have that method.
 
   @Test
-  public void queryChangeApiWithNullAttribute() throws Exception {
+  public void queryChangeWithNullAttribute() throws Exception {
     getChangeWithNullAttribute(
         id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
   }
 
   @Test
-  public void getChangeApiWithNullAttribute() throws Exception {
+  public void getChangeWithNullAttribute() throws Exception {
     getChangeWithNullAttribute(
         id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
   }
 
   @Test
-  public void queryChangeApiWithSimpleAttribute() throws Exception {
+  public void queryChangeWithSimpleAttribute() throws Exception {
     getChangeWithSimpleAttribute(
         id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()));
   }
 
   @Test
-  public void getChangeApiWithSimpleAttribute() throws Exception {
+  public void getChangeWithSimpleAttribute() throws Exception {
     getChangeWithSimpleAttribute(
         id -> pluginInfoFromChangeInfo(gApi.changes().id(id.toString()).get()));
   }
 
   @Test
-  public void queryChangeApiWithOption() throws Exception {
+  public void queryChangeWithOption() throws Exception {
     getChangeWithOption(
         id -> pluginInfoFromSingletonList(gApi.changes().query(id.toString()).get()),
         (id, opts) ->
@@ -59,7 +59,7 @@
   }
 
   @Test
-  public void getChangeApiWithOption() throws Exception {
+  public void getChangeWithOption() throws Exception {
     getChangeWithOption(
         id -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get()),
         (id, opts) -> pluginInfoFromChangeInfo(gApi.changes().id(id.get()).get(opts)));
@@ -75,7 +75,7 @@
   }
 
   @Test
-  public void getChangeApiWithSimpleAttributeWithExplicitExport() throws Exception {
+  public void getChangeWithSimpleAttributeWithExplicitExport() throws Exception {
     // For backwards compatibility with old plugins, allow modules to bind into the
     // DynamicSet<ChangeAttributeFactory> as if it were a DynamicMap. We only need one variant of
     // this test to prove that the mapping works.
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
index 6388fe9..649c7ae 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/PluginFieldsIT.java
@@ -33,62 +33,59 @@
   private static final Gson GSON = OutputFormat.JSON.newGson();
 
   @Test
-  public void queryChangeRestWithNullAttribute() throws Exception {
+  public void queryChangeWithNullAttribute() throws Exception {
     getChangeWithNullAttribute(
-        id -> pluginInfoFromSingletonListRest(adminRestSession.get(changeQueryUrl(id))));
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
   }
 
   @Test
-  public void getChangeRestWithNullAttribute() throws Exception {
+  public void getChangeWithNullAttribute() throws Exception {
+    getChangeWithNullAttribute(id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
+  }
+
+  @Test
+  public void getChangeDetailWithNullAttribute() throws Exception {
     getChangeWithNullAttribute(
-        id -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeUrl(id))));
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
   }
 
   @Test
-  public void getChangeDetailRestWithNullAttribute() throws Exception {
-    getChangeWithNullAttribute(
-        id -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeDetailUrl(id))));
-  }
-
-  @Test
-  public void queryChangeRestWithSimpleAttribute() throws Exception {
+  public void queryChangeWithSimpleAttribute() throws Exception {
     getChangeWithSimpleAttribute(
-        id -> pluginInfoFromSingletonListRest(adminRestSession.get(changeQueryUrl(id))));
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))));
   }
 
   @Test
-  public void getChangeRestWithSimpleAttribute() throws Exception {
+  public void getChangeWithSimpleAttribute() throws Exception {
     getChangeWithSimpleAttribute(
-        id -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeUrl(id))));
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))));
   }
 
   @Test
-  public void getChangeDetailRestWithSimpleAttribute() throws Exception {
+  public void getChangeDetailWithSimpleAttribute() throws Exception {
     getChangeWithSimpleAttribute(
-        id -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeDetailUrl(id))));
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))));
   }
 
   @Test
-  public void queryChangeRestWithOption() throws Exception {
+  public void queryChangeWithOption() throws Exception {
     getChangeWithOption(
-        id -> pluginInfoFromSingletonListRest(adminRestSession.get(changeQueryUrl(id))),
-        (id, opts) ->
-            pluginInfoFromSingletonListRest(adminRestSession.get(changeQueryUrl(id, opts))));
+        id -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id))),
+        (id, opts) -> pluginInfoFromSingletonList(adminRestSession.get(changeQueryUrl(id, opts))));
   }
 
   @Test
-  public void getChangeRestWithOption() throws Exception {
+  public void getChangeWithOption() throws Exception {
     getChangeWithOption(
-        id -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeUrl(id))),
-        (id, opts) -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeUrl(id, opts))));
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id))),
+        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeUrl(id, opts))));
   }
 
   @Test
-  public void getChangeDetailRestWithOption() throws Exception {
+  public void getChangeDetailWithOption() throws Exception {
     getChangeWithOption(
-        id -> pluginInfoFromChangeInfoRest(adminRestSession.get(changeDetailUrl(id))),
-        (id, opts) ->
-            pluginInfoFromChangeInfoRest(adminRestSession.get(changeDetailUrl(id, opts))));
+        id -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id))),
+        (id, opts) -> pluginInfoFromChangeInfo(adminRestSession.get(changeDetailUrl(id, opts))));
   }
 
   private String changeQueryUrl(Change.Id id) {
@@ -136,32 +133,19 @@
   }
 
   @Nullable
-  private static List<MyInfo> pluginInfoFromSingletonListRest(RestResponse res) throws Exception {
+  private static List<MyInfo> pluginInfoFromSingletonList(RestResponse res) throws Exception {
     res.assertOK();
-
-    // Don't deserialize to ChangeInfo directly, since that would treat the plugins field as
-    // List<PluginDefinedInfo> and ignore the unknown keys found in MyInfo.
     List<Map<String, Object>> changeInfos =
         GSON.fromJson(res.getReader(), new TypeToken<List<Map<String, Object>>>() {}.getType());
     assertThat(changeInfos).hasSize(1);
-    return myInfo(changeInfos.get(0));
+    return decodeRawPluginsList(GSON, changeInfos.get(0).get("plugins"));
   }
 
   @Nullable
-  private List<MyInfo> pluginInfoFromChangeInfoRest(RestResponse res) throws Exception {
+  private List<MyInfo> pluginInfoFromChangeInfo(RestResponse res) throws Exception {
     res.assertOK();
-
-    // Don't deserialize to ChangeInfo directly, since that would treat the plugins field as
-    // List<PluginDefinedInfo> and ignore the unknown keys found in MyInfo.
-    return myInfo(
-        GSON.fromJson(res.getReader(), new TypeToken<Map<String, Object>>() {}.getType()));
-  }
-
-  private static List<MyInfo> myInfo(Map<String, Object> changeInfo) {
-    Object plugins = changeInfo.get("plugins");
-    if (plugins == null) {
-      return null;
-    }
-    return GSON.fromJson(GSON.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
+    Map<String, Object> changeInfo =
+        GSON.fromJson(res.getReader(), new TypeToken<Map<String, Object>>() {}.getType());
+    return decodeRawPluginsList(GSON, changeInfo.get("plugins"));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
index d196c5a..e61e2cc 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/PluginChangeFieldsIT.java
@@ -40,23 +40,22 @@
   private static final Gson GSON = OutputStreamQuery.GSON;
 
   @Test
-  public void queryChangeSshWithNullAttribute() throws Exception {
+  public void queryChangeWithNullAttribute() throws Exception {
     getChangeWithNullAttribute(
-        id -> pluginInfoFromSingletonListSsh(adminSshSession.exec(changeQueryCmd(id))));
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
   }
 
   @Test
-  public void queryChangeSshWithSimpleAttribute() throws Exception {
+  public void queryChangeWithSimpleAttribute() throws Exception {
     getChangeWithSimpleAttribute(
-        id -> pluginInfoFromSingletonListSsh(adminSshSession.exec(changeQueryCmd(id))));
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))));
   }
 
   @Test
-  public void queryChangeSshWithOption() throws Exception {
+  public void queryChangeWithOption() throws Exception {
     getChangeWithOption(
-        id -> pluginInfoFromSingletonListSsh(adminSshSession.exec(changeQueryCmd(id))),
-        (id, opts) ->
-            pluginInfoFromSingletonListSsh(adminSshSession.exec(changeQueryCmd(id, opts))));
+        id -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id))),
+        (id, opts) -> pluginInfoFromSingletonList(adminSshSession.exec(changeQueryCmd(id, opts))));
   }
 
   private String changeQueryCmd(Change.Id id) {
@@ -73,11 +72,9 @@
   }
 
   @Nullable
-  private static List<MyInfo> pluginInfoFromSingletonListSsh(String sshOutput) throws Exception {
+  private static List<MyInfo> pluginInfoFromSingletonList(String sshOutput) throws Exception {
     List<Map<String, Object>> changeAttrs = new ArrayList<>();
     for (String line : CharStreams.readLines(new StringReader(sshOutput))) {
-      // Don't deserialize to ChangeAttribute directly, since that would treat the plugins field as
-      // List<PluginDefinedInfo> and ignore the unknown keys found in MyInfo.
       Map<String, Object> changeAttr =
           GSON.fromJson(line, new TypeToken<Map<String, Object>>() {}.getType());
       if (!"stats".equals(changeAttr.get("type"))) {
@@ -86,11 +83,6 @@
     }
 
     assertThat(changeAttrs).hasSize(1);
-
-    Object plugins = changeAttrs.get(0).get("plugins");
-    if (plugins == null) {
-      return null;
-    }
-    return GSON.fromJson(GSON.toJson(plugins), new TypeToken<List<MyInfo>>() {}.getType());
+    return decodeRawPluginsList(GSON, changeAttrs.get(0).get("plugins"));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 79170c4..504ec46 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -47,7 +47,7 @@
       case V6_5:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.4";
       case V6_6:
-        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.1";
+        return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.2";
       case V7_0:
         return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0-beta1";
     }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BUILD b/javatests/com/google/gerrit/server/cache/serialize/BUILD
index ddad4b9..92f33ad 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -17,5 +17,6 @@
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
+        "//proto/testing:test_java_proto",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
new file mode 100644
index 0000000..845da9b
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/ProtobufSerializerTest.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.proto.testing.Test.SerializableProto;
+import com.google.gerrit.testing.GerritBaseTests;
+import org.junit.Test;
+
+public class ProtobufSerializerTest extends GerritBaseTests {
+  @Test
+  public void requiredAndOptionalTypes() {
+    assertRoundTrip(SerializableProto.newBuilder().setId(123));
+    assertRoundTrip(SerializableProto.newBuilder().setId(123).setText("foo bar"));
+  }
+
+  @Test
+  public void exactByteSequence() {
+    ProtobufSerializer<SerializableProto> s = new ProtobufSerializer<>(SerializableProto.parser());
+    SerializableProto proto = SerializableProto.newBuilder().setId(123).setText("foo bar").build();
+    byte[] serialized = s.serialize(proto);
+    // Hard-code byte sequence to detect library changes
+    assertThat(serialized).isEqualTo(new byte[] {8, 123, 18, 7, 102, 111, 111, 32, 98, 97, 114});
+  }
+
+  private static void assertRoundTrip(SerializableProto.Builder input) {
+    ProtobufSerializer<SerializableProto> s = new ProtobufSerializer<>(SerializableProto.parser());
+    assertThat(s.deserialize(s.serialize(input.build()))).isEqualTo(input.build());
+  }
+}
diff --git a/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
new file mode 100644
index 0000000..8c17075
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/PureRevertCacheKeyTest.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteArray;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.cache.proto.Cache;
+import com.google.protobuf.ByteString;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class PureRevertCacheKeyTest {
+  @Test
+  public void serialization() {
+    ObjectId revert = ObjectId.zeroId();
+    ObjectId original = ObjectId.fromString("aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+
+    byte[] serializedRevert =
+        new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    byte[] serializedOriginal =
+        byteArray(
+            0xaa, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
+            0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb);
+
+    Cache.PureRevertKeyProto key =
+        PureRevertCache.key(new Project.NameKey("test"), revert, original);
+    assertThat(key)
+        .isEqualTo(
+            Cache.PureRevertKeyProto.newBuilder()
+                .setProject("test")
+                .setClaimedRevert(ByteString.copyFrom(serializedRevert))
+                .setClaimedOriginal(ByteString.copyFrom(serializedOriginal))
+                .build());
+  }
+}
diff --git a/plugins/delete-project b/plugins/delete-project
index 53311c0..9eee6bb 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 53311c006635e83faaa7a1b68039edebe0fe8e43
+Subproject commit 9eee6bb4de393cf5bf94a2861320f5e1bf618329
diff --git a/plugins/replication b/plugins/replication
index a4bae3f..ff75ac8 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit a4bae3f491bb3f693e2407d353d3a81ce2a5c8be
+Subproject commit ff75ac8a4c806e5f627cb755f49e08962fa6e6b0
diff --git a/plugins/webhooks b/plugins/webhooks
index 064ae07..ed81825 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 064ae07f55710034062ef2a36cd030abeee93265
+Subproject commit ed818254ca32196c0e0acf4090b7b44a918f25ee
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 1c018de..2146d9d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -139,7 +139,10 @@
       },
       /** @type {?} */
       patchRange: Object,
-      path: String,
+      path: {
+        type: String,
+        observer: '_pathObserver',
+      },
       prefs: {
         type: Object,
         observer: '_prefsObserver',
@@ -636,6 +639,11 @@
       }
     },
 
+    _pathObserver() {
+      // Call _prefsChanged(), because line-limit style value depends on path.
+      this._prefsChanged(this.prefs);
+    },
+
     _viewModeObserver() {
       this._prefsChanged(this.prefs);
     },
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 412b73e..21bf649 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -384,10 +384,12 @@
               disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
               on-tap="_handleAddEmailButton">Send verification</gr-button>
         </fieldset>
-        <h2 id="HTTPCredentials">HTTP Credentials</h2>
-        <fieldset>
-          <gr-http-password id="httpPass"></gr-http-password>
-        </fieldset>
+        <div hidden$="[[!_showHttpAuth(_serverConfig)]]">
+          <h2 id="HTTPCredentials">HTTP Credentials</h2>
+          <fieldset>
+            <gr-http-password id="httpPass"></gr-http-password>
+          </fieldset>
+        </div>
         <div hidden$="[[!_serverConfig.sshd]]">
           <h2
               id="SSHKeys"
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 706b1ac..f523034 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -40,6 +40,11 @@
 
   const RELOAD_MESSAGE = 'Reloading...';
 
+  const HTTP_AUTH = [
+    'HTTP',
+    'HTTP_LDAP',
+  ];
+
   Polymer({
     is: 'gr-settings-view',
 
@@ -413,5 +418,15 @@
         window.location.reload();
       }, 1);
     },
+
+    _showHttpAuth(config) {
+      if (config && config.auth &&
+          config.auth.git_basic_auth_policy) {
+        return HTTP_AUTH.includes(
+            config.auth.git_basic_auth_policy.toUpperCase());
+      }
+
+      return false;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index a51d310..506c6af 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -407,6 +407,46 @@
       assert.isTrue(overlayOpen.called);
     });
 
+    test('_showHttpAuth', () => {
+      let serverConfig;
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'HTTP',
+        },
+      };
+
+      assert.isTrue(element._showHttpAuth(serverConfig));
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'HTTP_LDAP',
+        },
+      };
+
+      assert.isTrue(element._showHttpAuth(serverConfig));
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'LDAP',
+        },
+      };
+
+      assert.isFalse(element._showHttpAuth(serverConfig));
+
+      serverConfig = {
+        auth: {
+          git_basic_auth_policy: 'OAUTH',
+        },
+      };
+
+      assert.isFalse(element._showHttpAuth(serverConfig));
+
+      serverConfig = {};
+
+      assert.isFalse(element._showHttpAuth(serverConfig));
+    });
+
     suite('_getFilterDocsLink', () => {
       test('with http: docs base URL', () => {
         const base = 'http://example.com/';
diff --git a/proto/cache.proto b/proto/cache.proto
index c978069..b34dbf3 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -234,3 +234,11 @@
   }
   repeated ExternalIdProto external_id = 1;
 }
+
+// Key for com.google.gerrit.server.git.PureRevertCache.
+// Next ID: 4
+message PureRevertKeyProto {
+  string project = 1;
+  bytes claimed_original = 2;
+  bytes claimed_revert = 3;
+}
diff --git a/proto/testing/BUILD b/proto/testing/BUILD
new file mode 100644
index 0000000..b9032cf
--- /dev/null
+++ b/proto/testing/BUILD
@@ -0,0 +1,12 @@
+proto_library(
+    name = "test_proto",
+    testonly = 1,
+    srcs = ["test.proto"],
+)
+
+java_proto_library(
+    name = "test_java_proto",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    deps = [":test_proto"],
+)
diff --git a/proto/testing/test.proto b/proto/testing/test.proto
new file mode 100644
index 0000000..e28c9ff
--- /dev/null
+++ b/proto/testing/test.proto
@@ -0,0 +1,26 @@
+// 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.
+
+syntax = "proto2";
+
+package devtools.gerritcodereview.testing;
+
+option java_package = "com.google.gerrit.proto.testing";
+
+// Test type for ProtobufSerializerTest
+// Next ID: 3
+message SerializableProto {
+  required int32 id = 1;
+  optional string text = 2;
+}
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index a7714a1..0245c50 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -425,7 +425,7 @@
     """Combine html, js, css files and optionally split into js and html bundles."""
     _bundle_rule(pkg = native.package_name(), *args, **kwargs)
 
-def polygerrit_plugin(name, app, srcs = [], assets = None, plugin_name = None, **kwargs):
+def polygerrit_plugin(name, app, srcs = [], deps = [], assets = None, plugin_name = None, **kwargs):
     """Bundles plugin dependencies for deployment.
 
     This rule bundles all Polymer elements and JS dependencies into .html and .js files.
@@ -450,6 +450,7 @@
             name = name + "_combined",
             app = app,
             srcs = srcs,
+            deps = deps,
             pkg = native.package_name(),
             **kwargs
         )