Merge branch 'stable-3.3' into stable-3.4

* stable-3.3:
  Manage removal of refs
  Use correct placeholders when logging in RevisionReader
  Honour the fetch ref-spec in replication.config
  Log the incoming git refs events for replication
  Fix pull-replication after the removal of Log4J from Gerrit
  Allow replication of refs that point to non-commit objects

Change-Id: I243026135c17cbbe6876f80dd380b1d71fe1d813
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
index d4e22d9..cee52b5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.WorkQueue.CanceledWhileRunning;
 import com.google.gerrit.server.ioutil.HexFormat;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -40,6 +41,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -54,7 +56,6 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.URIish;
-import org.slf4j.MDC;
 
 /**
  * A pull from remote operation started by command-line.
@@ -65,7 +66,7 @@
 public class FetchOne implements ProjectRunnable, CanceledWhileRunning {
   private final ReplicationStateListener stateLog;
   static final String ALL_REFS = "..all..";
-  static final String ID_MDC_KEY = "fetchOneId";
+  static final String ID_KEY = "fetchOneId";
 
   interface Factory {
     FetchOne create(Project.NameKey d, URIish u);
@@ -270,11 +271,16 @@
   }
 
   private void runFetchOperation() {
+    try (TraceContext ctx = TraceContext.open().addTag(ID_KEY, HexFormat.fromInt(id))) {
+      doRunFetchOperation();
+    }
+  }
+
+  private void doRunFetchOperation() {
     // Lock the queue, and remove ourselves, so we can't be modified once
     // we start replication (instead a new instance, with the same URI, is
     // created and scheduled for a future point in time.)
     //
-    MDC.put(ID_MDC_KEY, HexFormat.fromInt(id));
     if (!pool.requestRunway(this)) {
       if (!canceled) {
         repLog.info(
@@ -368,10 +374,25 @@
   }
 
   private List<RefSpec> getFetchRefSpecs() {
+    List<RefSpec> configRefSpecs = config.getFetchRefSpecs();
     if (delta.isEmpty()) {
-      return config.getFetchRefSpecs();
+      return configRefSpecs;
     }
-    return delta.stream().map(ref -> new RefSpec(ref + ":" + ref)).collect(Collectors.toList());
+
+    return delta.stream()
+        .map(ref -> refToFetchRefSpec(ref, configRefSpecs))
+        .filter(Optional::isPresent)
+        .map(Optional::get)
+        .collect(Collectors.toList());
+  }
+
+  private Optional<RefSpec> refToFetchRefSpec(String ref, List<RefSpec> configRefSpecs) {
+    for (RefSpec refSpec : configRefSpecs) {
+      if (refSpec.matchSource(ref)) {
+        return Optional.of(refSpec.expandFromSource(ref));
+      }
+    }
+    return Optional.empty();
   }
 
   private void updateStates(List<RefUpdateState> refUpdates) throws IOException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationLogFile.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationLogFile.java
index 299648d..932103b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationLogFile.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationLogFile.java
@@ -28,6 +28,6 @@
         systemLog,
         serverInfo,
         PullReplicationLogger.PULL_REPLICATION_LOG_NAME,
-        new PatternLayout("[%d] [%X{" + FetchOne.ID_MDC_KEY + "}] %m%n"));
+        new PatternLayout("[%d] %m%n"));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
index 61f2a64..7449263 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
@@ -31,7 +31,6 @@
 import com.googlesource.gerrit.plugins.replication.pull.FetchResultProcessing.GitUpdateProcessing;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
-import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.HttpResult;
 import com.googlesource.gerrit.plugins.replication.pull.filter.ExcludedRefsFilter;
@@ -134,7 +133,18 @@
   @Override
   public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
     if (isRefToBeReplicated(event.getRefName())) {
-      fire(event.getProjectName(), ObjectId.fromString(event.getNewObjectId()), event.getRefName());
+      repLog.info(
+          "Ref event received: {} on project {}:{} - {} => {}",
+          refUpdateType(event),
+          event.getProjectName(),
+          event.getRefName(),
+          event.getOldObjectId(),
+          event.getNewObjectId());
+      fire(
+          event.getProjectName(),
+          ObjectId.fromString(event.getNewObjectId()),
+          event.getRefName(),
+          event.isDelete());
     }
   }
 
@@ -148,30 +158,47 @@
                 source.getApis().forEach(apiUrl -> source.scheduleDeleteProject(apiUrl, project)));
   }
 
+  private static String refUpdateType(GitReferenceUpdatedListener.Event event) {
+    String forcedPrefix = event.isNonFastForward() ? "FORCED " : " ";
+    if (event.isCreate()) {
+      return forcedPrefix + "CREATE";
+    } else if (event.isDelete()) {
+      return forcedPrefix + "DELETE";
+    } else {
+      return forcedPrefix + "UPDATE";
+    }
+  }
+
   private Boolean isRefToBeReplicated(String refName) {
     return !refsFilter.match(refName);
   }
 
-  private void fire(String projectName, ObjectId objectId, String refName) {
+  private void fire(String projectName, ObjectId objectId, String refName, boolean isDelete) {
     ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher.get()));
-    fire(Project.nameKey(projectName), objectId, refName, state);
+    fire(Project.nameKey(projectName), objectId, refName, isDelete, state);
     state.markAllFetchTasksScheduled();
   }
 
   private void fire(
-      Project.NameKey project, ObjectId objectId, String refName, ReplicationState state) {
+      Project.NameKey project,
+      ObjectId objectId,
+      String refName,
+      boolean isDelete,
+      ReplicationState state) {
     if (!running) {
       stateLog.warn(
           "Replication plugin did not finish startup before event, event replication is postponed",
           state);
-      beforeStartupEventsQueue.add(ReferenceUpdatedEvent.create(project.get(), refName, objectId));
+      beforeStartupEventsQueue.add(
+          ReferenceUpdatedEvent.create(project.get(), refName, objectId, isDelete));
       return;
     }
     ForkJoinPool fetchCallsPool = null;
     try {
       fetchCallsPool = new ForkJoinPool(sources.get().getAll().size());
 
-      final Consumer<Source> callFunction = callFunction(project, refName, state);
+      final Consumer<Source> callFunction =
+          callFunction(project, objectId, refName, isDelete, state);
       fetchCallsPool
           .submit(() -> sources.get().getAll().parallelStream().forEach(callFunction))
           .get(fetchCallsTimeout, TimeUnit.MILLISECONDS);
@@ -189,8 +216,13 @@
     }
   }
 
-  private Consumer<Source> callFunction(NameKey project, String refName, ReplicationState state) {
-    CallFunction call = getCallFunction(project, refName, state);
+  private Consumer<Source> callFunction(
+      NameKey project,
+      ObjectId objectId,
+      String refName,
+      boolean isDelete,
+      ReplicationState state) {
+    CallFunction call = getCallFunction(project, objectId, refName, isDelete, state);
 
     return (source) -> {
       try {
@@ -201,13 +233,23 @@
     };
   }
 
-  private CallFunction getCallFunction(NameKey project, String refName, ReplicationState state) {
+  private CallFunction getCallFunction(
+      NameKey project,
+      ObjectId objectId,
+      String refName,
+      boolean isDelete,
+      ReplicationState state) {
+    if (isDelete) {
+      return ((source) -> callSendObject(source, project, refName, isDelete, null, state));
+    }
+
     try {
-      Optional<RevisionData> revisionData = revisionReader.read(project, refName);
+      Optional<RevisionData> revisionData = revisionReader.read(project, objectId, refName);
       if (revisionData.isPresent()) {
-        return ((source) -> callSendObject(source, project, refName, revisionData.get(), state));
+        return ((source) ->
+            callSendObject(source, project, refName, isDelete, revisionData.get(), state));
       }
-    } catch (InvalidObjectIdException | IOException | RefUpdateException e) {
+    } catch (InvalidObjectIdException | IOException e) {
       stateLog.error(
           String.format(
               "Exception during reading ref: %s, project:%s, message: %s",
@@ -223,6 +265,7 @@
       Source source,
       Project.NameKey project,
       String refName,
+      boolean isDelete,
       RevisionData revision,
       ReplicationState state)
       throws MissingParentObjectException {
@@ -232,7 +275,7 @@
           URIish uri = new URIish(apiUrl);
           FetchRestApiClient fetchClient = fetchClientFactory.create(source);
 
-          HttpResult result = fetchClient.callSendObject(project, refName, revision, uri);
+          HttpResult result = fetchClient.callSendObject(project, refName, isDelete, revision, uri);
           if (isProjectMissing(result, project) && source.isCreateMissingRepositories()) {
             result = initProject(project, uri, fetchClient, result);
           }
@@ -319,7 +362,7 @@
       String eventKey = String.format("%s:%s", event.projectName(), event.refName());
       if (!eventsReplayed.contains(eventKey)) {
         repLog.info("Firing pending task {}", event);
-        fire(event.projectName(), event.objectId(), event.refName());
+        fire(event.projectName(), event.objectId(), event.refName(), event.isDelete());
         eventsReplayed.add(eventKey);
       }
     }
@@ -339,8 +382,10 @@
   @AutoValue
   abstract static class ReferenceUpdatedEvent {
 
-    static ReferenceUpdatedEvent create(String projectName, String refName, ObjectId objectId) {
-      return new AutoValue_ReplicationQueue_ReferenceUpdatedEvent(projectName, refName, objectId);
+    static ReferenceUpdatedEvent create(
+        String projectName, String refName, ObjectId objectId, boolean isDelete) {
+      return new AutoValue_ReplicationQueue_ReferenceUpdatedEvent(
+          projectName, refName, objectId, isDelete);
     }
 
     public abstract String projectName();
@@ -348,6 +393,8 @@
     public abstract String refName();
 
     public abstract ObjectId objectId();
+
+    public abstract boolean isDelete();
   }
 
   @FunctionalInterface
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java
index 5718249..468c5fc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReader.java
@@ -23,7 +23,6 @@
 import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
-import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
@@ -34,9 +33,9 @@
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -57,22 +56,25 @@
             .getLong("replication", CONFIG_MAX_API_PAYLOAD_SIZE, DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES);
   }
 
-  public Optional<RevisionData> read(Project.NameKey project, String refName)
+  public Optional<RevisionData> read(Project.NameKey project, ObjectId objectId, String refName)
       throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
-          RepositoryNotFoundException, RefUpdateException, IOException {
+          RepositoryNotFoundException, IOException {
     try (Repository git = gitRepositoryManager.openRepository(project)) {
-      Ref head = git.exactRef(refName);
-      if (head == null) {
-        throw new RefUpdateException(
-            String.format("Cannot find ref %s in project %s", refName, project.get()));
-      }
-
       Long totalRefSize = 0l;
 
-      ObjectLoader commitLoader = git.open(head.getObjectId());
+      ObjectLoader commitLoader = git.open(objectId);
       totalRefSize += commitLoader.getSize();
       verifySize(totalRefSize, commitLoader);
 
+      if (commitLoader.getType() != Constants.OBJ_COMMIT) {
+        repLog.trace(
+            "Ref {} for project {} points to an object type {}",
+            refName,
+            project,
+            commitLoader.getType());
+        return Optional.empty();
+      }
+
       RevCommit commit = RevCommit.parse(commitLoader.getCachedBytes());
       RevisionObjectData commitRev =
           new RevisionObjectData(commit.getType(), commitLoader.getCachedBytes());
@@ -99,8 +101,10 @@
       return Optional.of(new RevisionData(commitRev, treeRev, blobs));
     } catch (LargeObjectException e) {
       repLog.trace(
-          "Ref %s size for project %s is greater than configured '%s'",
-          refName, project, CONFIG_MAX_API_PAYLOAD_SIZE);
+          "Ref {} size for project {} is greater than configured '{}'",
+          refName,
+          project,
+          CONFIG_MAX_API_PAYLOAD_SIZE);
       return Optional.empty();
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
index f029834..5132f41 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
@@ -33,11 +33,16 @@
 public class ApplyObjectAction implements RestModifyView<ProjectResource, RevisionInput> {
 
   private final ApplyObjectCommand command;
+  private final DeleteRefCommand deleteRefCommand;
   private final FetchPreconditions preConditions;
 
   @Inject
-  public ApplyObjectAction(ApplyObjectCommand command, FetchPreconditions preConditions) {
+  public ApplyObjectAction(
+      ApplyObjectCommand command,
+      DeleteRefCommand deleteRefCommand,
+      FetchPreconditions preConditions) {
     this.command = command;
+    this.deleteRefCommand = deleteRefCommand;
     this.preConditions = preConditions;
   }
 
@@ -56,7 +61,8 @@
       }
 
       if (Objects.isNull(input.getRevisionData())) {
-        throw new BadRequestException("Ref-update revision data cannot be null or empty");
+        deleteRefCommand.deleteRef(resource.getNameKey(), input.getRefName(), input.getLabel());
+        return Response.created(input);
       }
 
       if (Objects.isNull(input.getRevisionData().getCommitObject())
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
new file mode 100644
index 0000000..0c5e016
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.restapi.project.DeleteRef;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.pull.Context;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
+import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
+import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.RefUpdate;
+
+public class DeleteRefCommand {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final PullReplicationStateLogger fetchStateLog;
+  private final DeleteRef deleteRef;
+  private final DynamicItem<EventDispatcher> eventDispatcher;
+  private final ProjectCache projectCache;
+
+  @Inject
+  public DeleteRefCommand(
+      PullReplicationStateLogger fetchStateLog,
+      ProjectCache projectCache,
+      DeleteRef deleteRef,
+      DynamicItem<EventDispatcher> eventDispatcher) {
+    this.fetchStateLog = fetchStateLog;
+    this.projectCache = projectCache;
+    this.deleteRef = deleteRef;
+    this.eventDispatcher = eventDispatcher;
+  }
+
+  public void deleteRef(Project.NameKey name, String refName, String sourceLabel)
+      throws IOException, RestApiException {
+    try {
+      repLog.info("Delete ref from {} for project {}, ref name {}", sourceLabel, name, refName);
+      Optional<ProjectState> projectState = projectCache.get(name);
+      if (!projectState.isPresent()) {
+        throw new ResourceNotFoundException(String.format("Project %s was not found", name));
+      }
+
+      try {
+        Context.setLocalEvent(true);
+        deleteRef.deleteSingleRef(projectState.get(), refName);
+
+        eventDispatcher
+            .get()
+            .postEvent(
+                new FetchRefReplicatedEvent(
+                    name.get(),
+                    refName,
+                    sourceLabel,
+                    ReplicationState.RefFetchResult.SUCCEEDED,
+                    RefUpdate.Result.FORCED));
+      } catch (PermissionBackendException e) {
+        logger.atSevere().withCause(e).log(
+            "Unexpected error while trying to delete ref '%s' on project %s and notifying it",
+            refName, name);
+        throw RestApiException.wrap(e.getMessage(), e);
+      } catch (ResourceConflictException e) {
+        eventDispatcher
+            .get()
+            .postEvent(
+                new FetchRefReplicatedEvent(
+                    name.get(),
+                    refName,
+                    sourceLabel,
+                    ReplicationState.RefFetchResult.FAILED,
+                    RefUpdate.Result.LOCK_FAILURE));
+        String message =
+            String.format(
+                "RefUpdate lock failure for: sourceLabel=%s, project=%s, refName=%s",
+                sourceLabel, name, refName);
+        logger.atSevere().withCause(e).log(message);
+        fetchStateLog.error(message);
+        throw e;
+      } finally {
+        Context.unsetLocalEvent();
+      }
+
+      repLog.info(
+          "Delete ref from {} for project {}, ref name {} completed", sourceLabel, name, refName);
+    } catch (PermissionBackendException e) {
+      throw RestApiException.wrap(e.getMessage(), e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
index a916a7c..ab3d3c5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.net.MediaType;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.Url;
@@ -142,9 +143,19 @@
   }
 
   public HttpResult callSendObject(
-      Project.NameKey project, String refName, RevisionData revisionData, URIish targetUri)
+      Project.NameKey project,
+      String refName,
+      boolean isDelete,
+      @Nullable RevisionData revisionData,
+      URIish targetUri)
       throws ClientProtocolException, IOException {
 
+    if (!isDelete) {
+      requireNonNull(
+          revisionData, "RevisionData MUST not be null when the ref-update is not a DELETE");
+    } else {
+      requireNull(revisionData, "DELETE ref-updates cannot be associated with a RevisionData");
+    }
     RevisionInput input = new RevisionInput(instanceLabel, refName, revisionData);
 
     String url =
@@ -158,6 +169,12 @@
     return httpClientFactory.create(source).execute(post, this, getContext(targetUri));
   }
 
+  private void requireNull(Object object, String string) {
+    if (object != null) {
+      throw new IllegalArgumentException(string);
+    }
+  }
+
   @Override
   public HttpResult handleResponse(HttpResponse response) {
     Optional<String> responseBody = Optional.empty();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index c5b1790..cf94086 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -15,6 +15,10 @@
 package com.googlesource.gerrit.plugins.replication.pull;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.GitUtil.pushOne;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
@@ -24,7 +28,10 @@
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.common.Input;
@@ -47,16 +54,23 @@
 import java.nio.file.Path;
 import java.time.Duration;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Supplier;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
@@ -69,7 +83,7 @@
   private static final Optional<String> ALL_PROJECTS = Optional.empty();
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final int TEST_REPLICATION_DELAY = 60;
-  private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2);
+  private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000);
   private static final String TEST_REPLICATION_SUFFIX = "suffix1";
   private static final String TEST_REPLICATION_REMOTE = "remote1";
 
@@ -161,6 +175,76 @@
   }
 
   @Test
+  @UseLocalDisk
+  public void shouldReplicateForceUpdatedBranch() throws Exception {
+    boolean forcedPush = true;
+    String testProjectName = project + TEST_REPLICATION_SUFFIX;
+    NameKey testProjectNameKey = createTestProject(testProjectName);
+
+    String newBranch = "refs/heads/mybranch";
+    String master = "refs/heads/master";
+    BranchInput input = new BranchInput();
+    input.revision = master;
+    gApi.projects().name(testProjectName).branch(newBranch).create(input);
+
+    projectOperations
+        .project(testProjectNameKey)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(newBranch).group(REGISTERED_USERS).force(true))
+        .update();
+
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+    GitReferenceUpdatedListener.Event event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            ReceiveCommand.Type.CREATE);
+    pullReplicationQueue.onGitReferenceUpdated(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, newBranch) != null);
+
+      Ref targetBranchRef = getRef(repo, newBranch);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision);
+    }
+
+    TestRepository<InMemoryRepository> testProject = cloneProject(testProjectNameKey);
+    fetch(testProject, RefNames.REFS_HEADS + "*:" + RefNames.REFS_HEADS + "*");
+    RevCommit amendedCommit = testProject.amendRef(newBranch).message("Amended commit").create();
+    PushResult pushResult =
+        pushOne(testProject, newBranch, newBranch, false, forcedPush, Collections.emptyList());
+    Collection<RemoteRefUpdate> pushedRefs = pushResult.getRemoteUpdates();
+    assertThat(pushedRefs).hasSize(1);
+    assertThat(pushedRefs.iterator().next().getStatus()).isEqualTo(Status.OK);
+
+    GitReferenceUpdatedListener.Event forcedPushEvent =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            branchRevision,
+            amendedCommit.getId().getName(),
+            ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
+    pullReplicationQueue.onGitReferenceUpdated(forcedPushEvent);
+
+    try (Repository repo = repoManager.openRepository(project);
+        Repository sourceRepo = repoManager.openRepository(project)) {
+      waitUntil(
+          () ->
+              checkedGetRef(repo, newBranch) != null
+                  && checkedGetRef(repo, newBranch)
+                      .getObjectId()
+                      .getName()
+                      .equals(amendedCommit.getId().getName()));
+    }
+  }
+
+  @Test
   public void shouldReplicateNewChangeRefCGitClient() throws Exception {
     AutoReloadConfigDecorator autoReloadConfigDecorator =
         getInstance(AutoReloadConfigDecorator.class);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
index 8402782..82dc2b8 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
@@ -16,6 +16,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.file.Files.createTempDirectory;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.never;
@@ -86,7 +88,7 @@
   private Path pluginDataPath;
 
   @Before
-  public void setup() throws IOException, LargeObjectException, RefUpdateException {
+  public void setup() throws IOException, LargeObjectException {
     Path sitePath = createTempPath("site");
     sitePaths = new SitePaths(sitePath);
     Path pluginDataPath = createTempPath("data");
@@ -99,9 +101,9 @@
     when(source.getApis()).thenReturn(apis);
     when(sourceCollection.getAll()).thenReturn(Lists.newArrayList(source));
     when(rd.get()).thenReturn(sourceCollection);
-    when(revReader.read(any(), anyString())).thenReturn(Optional.of(revisionData));
+    when(revReader.read(any(), any(), anyString())).thenReturn(Optional.of(revisionData));
     when(fetchClientFactory.create(any())).thenReturn(fetchRestApiClient);
-    when(fetchRestApiClient.callSendObject(any(), anyString(), any(), any()))
+    when(fetchRestApiClient.callSendObject(any(), anyString(), anyBoolean(), any(), any()))
         .thenReturn(httpResult);
     when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(httpResult);
     when(httpResult.isSuccessful()).thenReturn(true);
@@ -117,7 +119,7 @@
     objectUnderTest.start();
     objectUnderTest.onGitReferenceUpdated(event);
 
-    verify(fetchRestApiClient).callSendObject(any(), anyString(), any(), any());
+    verify(fetchRestApiClient).callSendObject(any(), anyString(), eq(false), any(), any());
   }
 
   @Test
@@ -152,7 +154,7 @@
     objectUnderTest.start();
     objectUnderTest.onGitReferenceUpdated(event);
 
-    verify(fetchRestApiClient).callSendObject(any(), anyString(), any(), any());
+    verify(fetchRestApiClient).callSendObject(any(), anyString(), eq(false), any(), any());
   }
 
   @Test
@@ -161,7 +163,7 @@
     Event event = new TestEvent("refs/changes/01/1/meta");
     objectUnderTest.start();
 
-    when(revReader.read(any(), anyString())).thenThrow(IOException.class);
+    when(revReader.read(any(), any(), anyString())).thenThrow(IOException.class);
 
     objectUnderTest.onGitReferenceUpdated(event);
 
@@ -174,7 +176,7 @@
     Event event = new TestEvent("refs/changes/01/1/1");
     objectUnderTest.start();
 
-    when(revReader.read(any(), anyString())).thenReturn(Optional.empty());
+    when(revReader.read(any(), any(), anyString())).thenReturn(Optional.empty());
 
     objectUnderTest.onGitReferenceUpdated(event);
 
@@ -189,7 +191,7 @@
 
     when(httpResult.isSuccessful()).thenReturn(false);
     when(httpResult.isParentObjectMissing()).thenReturn(true);
-    when(fetchRestApiClient.callSendObject(any(), anyString(), any(), any()))
+    when(fetchRestApiClient.callSendObject(any(), anyString(), eq(false), any(), any()))
         .thenReturn(httpResult);
 
     objectUnderTest.onGitReferenceUpdated(event);
@@ -362,7 +364,7 @@
 
     @Override
     public String getOldObjectId() {
-      return null;
+      return ObjectId.zeroId().getName();
     }
 
     @Override
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java
index d1a4c85..e300613 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/RevisionReaderIT.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.truth.Truth8;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.TestPlugin;
@@ -29,14 +30,19 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Scopes;
 import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
 import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
 import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
+import java.io.IOException;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -57,7 +63,8 @@
     Result pushResult = createChange();
     String refName = RefNames.changeMetaRef(pushResult.getChange().getId());
 
-    Optional<RevisionData> revisionDataOption = objectUnderTest.read(project, refName);
+    Optional<RevisionData> revisionDataOption =
+        refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId));
 
     assertThat(revisionDataOption.isPresent()).isTrue();
     RevisionData revisionData = revisionDataOption.get();
@@ -73,6 +80,20 @@
     assertThat(revisionData.getBlobs()).isEmpty();
   }
 
+  private Optional<RevisionData> readRevisionFromObjectUnderTest(String refName, ObjectId objId) {
+    try {
+      return objectUnderTest.read(project, objId, refName);
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  protected Optional<ObjectId> refObjectId(String refName) throws IOException {
+    try (Repository repo = repoManager.openRepository(project)) {
+      return Optional.ofNullable(repo.exactRef(refName)).map(Ref::getObjectId);
+    }
+  }
+
   @Test
   public void shouldReadRefMetaObjectWithComments() throws Exception {
     Result pushResult = createChange();
@@ -85,7 +106,8 @@
     reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
     gApi.changes().id(changeId.get()).current().review(reviewInput);
 
-    Optional<RevisionData> revisionDataOption = objectUnderTest.read(project, refName);
+    Optional<RevisionData> revisionDataOption =
+        refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId));
 
     assertThat(revisionDataOption.isPresent()).isTrue();
     RevisionData revisionData = revisionDataOption.get();
@@ -104,6 +126,16 @@
     assertThat(blobObject.getContent()).isNotEmpty();
   }
 
+  @Test
+  public void shouldNotReadRefsSequences() throws Exception {
+    createChange().assertOkStatus();
+    String refName = RefNames.REFS_SEQUENCES + Sequences.NAME_CHANGES;
+    Optional<RevisionData> revisionDataOption =
+        refObjectId(refName).flatMap(objId -> readRevisionFromObjectUnderTest(refName, objId));
+
+    Truth8.assertThat(revisionDataOption).isEmpty();
+  }
+
   private CommentInput createCommentInput(
       int startLine, int startCharacter, int endLine, int endCharacter, String message) {
     CommentInput comment = new CommentInput();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
index 34aaf8b..cef1051 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -55,6 +55,7 @@
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 
@@ -147,7 +148,9 @@
 
   protected Optional<RevisionData> createRevisionData(NameKey projectName, String refName)
       throws Exception {
-    return revisionReader.read(projectName, refName);
+    try (Repository repository = repoManager.openRepository(projectName)) {
+      return revisionReader.read(projectName, repository.exactRef(refName).getObjectId(), refName);
+    }
   }
 
   protected Object encode(byte[] content) {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
index 8738046..56e1429 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
@@ -72,6 +72,7 @@
           + "Submitted-with: OK: Code-Review: Gerrit User 1000000 <1000000@69ec38f0-350e-4d9c-96d4-bc956f2faaac>";
 
   @Mock ApplyObjectCommand applyObjectCommand;
+  @Mock DeleteRefCommand deleteRefCommand;
   @Mock ProjectResource projectResource;
   @Mock FetchPreconditions preConditions;
 
@@ -79,7 +80,7 @@
   public void setup() {
     when(preConditions.canCallFetchApi()).thenReturn(true);
 
-    applyObjectAction = new ApplyObjectAction(applyObjectCommand, preConditions);
+    applyObjectAction = new ApplyObjectAction(applyObjectCommand, deleteRefCommand, preConditions);
   }
 
   @Test
@@ -129,13 +130,6 @@
   }
 
   @Test(expected = BadRequestException.class)
-  public void shouldThrowBadRequestExceptionWhenMissingRevisionData() throws Exception {
-    RevisionInput inputParams = new RevisionInput(label, refName, null);
-
-    applyObjectAction.apply(projectResource, inputParams);
-  }
-
-  @Test(expected = BadRequestException.class)
   public void shouldThrowBadRequestExceptionWhenMissingCommitObjectData() throws Exception {
     RevisionObjectData commitData = new RevisionObjectData(Constants.OBJ_COMMIT, null);
     RevisionObjectData treeData = new RevisionObjectData(Constants.OBJ_TREE, new byte[] {});
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
new file mode 100644
index 0000000..daf2001
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.restapi.project.DeleteRef;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
+import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DeleteRefCommandTest {
+  private static final String TEST_SOURCE_LABEL = "test-source-label";
+  private static final String TEST_REF_NAME = "refs/changes/01/1/1";
+  private static final NameKey TEST_PROJECT_NAME = Project.nameKey("test-project");
+
+  @Mock private PullReplicationStateLogger fetchStateLog;
+  @Mock private DynamicItem<EventDispatcher> eventDispatcherDataItem;
+  @Mock private EventDispatcher eventDispatcher;
+  @Mock private ProjectCache projectCache;
+  @Mock private DeleteRef deleteRef;
+  @Mock private ProjectState projectState;
+  @Captor ArgumentCaptor<Event> eventCaptor;
+
+  private DeleteRefCommand objectUnderTest;
+
+  @Before
+  public void setup() {
+    when(eventDispatcherDataItem.get()).thenReturn(eventDispatcher);
+    when(projectCache.get(any())).thenReturn(Optional.of(projectState));
+
+    objectUnderTest =
+        new DeleteRefCommand(fetchStateLog, projectCache, deleteRef, eventDispatcherDataItem);
+  }
+
+  @Test
+  public void shouldSendEventWhenDeletingRef() throws Exception {
+    objectUnderTest.deleteRef(TEST_PROJECT_NAME, TEST_REF_NAME, TEST_SOURCE_LABEL);
+
+    verify(eventDispatcher).postEvent(eventCaptor.capture());
+    Event sentEvent = eventCaptor.getValue();
+    assertThat(sentEvent).isInstanceOf(FetchRefReplicatedEvent.class);
+    FetchRefReplicatedEvent fetchEvent = (FetchRefReplicatedEvent) sentEvent;
+    assertThat(fetchEvent.getProjectNameKey()).isEqualTo(TEST_PROJECT_NAME);
+    assertThat(fetchEvent.getRefName()).isEqualTo(TEST_REF_NAME);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
index a778049..394a4a4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
@@ -64,6 +64,8 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class FetchRestApiClientTest {
+  private static final boolean IS_REF_UPDATE = false;
+
   @Mock CredentialsProvider credentialProvider;
   @Mock CredentialsFactory credentials;
   @Mock HttpClient httpClient;
@@ -273,7 +275,11 @@
       throws ClientProtocolException, IOException, URISyntaxException {
 
     objectUnderTest.callSendObject(
-        Project.nameKey("test_repo"), refName, createSampleRevisionData(), new URIish(api));
+        Project.nameKey("test_repo"),
+        refName,
+        IS_REF_UPDATE,
+        createSampleRevisionData(),
+        new URIish(api));
 
     verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any());
 
@@ -288,7 +294,11 @@
       throws ClientProtocolException, IOException, URISyntaxException {
 
     objectUnderTest.callSendObject(
-        Project.nameKey("test_repo"), refName, createSampleRevisionData(), new URIish(api));
+        Project.nameKey("test_repo"),
+        refName,
+        IS_REF_UPDATE,
+        createSampleRevisionData(),
+        new URIish(api));
 
     verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any());
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
index f6cd342..e2a162e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
@@ -77,16 +78,16 @@
     String refName = RefNames.changeMetaRef(pushResult.getChange().getId());
 
     Optional<RevisionData> revisionData =
-        reader.read(Project.nameKey(testRepoProjectName), refName);
+        reader.read(
+            Project.nameKey(testRepoProjectName), pushResult.getCommit().toObjectId(), refName);
 
     RefSpec refSpec = new RefSpec(refName);
     objectUnderTest.apply(project, refSpec, revisionData.get());
-    Optional<RevisionData> newRevisionData = reader.read(project, refName);
-
-    compareObjects(revisionData.get(), newRevisionData);
-
     try (Repository repo = repoManager.openRepository(project);
         TestRepository<Repository> testRepo = new TestRepository<>(repo); ) {
+      Optional<RevisionData> newRevisionData =
+          reader.read(project, repo.exactRef(refName).getObjectId(), refName);
+      compareObjects(revisionData.get(), newRevisionData);
       testRepo.fsck();
     }
   }
@@ -101,26 +102,30 @@
     String refName = RefNames.changeMetaRef(changeId);
     RefSpec refSpec = new RefSpec(refName);
 
-    Optional<RevisionData> revisionData =
-        reader.read(Project.nameKey(testRepoProjectName), refName);
-    objectUnderTest.apply(project, refSpec, revisionData.get());
+    NameKey testRepoKey = Project.nameKey(testRepoProjectName);
+    try (Repository repo = repoManager.openRepository(testRepoKey)) {
+      Optional<RevisionData> revisionData =
+          reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName);
+      objectUnderTest.apply(project, refSpec, revisionData.get());
+    }
 
     ReviewInput reviewInput = new ReviewInput();
     CommentInput comment = createCommentInput(1, 0, 1, 1, "Test comment");
     reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
     gApi.changes().id(changeId.get()).current().review(reviewInput);
 
-    Optional<RevisionData> revisionDataWithComment =
-        reader.read(Project.nameKey(testRepoProjectName), refName);
-
-    objectUnderTest.apply(project, refSpec, revisionDataWithComment.get());
-
-    Optional<RevisionData> newRevisionData = reader.read(project, refName);
-
-    compareObjects(revisionDataWithComment.get(), newRevisionData);
-
     try (Repository repo = repoManager.openRepository(project);
         TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
+      Optional<RevisionData> revisionDataWithComment =
+          reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName);
+
+      objectUnderTest.apply(project, refSpec, revisionDataWithComment.get());
+
+      Optional<RevisionData> newRevisionData =
+          reader.read(project, repo.exactRef(refName).getObjectId(), refName);
+
+      compareObjects(revisionDataWithComment.get(), newRevisionData);
+
       testRepo.fsck();
     }
   }
@@ -128,24 +133,27 @@
   @Test
   public void shouldThrowExceptionWhenParentCommitObjectIsMissing() throws Exception {
     String testRepoProjectName = project + TEST_REPLICATION_SUFFIX;
-    testRepo = cloneProject(createTestProject(testRepoProjectName));
+    NameKey createTestProject = createTestProject(testRepoProjectName);
+    try (Repository repo = repoManager.openRepository(createTestProject)) {
+      testRepo = cloneProject(createTestProject);
 
-    Result pushResult = createChange();
-    Change.Id changeId = pushResult.getChange().getId();
-    String refName = RefNames.changeMetaRef(changeId);
+      Result pushResult = createChange();
+      Change.Id changeId = pushResult.getChange().getId();
+      String refName = RefNames.changeMetaRef(changeId);
 
-    CommentInput comment = createCommentInput(1, 0, 1, 1, "Test comment");
-    ReviewInput reviewInput = new ReviewInput();
-    reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
-    gApi.changes().id(changeId.get()).current().review(reviewInput);
+      CommentInput comment = createCommentInput(1, 0, 1, 1, "Test comment");
+      ReviewInput reviewInput = new ReviewInput();
+      reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
+      gApi.changes().id(changeId.get()).current().review(reviewInput);
 
-    Optional<RevisionData> revisionData =
-        reader.read(Project.nameKey(testRepoProjectName), refName);
+      Optional<RevisionData> revisionData =
+          reader.read(createTestProject, repo.exactRef(refName).getObjectId(), refName);
 
-    RefSpec refSpec = new RefSpec(refName);
-    assertThrows(
-        MissingParentObjectException.class,
-        () -> objectUnderTest.apply(project, refSpec, revisionData.get()));
+      RefSpec refSpec = new RefSpec(refName);
+      assertThrows(
+          MissingParentObjectException.class,
+          () -> objectUnderTest.apply(project, refSpec, revisionData.get()));
+    }
   }
 
   private void compareObjects(RevisionData expected, Optional<RevisionData> actualOption) {